diff --git a/src/ui/src/builder/BuilderHeaderMoreDropdown.vue b/src/ui/src/builder/BuilderHeaderMoreDropdown.vue
index dcff8cc2b..3f9f35fac 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"
/>
+
@@ -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(() => [
{
@@ -68,6 +92,8 @@ const importModalActions = computed(() => [
fn: () => {
importConfirmCheckbox.value = false;
importConfirmShown.value = false;
+ importErrorSummary.value = "";
+ importErrorDetails.value = "";
},
},
{
@@ -105,6 +131,8 @@ async function importModalConfirm() {
if (!file) return;
importInProgress.value = true;
+ importErrorSummary.value = "";
+ importErrorDetails.value = "";
try {
// Prepare form data
@@ -117,6 +145,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 +193,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..bd0ed7a65 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.")
+ 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):