From 10bc874f05180a3b3655b74279615b1d80862907 Mon Sep 17 00:00:00 2001 From: linuztx Date: Fri, 12 Dec 2025 10:11:12 +0800 Subject: [PATCH 001/192] refactor: migrate user data to usr/ + update frontend paths --- initialize.py | 6 + python/api/api_files_get.py | 2 +- python/api/api_message.py | 4 +- python/api/message.py | 4 +- python/api/upload.py | 2 +- python/helpers/backup.py | 26 +---- python/helpers/dotenv.py | 2 +- python/helpers/email_client.py | 2 +- python/helpers/files.py | 4 + python/helpers/memory.py | 22 +++- python/helpers/migration.py | 110 ++++++++++++++++++ python/helpers/persist_chat.py | 2 +- python/helpers/secrets.py | 2 +- python/helpers/settings.py | 8 +- python/helpers/task_scheduler.py | 2 +- python/tools/browser_agent.py | 2 +- run_ui.py | 3 + .../chat/attachments/attachmentsStore.js | 4 +- .../settings/external/api-examples.html | 8 +- 19 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 python/helpers/migration.py diff --git a/initialize.py b/initialize.py index ec26227fa..1ef2d0f88 100644 --- a/initialize.py +++ b/initialize.py @@ -140,6 +140,12 @@ def initialize_preload(): import preload return defer.DeferredTask().start_task(preload.preload) +def initialize_migration(): + from python.helpers import migration + # run migration + migration.migrate_user_data() + # reload settings to ensure new paths are picked up + settings.reload_settings() def _args_override(config): # update config with runtime args diff --git a/python/api/api_files_get.py b/python/api/api_files_get.py index e021af60f..4d6dc2059 100644 --- a/python/api/api_files_get.py +++ b/python/api/api_files_get.py @@ -50,7 +50,7 @@ class ApiFilesGet(ApiHandler): if path.startswith("/a0/tmp/uploads/"): # Internal path - convert to external filename = path.replace("/a0/tmp/uploads/", "") - external_path = files.get_abs_path("tmp/uploads", filename) + external_path = files.get_abs_path("usr/uploads", filename) filename = os.path.basename(external_path) elif path.startswith("/a0/"): # Other internal Agent Zero paths diff --git a/python/api/api_message.py b/python/api/api_message.py index 385d556dd..e3fd7e06b 100644 --- a/python/api/api_message.py +++ b/python/api/api_message.py @@ -40,8 +40,8 @@ class ApiMessage(ApiHandler): # Handle attachments (base64 encoded) attachment_paths = [] if attachments: - upload_folder_int = "/a0/tmp/uploads" - upload_folder_ext = files.get_abs_path("tmp/uploads") + upload_folder_int = "/a0/usr/uploads" + upload_folder_ext = files.get_abs_path("usr/uploads") os.makedirs(upload_folder_ext, exist_ok=True) for attachment in attachments: diff --git a/python/api/message.py b/python/api/message.py index bd378e4f7..e328b0660 100644 --- a/python/api/message.py +++ b/python/api/message.py @@ -29,8 +29,8 @@ class Message(ApiHandler): attachments = request.files.getlist("attachments") attachment_paths = [] - upload_folder_int = "/a0/tmp/uploads" - upload_folder_ext = files.get_abs_path("tmp/uploads") # for development environment + upload_folder_int = "/a0/usr/uploads" + upload_folder_ext = files.get_abs_path("usr/uploads") # for development environment if attachments: os.makedirs(upload_folder_ext, exist_ok=True) diff --git a/python/api/upload.py b/python/api/upload.py index 4a596a577..14b656e04 100644 --- a/python/api/upload.py +++ b/python/api/upload.py @@ -14,7 +14,7 @@ class UploadFile(ApiHandler): for file in file_list: if file and self.allowed_file(file.filename): # Check file type filename = secure_filename(file.filename) # type: ignore - file.save(files.get_abs_path("tmp/upload", filename)) + file.save(files.get_abs_path("usr/upload", filename)) saved_filenames.append(filename) return {"filenames": saved_filenames} # Return saved filenames diff --git a/python/helpers/backup.py b/python/helpers/backup.py index 4e4873371..8ebc07033 100644 --- a/python/helpers/backup.py +++ b/python/helpers/backup.py @@ -60,28 +60,12 @@ class BackupService: # Ensure paths don't have double slashes agent_root = self.agent_zero_root.rstrip('/') - return f"""# Agent Zero Knowledge (excluding defaults) -{agent_root}/knowledge/** -!{agent_root}/knowledge/default/** - -# Agent Zero Instruments (excluding defaults) -{agent_root}/instruments/** -!{agent_root}/instruments/default/** - -# Memory (excluding embeddings cache) -{agent_root}/memory/** -!{agent_root}/memory/**/embeddings/** - -# Configuration and Settings (CRITICAL) -{agent_root}/.env -{agent_root}/tmp/settings.json -{agent_root}/tmp/secrets.env -{agent_root}/tmp/chats/** -{agent_root}/tmp/scheduler/** -{agent_root}/tmp/uploads/** - -# User data + return f"""# User data +# All persistent user data is now centralized in /usr for easier backup and restore {agent_root}/usr/** + +# Explicitly include .env +{agent_root}/usr/.env """ def _get_agent_zero_version(self) -> str: diff --git a/python/helpers/dotenv.py b/python/helpers/dotenv.py index 07ef0942b..3ce4d938f 100644 --- a/python/helpers/dotenv.py +++ b/python/helpers/dotenv.py @@ -15,7 +15,7 @@ def load_dotenv(): def get_dotenv_file_path(): - return get_abs_path(".env") + return get_abs_path("usr/.env") def get_dotenv_value(key: str, default: Any = None): # load_dotenv() diff --git a/python/helpers/email_client.py b/python/helpers/email_client.py index 741e00899..45e795ebe 100644 --- a/python/helpers/email_client.py +++ b/python/helpers/email_client.py @@ -537,7 +537,7 @@ async def read_messages( port: int = 993, username: str = "", password: str = "", - download_folder: str = "tmp/email", + download_folder: str = "usr/email", options: Optional[Dict[str, Any]] = None, filter: Optional[Dict[str, Any]] = None, ) -> List[Message]: diff --git a/python/helpers/files.py b/python/helpers/files.py index 0ed9cb06d..8fcf799c8 100644 --- a/python/helpers/files.py +++ b/python/helpers/files.py @@ -410,6 +410,10 @@ def move_dir(old_path: str, new_path: str): abs_new = get_abs_path(new_path) if not os.path.isdir(abs_old): return # nothing to rename + + # ensure parent directory exists + os.makedirs(os.path.dirname(abs_new), exist_ok=True) + try: os.rename(abs_old, abs_new) except Exception: diff --git a/python/helpers/memory.py b/python/helpers/memory.py index 8c8785c5a..d18af5453 100644 --- a/python/helpers/memory.py +++ b/python/helpers/memory.py @@ -139,7 +139,7 @@ class Memory: log_item.stream(progress="\nInitializing VectorDB") em_dir = files.get_abs_path( - "memory/embeddings" + "tmp/memory/embeddings" ) # just caching, no need to parameterize db_dir = abs_db_dir(memory_subdir) @@ -333,6 +333,16 @@ class Memory: recursive=True, ) + # load custom instruments descriptions + index = knowledge_import.load_knowledge( + log_item, + files.get_abs_path("usr/instruments"), + index, + {"area": Memory.Area.INSTRUMENTS.value}, + filename_pattern="**/*.md", + recursive=True, + ) + return index def get_document_by_id(self, id: str) -> Document | None: @@ -483,7 +493,7 @@ class Memory: def get_custom_knowledge_subdir_abs(agent: Agent) -> str: for dir in agent.config.knowledge_subdirs: if dir != "default": - return files.get_abs_path("knowledge", dir) + return files.get_abs_path("usr/knowledge", dir) raise Exception("No custom knowledge subdir set") @@ -499,7 +509,7 @@ def abs_db_dir(memory_subdir: str) -> str: return files.get_abs_path(get_project_meta_folder(memory_subdir[9:]), "memory") # standard subdirs - return files.get_abs_path("memory", memory_subdir) + return files.get_abs_path("usr/memory", memory_subdir) def abs_knowledge_dir(knowledge_subdir: str, *sub_dirs: str) -> str: @@ -511,7 +521,9 @@ def abs_knowledge_dir(knowledge_subdir: str, *sub_dirs: str) -> str: get_project_meta_folder(knowledge_subdir[9:]), "knowledge", *sub_dirs ) # standard subdirs - return files.get_abs_path("knowledge", knowledge_subdir, *sub_dirs) + if knowledge_subdir == "default": + return files.get_abs_path("knowledge", *sub_dirs) + return files.get_abs_path("usr/knowledge", knowledge_subdir, *sub_dirs) def get_memory_subdir_abs(agent: Agent) -> str: @@ -546,7 +558,7 @@ def get_existing_memory_subdirs() -> list[str]: ) # Get subdirectories from memory folder - subdirs = files.get_subdirectories("memory", exclude="embeddings") + subdirs = files.get_subdirectories("usr/memory") project_subdirs = files.get_subdirectories(get_projects_parent_folder()) for project_subdir in project_subdirs: diff --git a/python/helpers/migration.py b/python/helpers/migration.py new file mode 100644 index 000000000..b49fe6ac4 --- /dev/null +++ b/python/helpers/migration.py @@ -0,0 +1,110 @@ +import os +from python.helpers import files +from python.helpers.print_style import PrintStyle + +def migrate_user_data() -> None: + """ + Migrate user data from /tmp and other locations to /usr. + """ + + PrintStyle().print("Checking for data migration...") + + # --- Migrate Directories ------------------------------------------------------- + # Move directories from tmp/ or other source locations to usr/ + + _move_dir("tmp/chats", "usr/chats") + _move_dir("tmp/scheduler", "usr/scheduler") + _move_dir("tmp/uploads", "usr/uploads") + _move_dir("tmp/upload", "usr/upload") + _move_dir("tmp/downloads", "usr/downloads") + _move_dir("tmp/email", "usr/email") + _move_dir("knowledge/custom", "usr/knowledge") + _move_dir("instruments/custom", "usr/instruments") + + # --- Migrate Files ------------------------------------------------------------- + # Move specific configuration files to usr/ + + _move_file("tmp/settings.json", "usr/settings.json") + _move_file("tmp/secrets.env", "usr/secrets.env") + _move_file(".env", "usr/.env") + + # --- Special Migration Cases --------------------------------------------------- + + # Migrate Memory + _migrate_memory() + + # Flatten default directories (knowledge/default -> knowledge/, etc.) + # We use _merge_dir_contents because we want to move the *contents* of default/ + # into the parent directory, not move the default directory itself. + _merge_dir_contents("knowledge/default", "knowledge") + _merge_dir_contents("instruments/default", "instruments") + + # --- Cleanup ------------------------------------------------------------------- + + # Remove obsolete directories after migration + _cleanup_obsolete() + + PrintStyle().print("Migration check complete.") + +# --- Helper Functions ---------------------------------------------------------- + +def _move_dir(src: str, dst: str) -> None: + """ + Move a directory from src to dst if src exists and dst does not. + """ + if files.exists(src) and not files.exists(dst): + PrintStyle().print(f"Migrating {src} to {dst}...") + files.move_dir(src, dst) + +def _move_file(src: str, dst: str) -> None: + """ + Move a file from src to dst if src exists and dst does not. + """ + if files.exists(src) and not files.exists(dst): + PrintStyle().print(f"Migrating {src} to {dst}...") + files.move_file(src, dst) + +def _migrate_memory(base_path: str = "memory") -> None: + """ + Migrate memory subdirectories. + """ + subdirs = files.get_subdirectories(base_path) + for subdir in subdirs: + if subdir == "embeddings": + # Special case: Embeddings + _move_dir("memory/embeddings", "tmp/memory/embeddings") + else: + # Move other memory items to usr/memory + dst = f"usr/memory/{subdir}" + _move_dir(f"memory/{subdir}", dst) + +def _merge_dir_contents(src_parent: str, dst_parent: str) -> None: + """ + Moves all subdirectories from src_parent to dst_parent. + Useful for flattening structures like 'knowledge/default/*' -> 'knowledge/*'. + """ + if not files.exists(src_parent): + return + + # Iterate over subdirectories in the source parent + subdirs = files.get_subdirectories(src_parent) + for subdir in subdirs: + src = f"{src_parent}/{subdir}" + dst = f"{dst_parent}/{subdir}" + + # Move the subdirectory if it doesn't exist in destination + _move_dir(src, dst) + +def _cleanup_obsolete() -> None: + """ + Remove directories that are no longer needed. + """ + to_remove = [ + "knowledge/default", + "instruments/default", + "memory" + ] + for path in to_remove: + if files.exists(path): + PrintStyle().print(f"Removing {path}...") + files.delete_dir(path) diff --git a/python/helpers/persist_chat.py b/python/helpers/persist_chat.py index 55867e6fe..9cb708356 100644 --- a/python/helpers/persist_chat.py +++ b/python/helpers/persist_chat.py @@ -9,7 +9,7 @@ from initialize import initialize_agent from python.helpers.log import Log, LogItem -CHATS_FOLDER = "tmp/chats" +CHATS_FOLDER = "usr/chats" LOG_SIZE = 1000 CHAT_FILE_NAME = "chat.json" diff --git a/python/helpers/secrets.py b/python/helpers/secrets.py index 53ee85d9b..e87faca88 100644 --- a/python/helpers/secrets.py +++ b/python/helpers/secrets.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: # New alias-based placeholder format §§secret(KEY) ALIAS_PATTERN = r"§§secret\(([A-Za-z_][A-Za-z0-9_]*)\)" -DEFAULT_SECRETS_FILE = "tmp/secrets.env" +DEFAULT_SECRETS_FILE = "usr/secrets.env" def alias_for_key(key: str, placeholder: str = "§§secret({key})") -> str: diff --git a/python/helpers/settings.py b/python/helpers/settings.py index 9e71b7956..151c1f36b 100644 --- a/python/helpers/settings.py +++ b/python/helpers/settings.py @@ -161,7 +161,7 @@ class SettingsOutput(TypedDict): PASSWORD_PLACEHOLDER = "****PSWD****" API_KEY_PLACEHOLDER = "************" -SETTINGS_FILE = files.get_abs_path("tmp/settings.json") +SETTINGS_FILE = files.get_abs_path("usr/settings.json") _settings: Settings | None = None @@ -1343,6 +1343,12 @@ def get_settings() -> Settings: return norm +def reload_settings() -> Settings: + global _settings + _settings = None + return get_settings() + + def set_settings(settings: Settings, apply: bool = True): global _settings previous = _settings diff --git a/python/helpers/task_scheduler.py b/python/helpers/task_scheduler.py index 5f9321754..f5c3ed0c2 100644 --- a/python/helpers/task_scheduler.py +++ b/python/helpers/task_scheduler.py @@ -26,7 +26,7 @@ from python.helpers import projects import pytz from typing import Annotated -SCHEDULER_FOLDER = "tmp/scheduler" +SCHEDULER_FOLDER = "usr/scheduler" # ---------------------- # Task Models diff --git a/python/tools/browser_agent.py b/python/tools/browser_agent.py index 6d5f085b2..d9a53f822 100644 --- a/python/tools/browser_agent.py +++ b/python/tools/browser_agent.py @@ -56,7 +56,7 @@ class State: disable_security=True, chromium_sandbox=False, accept_downloads=True, - downloads_path=files.get_abs_path("tmp/downloads"), + downloads_path=files.get_abs_path("usr/downloads"), allowed_domains=["*", "http://*", "https://*"], executable_path=pw_binary, keep_alive=True, diff --git a/run_ui.py b/run_ui.py index 1691f69e7..9872fbbb3 100644 --- a/run_ui.py +++ b/run_ui.py @@ -190,6 +190,9 @@ async def serve_index(): def run(): PrintStyle().print("Initializing framework...") + # migrate data before anything else + initialize.initialize_migration() + # Suppress only request logs but keep the startup messages from werkzeug.serving import WSGIRequestHandler from werkzeug.serving import make_server diff --git a/webui/components/chat/attachments/attachmentsStore.js b/webui/components/chat/attachments/attachmentsStore.js index 7e17b9b55..08b7815fd 100644 --- a/webui/components/chat/attachments/attachmentsStore.js +++ b/webui/components/chat/attachments/attachmentsStore.js @@ -259,11 +259,11 @@ const model = { // Generate server-side API URL for file (for device sync) getServerImgUrl(filename) { - return `/image_get?path=/a0/tmp/uploads/${encodeURIComponent(filename)}`; + return `/image_get?path=/a0/usr/uploads/${encodeURIComponent(filename)}`; }, getServerFileUrl(filename) { - return `/a0/tmp/uploads/${encodeURIComponent(filename)}`; + return `/a0/usr/uploads/${encodeURIComponent(filename)}`; }, // Check if file is an image based on extension diff --git a/webui/components/settings/external/api-examples.html b/webui/components/settings/external/api-examples.html index 08569a1e9..f0a2ce7f6 100644 --- a/webui/components/settings/external/api-examples.html +++ b/webui/components/settings/external/api-examples.html @@ -146,7 +146,7 @@

Parameters:
- • paths (array, required): Array of file paths to retrieve (e.g., ["/a0/tmp/uploads/file.txt"]) + • paths (array, required): Array of file paths to retrieve (e.g., ["/a0/usr/uploads/file.txt"])

Headers: X-API-KEY (required), Content-Type: application/json @@ -562,8 +562,8 @@ async function getFiles(filePaths) { // Example 1: Get specific files const filePaths = [ - "/a0/tmp/uploads/document.txt", - "/a0/tmp/uploads/data.json" + "/a0/usr/uploads/document.txt", + "/a0/usr/uploads/data.json" ]; getFiles(filePaths); @@ -590,7 +590,7 @@ async function attachmentWorkflow() { console.log('Message sent with attachment'); // Step 2: Retrieve the uploaded file - const retrievedFiles = await getFiles(["/a0/tmp/uploads/test.txt"]); + const retrievedFiles = await getFiles(["/a0/usr/uploads/test.txt"]); if (retrievedFiles && retrievedFiles["test.txt"]) { const originalContent = atob(retrievedFiles["test.txt"]); From 5b6fbea1ed81698ba5c13bb1e536781ad3458c96 Mon Sep 17 00:00:00 2001 From: linuztx Date: Fri, 12 Dec 2025 11:45:56 +0800 Subject: [PATCH 002/192] migration: add overwrite support for .env migration --- python/helpers/migration.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/python/helpers/migration.py b/python/helpers/migration.py index b49fe6ac4..ea50c10c2 100644 --- a/python/helpers/migration.py +++ b/python/helpers/migration.py @@ -26,7 +26,7 @@ def migrate_user_data() -> None: _move_file("tmp/settings.json", "usr/settings.json") _move_file("tmp/secrets.env", "usr/secrets.env") - _move_file(".env", "usr/.env") + _move_file(".env", "usr/.env", overwrite=True) # --- Special Migration Cases --------------------------------------------------- @@ -48,19 +48,21 @@ def migrate_user_data() -> None: # --- Helper Functions ---------------------------------------------------------- -def _move_dir(src: str, dst: str) -> None: +def _move_dir(src: str, dst: str, overwrite: bool = False) -> None: """ Move a directory from src to dst if src exists and dst does not. """ - if files.exists(src) and not files.exists(dst): + if files.exists(src) and (not files.exists(dst) or overwrite): PrintStyle().print(f"Migrating {src} to {dst}...") + if overwrite and files.exists(dst): + files.delete_dir(dst) files.move_dir(src, dst) -def _move_file(src: str, dst: str) -> None: +def _move_file(src: str, dst: str, overwrite: bool = False) -> None: """ Move a file from src to dst if src exists and dst does not. """ - if files.exists(src) and not files.exists(dst): + if files.exists(src) and (not files.exists(dst) or overwrite): PrintStyle().print(f"Migrating {src} to {dst}...") files.move_file(src, dst) From 4d11a2620d066a513235e1d2063d3493498e1f1d Mon Sep 17 00:00:00 2001 From: linuztx Date: Fri, 12 Dec 2025 13:05:21 +0800 Subject: [PATCH 003/192] migration: reload .env after moving to usr/ to update config --- initialize.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/initialize.py b/initialize.py index 1ef2d0f88..eb6120fdd 100644 --- a/initialize.py +++ b/initialize.py @@ -141,9 +141,11 @@ def initialize_preload(): return defer.DeferredTask().start_task(preload.preload) def initialize_migration(): - from python.helpers import migration + from python.helpers import migration, dotenv # run migration migration.migrate_user_data() + # reload .env as it might have been moved + dotenv.load_dotenv() # reload settings to ensure new paths are picked up settings.reload_settings() From 2b0c97f654f65c0f2794a311f2a2ccf168b2bd2b Mon Sep 17 00:00:00 2001 From: linuztx Date: Fri, 12 Dec 2025 13:54:30 +0800 Subject: [PATCH 004/192] migration: force overwrite for scheduler, knowledge, and instruments dirs --- python/helpers/migration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/helpers/migration.py b/python/helpers/migration.py index ea50c10c2..e27a419bc 100644 --- a/python/helpers/migration.py +++ b/python/helpers/migration.py @@ -13,13 +13,13 @@ def migrate_user_data() -> None: # Move directories from tmp/ or other source locations to usr/ _move_dir("tmp/chats", "usr/chats") - _move_dir("tmp/scheduler", "usr/scheduler") + _move_dir("tmp/scheduler", "usr/scheduler", overwrite=True) _move_dir("tmp/uploads", "usr/uploads") _move_dir("tmp/upload", "usr/upload") _move_dir("tmp/downloads", "usr/downloads") _move_dir("tmp/email", "usr/email") - _move_dir("knowledge/custom", "usr/knowledge") - _move_dir("instruments/custom", "usr/instruments") + _move_dir("knowledge/custom", "usr/knowledge", overwrite=True) + _move_dir("instruments/custom", "usr/instruments", overwrite=True) # --- Migrate Files ------------------------------------------------------------- # Move specific configuration files to usr/ From 844af16748c429c2be99c4fb4793212f77504e9e Mon Sep 17 00:00:00 2001 From: linuztx Date: Fri, 12 Dec 2025 15:23:57 +0800 Subject: [PATCH 005/192] migration: correct custom knowledge directory path mapping --- python/helpers/memory.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/helpers/memory.py b/python/helpers/memory.py index d18af5453..49ec7db4c 100644 --- a/python/helpers/memory.py +++ b/python/helpers/memory.py @@ -493,6 +493,8 @@ class Memory: def get_custom_knowledge_subdir_abs(agent: Agent) -> str: for dir in agent.config.knowledge_subdirs: if dir != "default": + if dir == "custom": + return files.get_abs_path("usr/knowledge") return files.get_abs_path("usr/knowledge", dir) raise Exception("No custom knowledge subdir set") @@ -523,6 +525,8 @@ def abs_knowledge_dir(knowledge_subdir: str, *sub_dirs: str) -> str: # standard subdirs if knowledge_subdir == "default": return files.get_abs_path("knowledge", *sub_dirs) + if knowledge_subdir == "custom": + return files.get_abs_path("usr/knowledge", *sub_dirs) return files.get_abs_path("usr/knowledge", knowledge_subdir, *sub_dirs) From c07bbba4da552576183aec9617f58b069d021172 Mon Sep 17 00:00:00 2001 From: Wabifocus Date: Sun, 21 Dec 2025 01:09:23 -0800 Subject: [PATCH 006/192] InlineButtonConfirmations --- .../modals/file-browser/file-browser-store.js | 5 +- .../modals/file-browser/file-browser.html | 43 ++------------ .../settings/memory/memory-dashboard-store.js | 8 --- .../settings/memory/memory-dashboard.html | 4 +- .../settings/memory/memory-detail-modal.html | 2 +- .../components/sidebar/chats/chats-list.html | 25 ++------ .../components/sidebar/tasks/tasks-list.html | 29 ++++----- webui/css/buttons.css | 48 +++++++++++++++ webui/js/confirmClick.js | 59 +++++++++++++++++++ webui/js/initFw.js | 4 ++ webui/js/scheduler.js | 5 -- 11 files changed, 139 insertions(+), 93 deletions(-) create mode 100644 webui/js/confirmClick.js diff --git a/webui/components/modals/file-browser/file-browser-store.js b/webui/components/modals/file-browser/file-browser-store.js index 3764f7215..7825b37b0 100644 --- a/webui/components/modals/file-browser/file-browser-store.js +++ b/webui/components/modals/file-browser/file-browser-store.js @@ -172,7 +172,6 @@ const model = { // --- File actions -------------------------------------------------------- async deleteFile(file) { - if (!confirm(`Are you sure you want to delete ${file.name}?`)) return; try { const resp = await fetchApi("/delete_work_dir_file", { method: "POST", @@ -186,9 +185,9 @@ const model = { this.browser.entries = this.browser.entries.filter( (e) => e.path !== file.path ); - alert("File deleted successfully."); + window.toastFrontendSuccess("File deleted successfully", "File Deleted"); } else { - alert(`Error deleting file: ${await resp.text()}`); + window.toastFrontendError(`Error deleting file: ${await resp.text()}`, "Delete Error"); } } catch (e) { window.toastFrontendError( diff --git a/webui/components/modals/file-browser/file-browser.html b/webui/components/modals/file-browser/file-browser.html index b9689fb53..0ccec66a6 100644 --- a/webui/components/modals/file-browser/file-browser.html +++ b/webui/components/modals/file-browser/file-browser.html @@ -48,11 +48,11 @@

- -
@@ -279,47 +279,12 @@ .btn-upload:active { background-color: #2b309c; } - /* Delete Button Styles */ - .delete-button { - background: none; - border: none; - color: var(--color-primary); - cursor: pointer; - width: 32px; - padding: 4px 8px; - border-radius: 4px; - transition: opacity 0.2s, background-color 0.2s; - } - .delete-button:hover { - color: #ff7878; - } - .delete-button:active { - opacity: 0.6; - } /* File Actions */ .file-actions { display: flex; gap: var(--spacing-xs); } - .action-button { - background: none; - border: none; - cursor: pointer; - width: 32px; - padding: 6px 8px; - border-radius: 4px; - transition: background-color 0.2s; - } - .download-button { - color: var(--color-primary); - } - .download-button:hover { - background-color: var(--color-border); - } - .light-mode .download-button:hover { - background-color: #c6d4de; - } /* Responsive Design */ @media (max-width: 768px) { .file-header, diff --git a/webui/components/settings/memory/memory-dashboard-store.js b/webui/components/settings/memory/memory-dashboard-store.js index bf246f5e8..fe8317370 100644 --- a/webui/components/settings/memory/memory-dashboard-store.js +++ b/webui/components/settings/memory/memory-dashboard-store.js @@ -554,14 +554,6 @@ ${memory.content_full} }, async deleteMemory(memory) { - if ( - !confirm( - `Are you sure you want to delete this memory from ${memory.area}?` - ) - ) { - return; - } - try { // Check if this is the memory currently being viewed in detail modal const isViewingThisMemory = diff --git a/webui/components/settings/memory/memory-dashboard.html b/webui/components/settings/memory/memory-dashboard.html index aaaeac15d..887f32117 100644 --- a/webui/components/settings/memory/memory-dashboard.html +++ b/webui/components/settings/memory/memory-dashboard.html @@ -235,8 +235,8 @@ content_copy - diff --git a/webui/components/settings/memory/memory-detail-modal.html b/webui/components/settings/memory/memory-detail-modal.html index 0bd45ba4c..0905bff23 100644 --- a/webui/components/settings/memory/memory-detail-modal.html +++ b/webui/components/settings/memory/memory-detail-modal.html @@ -35,7 +35,7 @@ title="Edit Memory"> edit_document - diff --git a/webui/components/sidebar/chats/chats-list.html b/webui/components/sidebar/chats/chats-list.html index 7a11ec98f..ab464d58f 100644 --- a/webui/components/sidebar/chats/chats-list.html +++ b/webui/components/sidebar/chats/chats-list.html @@ -22,7 +22,9 @@ - + @@ -110,28 +112,9 @@ font-size: var(--font-size-small); } - .edit-button { + .chat-container .btn-icon-action { flex-shrink: 0; margin-right: 8px; - background-color: transparent; - border: 1px solid var(--color-border); - border-radius: 0.1875rem; - color: var(--color-primary); - cursor: pointer; - padding: 0.125rem 0.5rem; - transition: all var(--transition-speed) ease-in-out; - width: 2rem; - height: 2rem; - } - - .edit-button:hover { - border-color: var(--color-primary); - background-color: #32455690; - } - - .edit-button:active { - background-color: #131a2090; - color: rgba(253, 253, 253, 0.35); } .empty-list-message { diff --git a/webui/components/sidebar/tasks/tasks-list.html b/webui/components/sidebar/tasks/tasks-list.html index bf51bfabe..0344fb44f 100644 --- a/webui/components/sidebar/tasks/tasks-list.html +++ b/webui/components/sidebar/tasks/tasks-list.html @@ -38,20 +38,20 @@ - - - +
+ + + +
@@ -138,6 +138,7 @@ .light-mode .task-name:hover { background-color: rgba(0,0,0,0.05); } .task-info-line { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: 2px; } + .task-actions { display: flex; gap: 0.375rem; margin-left: auto; } .task-container { width: 100%; diff --git a/webui/css/buttons.css b/webui/css/buttons.css index 8836591fd..db8331488 100644 --- a/webui/css/buttons.css +++ b/webui/css/buttons.css @@ -61,3 +61,51 @@ padding-left: 0.75rem; padding-right: 0.75rem; } + +/* Inline Confirmation State */ +.confirming { + border-color: var(--color-highlight) !important; + background-color: var(--color-highlight) !important; + color: #fff !important; +} + +.confirming:hover { + filter: brightness(1.1); +} + +/* Standard icon-only action button for destructive actions */ +.btn-icon-action { + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid var(--color-border); + border-radius: 0.25rem; + color: var(--color-primary); + cursor: pointer; + padding: 0.25rem; + width: 1.75rem; + height: 1.75rem; + transition: all 0.18s cubic-bezier(0.4, 0, 0.2, 1); + flex-shrink: 0; +} + +.btn-icon-action .material-symbols-outlined, +.btn-icon-action .material-icons-outlined { + font-size: 1rem; +} + +.btn-icon-action:hover { + border-color: var(--color-primary); + background-color: rgba(50, 69, 86, 0.56); +} + +.btn-icon-action:active { + background-color: rgba(19, 26, 32, 0.56); + color: rgba(253, 253, 253, 0.35); +} + +.btn-icon-action.danger:hover { + border-color: #e57373; + color: #e57373; +} diff --git a/webui/js/confirmClick.js b/webui/js/confirmClick.js new file mode 100644 index 000000000..6e5f2cdf3 --- /dev/null +++ b/webui/js/confirmClick.js @@ -0,0 +1,59 @@ +// Inline button two-click confirmation for destructive actions. +// First click arms, second click confirms, timeout resets. + +const CONFIRM_TIMEOUT = 2000; +const CONFIRM_CLASS = 'confirming'; +const CONFIRM_ICON = 'check'; + +const buttonStates = new WeakMap(); + +// Handles inline two-click confirmation for a button. +export function confirmClick(event, action) { + const button = event.currentTarget; + if (!button) return; + + const state = buttonStates.get(button); + + if (state?.confirming) { + clearTimeout(state.timeoutId); + resetButton(button, state); + buttonStates.delete(button); + action(); + } else { + const iconEl = button.querySelector('.material-symbols-outlined, .material-icons-outlined'); + const originalIcon = iconEl?.textContent?.trim(); + + const newState = { + confirming: true, + originalIcon, + timeoutId: setTimeout(() => { + resetButton(button, newState); + buttonStates.delete(button); + }, CONFIRM_TIMEOUT) + }; + + buttonStates.set(button, newState); + + // Apply confirming state + button.classList.add(CONFIRM_CLASS); + if (iconEl) { + iconEl.textContent = CONFIRM_ICON; + } + } +} + +// Reset button to original state +function resetButton(button, state) { + button.classList.remove(CONFIRM_CLASS); + const iconEl = button.querySelector('.material-symbols-outlined, .material-icons-outlined'); + if (iconEl && state.originalIcon) { + iconEl.textContent = state.originalIcon; + } +} + +// Register Alpine magic helper +export function registerAlpineMagic() { + if (globalThis.Alpine) { + Alpine.magic('confirmClick', () => confirmClick); + } +} diff --git a/webui/js/initFw.js b/webui/js/initFw.js index c758b6d6c..f56ba15d3 100644 --- a/webui/js/initFw.js +++ b/webui/js/initFw.js @@ -1,6 +1,7 @@ import * as initializer from "./initializer.js"; import * as _modals from "./modals.js"; import * as _components from "./components.js"; +import { registerAlpineMagic } from "./confirmClick.js"; // initialize required elements await initializer.initialize(); @@ -8,6 +9,9 @@ await initializer.initialize(); // import alpine library await import("../vendor/alpine/alpine.min.js"); +// register $confirmClick magic helper for inline button confirmations +registerAlpineMagic(); + // add x-destroy directive to alpine Alpine.directive( "destroy", diff --git a/webui/js/scheduler.js b/webui/js/scheduler.js index 96cb548dd..63d5b65ba 100644 --- a/webui/js/scheduler.js +++ b/webui/js/scheduler.js @@ -1090,11 +1090,6 @@ const fullComponentImplementation = function() { // Delete a task async deleteTask(taskId) { - // Confirm deletion - if (!confirm('Are you sure you want to delete this task? This action cannot be undone.')) { - return; - } - try { // if we delete selected context, switch to another first From 6d31f86f13f576511f2aaf06f9e8677f4e85783d Mon Sep 17 00:00:00 2001 From: Wabifocus Date: Sun, 21 Dec 2025 08:37:40 -0800 Subject: [PATCH 007/192] css & html cleanup --- webui/components/sidebar/tasks/tasks-list.html | 2 -- webui/css/buttons.css | 5 ----- 2 files changed, 7 deletions(-) diff --git a/webui/components/sidebar/tasks/tasks-list.html b/webui/components/sidebar/tasks/tasks-list.html index 0344fb44f..e1aa7f1fc 100644 --- a/webui/components/sidebar/tasks/tasks-list.html +++ b/webui/components/sidebar/tasks/tasks-list.html @@ -68,8 +68,6 @@ - - + diff --git a/webui/components/sidebar/tasks/tasks-store.js b/webui/components/sidebar/tasks/tasks-store.js index f014207f0..bedacd27f 100644 --- a/webui/components/sidebar/tasks/tasks-store.js +++ b/webui/components/sidebar/tasks/tasks-store.js @@ -1,5 +1,7 @@ import { createStore } from "/js/AlpineStore.js"; import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"; +import { store as schedulerStore } from "/components/settings/scheduler/scheduler-store.js"; +import { store as settingsStore } from "/components/settings/settings-store.js"; // Tasks sidebar store: tasks list and selected task id const model = { @@ -50,8 +52,9 @@ const model = { }, openDetail(taskId) { - if (globalThis.openTaskDetail) { - globalThis.openTaskDetail(taskId); + // Use the new settings modal store to open scheduler task detail + if (settingsStore?.openSchedulerTaskDetail) { + settingsStore.openSchedulerTaskDetail(taskId); } }, @@ -60,8 +63,8 @@ const model = { }, deleteTask(taskId) { - if (globalThis.deleteTaskGlobal) { - globalThis.deleteTaskGlobal(taskId); + if (schedulerStore?.deleteTaskFromSidebar) { + schedulerStore.deleteTaskFromSidebar(taskId); } }, }; diff --git a/webui/components/sidebar/top-section/quick-actions.html b/webui/components/sidebar/top-section/quick-actions.html index a4d76e147..8b807e9ec 100644 --- a/webui/components/sidebar/top-section/quick-actions.html +++ b/webui/components/sidebar/top-section/quick-actions.html @@ -3,6 +3,9 @@ @@ -13,7 +16,7 @@ - + + + - - -
- - -
From 745d4ebd318924904591d58ff0cc08e53b4c1b4a Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Mon, 22 Dec 2025 06:07:48 +0100 Subject: [PATCH 012/192] fix scheduler flickering --- .../settings/scheduler/scheduler-store.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/webui/components/settings/scheduler/scheduler-store.js b/webui/components/settings/scheduler/scheduler-store.js index 1ea2be670..a65395e2d 100644 --- a/webui/components/settings/scheduler/scheduler-store.js +++ b/webui/components/settings/scheduler/scheduler-store.js @@ -549,12 +549,11 @@ const schedulerStoreModel = { startPolling() { if (this.pollingInterval) return; this.fetchTasks(); - // Poll every 5 seconds - balances responsiveness with performance this.pollingInterval = setInterval(() => { if (this.pollingActive) { this.fetchTasks(); } - }, 5000); + }, 2000); }, stopPolling() { @@ -568,7 +567,8 @@ const schedulerStoreModel = { // Data fetching ------------------------------------------------------------- async fetchTasks({ manual = false } = {}) { if (this.isCreating || this.isEditing) return; - this.isLoading = true; + if (manual) this.isLoading = true; + try { const { ok, error, tasks } = await schedulerApi.listTasks(); if (!ok) { @@ -577,12 +577,22 @@ const schedulerStoreModel = { this.hasNoTasks = true; return; } - // Only update if data actually changed - prevents DOM thrashing during polling - const newJson = JSON.stringify(tasks); - if (newJson !== JSON.stringify(this.tasks)) { - this.tasks = tasks; - } - this.hasNoTasks = tasks.length === 0; + + // Smart merge: preserve object references to prevent UI flickering + const taskMap = new Map(this.tasks.map((t) => [t.uuid, t])); + this.tasks = tasks.map((newTask) => { + const existing = taskMap.get(newTask.uuid); + if (existing) { + // Update existing object in-place if different + if (JSON.stringify(existing) !== JSON.stringify(newTask)) { + Object.assign(existing, newTask); + } + return existing; // Return the SAME object reference + } + return newTask; // New object + }); + + this.hasNoTasks = this.tasks.length === 0; } catch (error) { if (manual) this.notifyError(`Failed to fetch tasks: ${error.message}`); this.tasks = []; From d3a02482766a3950fc698562a3b5ad539f8c897f Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Mon, 22 Dec 2025 08:46:21 +0100 Subject: [PATCH 013/192] task detail ux/quality of life --- .../settings/scheduler/scheduler-store.js | 13 ++ .../scheduler/scheduler-task-detail.html | 31 ++++- .../scheduler/scheduler-task-list.html | 31 ++--- webui/css/modals.css | 1 - webui/css/settings.css | 129 +++++++++++------- webui/index.css | 32 ----- 6 files changed, 127 insertions(+), 110 deletions(-) diff --git a/webui/components/settings/scheduler/scheduler-store.js b/webui/components/settings/scheduler/scheduler-store.js index a65395e2d..e2b0afe54 100644 --- a/webui/components/settings/scheduler/scheduler-store.js +++ b/webui/components/settings/scheduler/scheduler-store.js @@ -915,6 +915,19 @@ const schedulerStoreModel = { window.closeModal(); }, + editFromDetail() { + const taskId = this.selectedTaskForDetail?.uuid; + if (!taskId) return; + this.closeTaskDetail(); + this.startEditTask(taskId); + }, + + async deleteFromDetail() { + const taskId = this.selectedTaskForDetail?.uuid; + if (!taskId) return; + await this.deleteTask(taskId); + }, + async startCreateTask() { this.isCreating = true; this.isEditing = false; diff --git a/webui/components/settings/scheduler/scheduler-task-detail.html b/webui/components/settings/scheduler/scheduler-task-detail.html index 85dae0187..050a2ac58 100644 --- a/webui/components/settings/scheduler/scheduler-task-detail.html +++ b/webui/components/settings/scheduler/scheduler-task-detail.html @@ -11,12 +11,31 @@
-

-
- +
+

+
+
+
+ + + + +
diff --git a/webui/components/settings/scheduler/scheduler-task-list.html b/webui/components/settings/scheduler/scheduler-task-list.html index 0d60e45a9..f140be8fc 100644 --- a/webui/components/settings/scheduler/scheduler-task-list.html +++ b/webui/components/settings/scheduler/scheduler-task-list.html @@ -53,57 +53,46 @@ x-effect="$el.style.display = (!$store.schedulerStore.isLoading && $store.schedulerStore.filteredTasks.length > 0) ? '' : 'none'"> - + Name - + State - Type - Project - Schedule - + Project + Last Run - Actions + Actions