From 45bae59e9cfc9473b646a2b5c9df174ce7f8494a Mon Sep 17 00:00:00 2001 From: Guilhem Date: Mon, 25 May 2026 20:15:14 +0200 Subject: [PATCH] feat(raw_apps): surface UI Builder build errors over the preview pane (#9316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(raw_apps): surface UI Builder build errors over the preview pane Companion to the matching change in the UI Builder repo (see linked PR), which stops rendering the build-error overlay over the VS Code editor iframe and instead emits a `buildError` postMessage on every build (message: undefined on success to clear). Listen for that message on the existing window message handler (already source-gated by the UI Builder iframe), store it in a `buildError` $state, and surface it in two places: * A red banner over the preview iframe, sibling to the existing logs overlay (`top-12 left-2 right-2 z-20` so it clears the tab bar) — failures appear right where the user looks for the rendered output. * The Preview tab's icon and label tint red (`text-red-600 dark:text-red-400`, matching the existing error convention in raw_apps) — important in single-tab mode where the preview pane is collapsed to 0px and the banner would be hidden. Done by mapping `leftPaneTabs` / `rightPaneTabs` through a small `tintPreviewOnError` helper so the source-of-truth `tabs` array is untouched (DnD, ordering, fallback selection keep using the original previewTab object). Co-Authored-By: Claude Opus 4.7 (1M context) * refactor(raw_apps): use Alert component for the build-error banner Replace the hand-rolled red div with the shared `Alert` component (`type="error"`, `title="Build failed"`). The error text stays in a `
` child so multi-line bundler output keeps its formatting, with
`max-h-60` so a long error never takes over the whole preview pane.

The absolute-positioned wrapper (`top-12 left-2 right-2 z-20`) and the
`role="alert"` move to that wrapper so the Alert component itself stays
unstyled at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) 

* fix(raw_apps): solid bg-surface backing behind build-error Alert

The Alert's error background is semi-transparent in dark mode
(`bg-red-900/40` in `common/alert/model.ts`), so the preview iframe
shows through when the banner is laid over it. Add a `::before`
pseudo on the Alert root with `bg-surface` (matched `rounded-md`,
`-z-10` so it sits behind the red bg) to give it a solid plate.

Co-Authored-By: Claude Opus 4.7 (1M context) 

* refactor(raw_apps): isolate banner stacking context, DRY tab tint chain

Two small follow-ups from review:

* Add `isolate` to the build-error banner wrapper so the `before:-z-10`
  pseudo's stacking context is pinned locally — it works today because
  `position: absolute` + `z-20` creates one, but `isolate` makes the
  dependency self-documenting and survives a future refactor that
  removes the explicit `z-20`.
* Extract `tintTabs = (ts) => ts.map(tintPreviewOnError)` so the two
  `$derived` blocks for leftPaneTabs / rightPaneTabs read identically.

Co-Authored-By: Claude Opus 4.7 (1M context) 

* chore(raw_apps): trim build-error overlay comments

Per review feedback. Keep only the load-bearing facts (bg-surface backs
the Alert's translucent red, isolate pins the pseudo stacking, the
`message: undefined` clear convention) and drop the prose context that
duplicated what the code already shows.

Co-Authored-By: Claude Opus 4.7 (1M context) 

* chore(raw_apps): bump bundled ui_builder to 00c9834

Brings in the postMessage emission from
windmill-labs/windmill-code-ui-builder#9 (merged) so this PR's host
listener actually receives `buildError` events. SHA verified against
the R2 artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) 

---------

Co-authored-by: Claude Opus 4.7 (1M context) 
---
 frontend/scripts/ui_builder_artifact.json     |  4 +--
 .../components/raw_apps/RawAppEditor.svelte   | 35 +++++++++++++++++--
 2 files changed, 35 insertions(+), 4 deletions(-)

diff --git a/frontend/scripts/ui_builder_artifact.json b/frontend/scripts/ui_builder_artifact.json
index 5685992677..05a9068963 100644
--- a/frontend/scripts/ui_builder_artifact.json
+++ b/frontend/scripts/ui_builder_artifact.json
@@ -1,5 +1,5 @@
 {
 	"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
-	"version": "b4f6219",
-	"sha256": "69575342aab968fc32a89d3410c21bf888b0f00ce5c68fe80936d3adc1359b36"
+	"version": "00c9834",
+	"sha256": "5757e5b9cbf79c20d507dc4c588640368e84b873cb48f3806ca8c67fd1aa625f"
 }
diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
index aa6658b4a9..7249f0ae05 100644
--- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
+++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
@@ -6,6 +6,7 @@
 	import RawAppEditorHeader from './RawAppEditorHeader.svelte'
 	import RawAppYamlEditor, { type RawAppYamlUpdate } from './RawAppYamlEditor.svelte'
 	import type Drawer from '../common/drawer/Drawer.svelte'
+	import Alert from '../common/alert/Alert.svelte'
 	import { type Policy, WorkspaceService } from '$lib/gen'
 	import DiffDrawer from '../DiffDrawer.svelte'
 	import { deepEqual } from 'fast-equals'
@@ -195,6 +196,9 @@
 	// them as an overlay inside the preview pane (right side) so they're
 	// visually tied to the build output, not to the source editor.
 	let logs = $state('')
+
+	// Latest UI Builder error; cleared on next successful build.
+	let buildError = $state(undefined)
 	let logsCollapsed = $state(false)
 	let logsDiv: HTMLDivElement | undefined = $state(undefined)
 	$effect(() => {
@@ -229,13 +233,20 @@
 				? 'file'
 				: 'runnable'
 	)
+	// Tint the Preview tab red so the error is visible when Preview isn't active.
+	function tintPreviewOnError(t: TabItem): TabItem {
+		if (t.id !== PREVIEW_TAB_ID || !buildError) return t
+		const errClass = 'text-red-600 dark:text-red-400'
+		return { ...t, iconClass: errClass, labelClass: errClass }
+	}
+	const tintTabs = (ts: TabItem[]) => ts.map(tintPreviewOnError)
 	// Single mode: both bars mirror the full list (the visible pane carries
 	// every tab). Split mode: left = files/runnables, right = Preview only.
 	const leftPaneTabs = $derived(
-		splitWithPreview ? tabs.filter((t) => t.id !== PREVIEW_TAB_ID) : tabs
+		tintTabs(splitWithPreview ? tabs.filter((t) => t.id !== PREVIEW_TAB_ID) : tabs)
 	)
 	const rightPaneTabs = $derived(
-		splitWithPreview ? tabs.filter((t) => t.id === PREVIEW_TAB_ID) : tabs
+		tintTabs(splitWithPreview ? tabs.filter((t) => t.id === PREVIEW_TAB_ID) : tabs)
 	)
 	// In split mode the right bar always highlights Preview, regardless of the
 	// left pane's active file/runnable.
@@ -938,6 +949,12 @@
 			return
 		}
 
+		// `message: undefined` arrives on the next successful build and clears the banner.
+		if (fromUiBuilder && e.data.type === 'buildError') {
+			buildError = typeof e.data.message === 'string' ? e.data.message : undefined
+			return
+		}
+
 		// Inspector events come exclusively from the preview iframe.
 		if (fromPreview && e.data.type === 'inspectorSelect') {
 			inspectorElement = e.data.element as InspectorElementInfo
@@ -1557,6 +1574,20 @@
 									src="/ui_builder/app-preview.html"
 									class="w-full flex-1 block"
 								>
+								{#if buildError}
+									
+									
+								{/if}
 								{#if logs}