feat(raw-apps): runtime-error overlay + AI import-React instruction (#9966)

* feat(raw-apps): render runtime-error overlay + instruct AI to import React

Render the `runtimeError` message the raw-app preview frame now posts as a
prominent overlay, so an uncaught exception that blanks the app is visible
instead of silent. Cleared on the next successful build (via a shared
`feedPreviewIframe` helper so every preview-feed path resets it).

Add an AI app-generation instruction to begin React files with
`import React from 'react'`: raw apps bundle with the classic JSX transform,
so a missing import compiles fine but throws "React is not defined" at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(raw-apps): add the import-React rule to the shared raw-app prompt

The global AI chat and the raw-app CLI skill draw their raw-app authoring
reference from system_prompts/base/raw-app.md — a separate surface from the
app chat's inline prompt (core.ts). Add the same "always begin JSX files with
`import React`" rule there (esbuild's classic transform needs React in scope,
or JSX throws "React is not defined" at runtime) and regenerate the derived
prompt files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(raw-apps): bump ui_builder tarball to f8cecf9 (runtime-error overlay)

Pins the ui_builder artifact to windmill-code-ui-builder#15, which pushes
uncaught runtime errors from the preview iframe to the parent so the raw-app
editor can render them in the error overlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-07-07 09:32:43 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 51e1eba1cc
commit 8df613b4d2
7 changed files with 49 additions and 18 deletions
+2
View File
@@ -5561,6 +5561,8 @@ A raw app has three logical parts:
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)
The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.
+2 -2
View File
@@ -1,5 +1,5 @@
{
"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
"version": "062d11c",
"sha256": "355bf3acfb935080e4a4fa23d0509e03e4bc01aeed622db110663d7ec6c1c438"
"version": "f8cecf9",
"sha256": "2a79b838e21e06abe06119263872afbe83c4a13f7aa72c722af3e34b819f0ce3"
}
@@ -927,6 +927,7 @@ export function prepareAppSystemMessage(customPrompt?: string): ChatCompletionSy
- The frontend is bundled using esbuild with entrypoint \`index.tsx\`
- Frontend files are managed separately from backend runnables
- The \`wmill.d.ts\` file is generated automatically from the backend runnables shape
- Begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`. Raw apps bundle with the classic JSX transform, so \`React\` must be in scope wherever JSX is used — a missing import compiles fine but throws \`React is not defined\` at runtime.
### Backend
Backend runnables can be of different types:
@@ -275,6 +275,8 @@
// Latest UI Builder error; cleared on next successful build.
let buildError = $state<string | undefined>(undefined)
// Latest uncaught runtime error thrown by the rendered app; cleared on next build.
let runtimeError = $state<string | undefined>(undefined)
let logsCollapsed = $state(false)
let logsDiv: HTMLDivElement | undefined = $state(undefined)
$effect(() => {
@@ -1034,10 +1036,7 @@
// the preview iframe so it renders the new app.
if (fromUiBuilder && e.data.type === 'preview') {
lastBuild = { css: e.data.css, js: e.data.js }
previewIframe?.contentWindow?.postMessage(
{ type: 'preview', css: e.data.css, js: e.data.js },
'*'
)
feedPreviewIframe(lastBuild)
syncExternalPreview()
return
}
@@ -1067,6 +1066,16 @@
return
}
// Uncaught error/rejection from the rendered app — surfaced in the preview
// overlay so a runtime crash isn't a silent blank error.
if (fromPreview && e.data.type === 'runtimeError') {
runtimeError =
typeof e.data.message === 'string' && e.data.message
? e.data.message
: 'Unknown runtime error'
return
}
// Inspector events come exclusively from the preview iframe.
if (fromPreview && e.data.type === 'inspectorSelect') {
inspectorElement = e.data.element as InspectorElementInfo
@@ -1156,6 +1165,17 @@
}
}
// Feed a build into the inline preview iframe. Clears any prior runtime-error
// overlay first: a fresh render supersedes the old crash, and if the new
// render throws again app-preview.html re-posts `runtimeError`.
function feedPreviewIframe(build: { css: string; js: string }) {
runtimeError = undefined
previewIframe?.contentWindow?.postMessage(
{ type: 'preview', css: build.css, js: build.js },
'*'
)
}
// Full (re)feed of the detached window: theme first, then the build. Used
// when (re)attaching to a window — open, focus-reuse, load, handshake — so
// it always matches the editor's current state. Plain rebuilds use
@@ -1300,10 +1320,7 @@
// Replay the last build so the preview repopulates without
// waiting for the user to trigger another bundle.
if (lastBuild) {
previewIframe?.contentWindow?.postMessage(
{ type: 'preview', css: lastBuild.css, js: lastBuild.js },
'*'
)
feedPreviewIframe(lastBuild)
}
// Escape inside the preview exits inspect mode — the keydown fires in
// the iframe's document, so the parent window listener can't see it.
@@ -1854,14 +1871,7 @@
aria-label="Rebuild"
onclick={() => {
if (lastBuild) {
previewIframe?.contentWindow?.postMessage(
{
type: 'preview',
css: lastBuild.css,
js: lastBuild.js
},
'*'
)
feedPreviewIframe(lastBuild)
}
}}
>
@@ -1911,6 +1921,18 @@
>
</Alert>
</div>
{:else if runtimeError}
<div class="absolute top-12 left-2 right-2 z-20 isolate" role="alert">
<Alert
type="error"
title="Runtime error"
class="relative before:absolute before:inset-0 before:-z-10 before:rounded-md before:bg-surface before:content-['']"
>
<pre class="overflow-auto whitespace-pre-wrap text-xs max-h-60"
>{runtimeError}</pre
>
</Alert>
</div>
{/if}
{#if logs}
<div
+2
View File
@@ -587,6 +587,8 @@ A raw app has three logical parts:
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)
The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.
@@ -251,6 +251,8 @@ A raw app has three logical parts:
`index.tsx` is the bundling entrypoint. It typically renders a top-level `App` component. The bundler is esbuild.
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.
### Generated bindings (`wmill.d.ts` / `wmill.ts`)
The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.
+2
View File
@@ -16,6 +16,8 @@ A raw app has three logical parts:
`index.tsx` is the bundling entrypoint. It typically renders a top-level `App` component. The bundler is esbuild.
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.
### Generated bindings (`wmill.d.ts` / `wmill.ts`)
The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.