From 3bbbafab2bcd9a07212c41399b4e0cb95c229949 Mon Sep 17 00:00:00 2001 From: UladzislauK-Writer Date: Wed, 4 Feb 2026 10:02:42 +0100 Subject: [PATCH 1/2] fix: don't import broken agents --- .../src/builder/BuilderHeaderMoreDropdown.vue | 73 +++++++++++++++++++ src/writer/app_runner.py | 3 + src/writer/serve.py | 8 +- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/ui/src/builder/BuilderHeaderMoreDropdown.vue b/src/ui/src/builder/BuilderHeaderMoreDropdown.vue index dcff8cc2b..dfb39dd31 100644 --- a/src/ui/src/builder/BuilderHeaderMoreDropdown.vue +++ b/src/ui/src/builder/BuilderHeaderMoreDropdown.vue @@ -23,6 +23,28 @@ v-model="importConfirmCheckbox" label="Yes, I want to replace the current agent with the imported file" /> +
+

+ Failed to import the agent +

+ +

+ {{ importErrorSummary }} +

+ +
+ Show details +
{{
+					importErrorDetails
+				}}
+
+
@@ -60,6 +82,8 @@ const toasts = useToasts(); const importInProgress = ref(false); const importConfirmShown = ref(false); const importConfirmCheckbox = ref(false); +const importErrorSummary = ref(""); +const importErrorDetails = ref(""); const importModalActions = computed(() => [ { @@ -117,6 +141,9 @@ async function importModalConfirm() { body: formData, }); if (!response.ok) { + const parsed = await response.json(); + importErrorSummary.value = parsed?.detail?.summary; + importErrorDetails.value = parsed?.detail?.details; throw new Error("Failed to connect to import API"); } importConfirmShown.value = false; @@ -162,4 +189,50 @@ async function onSelect(key: string) { .BuilderHeaderMoreDropdown:deep(.SharedMoreDropdown__dropdown) { min-width: 220px; } + +.BuilderHeaderMoreDropdown__importError { + margin-top: 16px; + padding: 12px 14px; + border-radius: 8px; + background: rgba(255, 149, 0, 0.08); + border: 1px solid rgba(255, 149, 0, 0.35); + grid-column: 2 / 4; +} + +.BuilderHeaderMoreDropdown__importError__header { + font-weight: 600; + color: var(--wdsColorOrange4); + font-size: 14px; +} + +.BuilderHeaderMoreDropdown__importError__summary { + margin-top: 6px; + font-size: 14px; +} + +.BuilderHeaderMoreDropdown__importError__details { + margin-top: 10px; +} + +.BuilderHeaderMoreDropdown__importError__details summary { + cursor: pointer; + font-size: 12px; + user-select: none; +} + +.BuilderHeaderMoreDropdown__importError__details summary:hover { + text-decoration: underline; +} + +.BuilderHeaderMoreDropdown__importError__trace { + margin-top: 8px; + max-height: 220px; + overflow: auto; + padding: 10px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.05); + font-size: 12px; + line-height: 1.4; + white-space: pre; +} diff --git a/src/writer/app_runner.py b/src/writer/app_runner.py index 5395a70c3..dde662f7c 100644 --- a/src/writer/app_runner.py +++ b/src/writer/app_runner.py @@ -1207,6 +1207,9 @@ async def import_zip(self, zip_path: str): if not os.path.isdir(wf_dir_path): raise ValueError(".wf directory not found alongside main.py in the archive.") + # Parse files to ensure there are no errors + wf_project.read_files(main_py_dir) + # Passed all checks; replace current app contents logging.info("Copying app at %s", main_py_dir) diff --git a/src/writer/serve.py b/src/writer/serve.py index 5f00838bb..1def4dc5d 100644 --- a/src/writer/serve.py +++ b/src/writer/serve.py @@ -263,9 +263,9 @@ async def export_zip(): @app.post("/api/import") async def import_zip(file: UploadFile = File(...)): if serve_mode != "edit": - raise HTTPException(status_code=403, detail="Invalid mode.") + raise HTTPException(status_code=403, detail={"summary": "Invalid mode. Expected 'edit'"}) if not file.filename or not file.filename.endswith(".zip"): - raise HTTPException(status_code=400, detail="Only .zip files are supported.") + raise HTTPException(status_code=400, detail={"summary": "Only .zip files are supported."}) MAX_FILE_SIZE = 200 * 1024 * 1024 @@ -278,13 +278,13 @@ async def import_zip(file: UploadFile = File(...)): if size > MAX_FILE_SIZE: tmp.close() os.unlink(tmp.name) - raise HTTPException(status_code=413, detail=f"File too large. Max file size: {MAX_FILE_SIZE}") + raise HTTPException(status_code=413, detail={"summary": f"File too large. Max file size: {MAX_FILE_SIZE}"}) tmp.write(chunk) tmp_path = tmp.name await app_runner.import_zip(tmp_path) os.remove(tmp_path) except ValueError: - raise HTTPException(status_code=400, detail="Invalid upload.") + raise HTTPException(status_code=400, detail={"summary": "Invalid archive contents", "details": traceback.format_exc()}) @app.post("/api/autogen") async def autogen(requestBody: AutogenRequestBody, request: Request): From 5ec2d9b11fd5b93fe5f5738c9822d3a74e8d0368 Mon Sep 17 00:00:00 2001 From: UladzislauK-Writer Date: Wed, 4 Feb 2026 10:11:49 +0100 Subject: [PATCH 2/2] fix: clear errors --- src/ui/src/builder/BuilderHeaderMoreDropdown.vue | 4 ++++ src/writer/serve.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/src/builder/BuilderHeaderMoreDropdown.vue b/src/ui/src/builder/BuilderHeaderMoreDropdown.vue index dfb39dd31..3f9f35fac 100644 --- a/src/ui/src/builder/BuilderHeaderMoreDropdown.vue +++ b/src/ui/src/builder/BuilderHeaderMoreDropdown.vue @@ -92,6 +92,8 @@ const importModalActions = computed(() => [ fn: () => { importConfirmCheckbox.value = false; importConfirmShown.value = false; + importErrorSummary.value = ""; + importErrorDetails.value = ""; }, }, { @@ -129,6 +131,8 @@ async function importModalConfirm() { if (!file) return; importInProgress.value = true; + importErrorSummary.value = ""; + importErrorDetails.value = ""; try { // Prepare form data diff --git a/src/writer/serve.py b/src/writer/serve.py index 1def4dc5d..bd0ed7a65 100644 --- a/src/writer/serve.py +++ b/src/writer/serve.py @@ -283,8 +283,8 @@ async def import_zip(file: UploadFile = File(...)): tmp_path = tmp.name await app_runner.import_zip(tmp_path) os.remove(tmp_path) - except ValueError: - raise HTTPException(status_code=400, detail={"summary": "Invalid archive contents", "details": traceback.format_exc()}) + except ValueError as e: + raise HTTPException(status_code=400, detail={"summary": "Invalid archive contents", "details": traceback.format_exc()}) from e @app.post("/api/autogen") async def autogen(requestBody: AutogenRequestBody, request: Request):