Two of the three civitai app init templates — including the default — appear to scaffold an app that can never signal readiness to the host, so the host times out and collapses it. This is the same symptom as #196 but a different cause, and the dev-tunnel preflight added in #198 deliberately does not catch it (it is gated to SDK projects).
The contract
The host marks a block ready only on a BLOCK_READY message. In civitai/civitai, src/components/AppBlocks/PageBlockHost.tsx has exactly one transition into ready, inside the BLOCK_READY handler:
// PageBlockHost.tsx:864
const off = onMessage<unknown>('BLOCK_READY', () => {
...
setStatus((current) => (current === 'loading' ? 'ready' : current)); // :871
Enumerating every setStatus( in that file, :871 is the only path to 'ready'. The failure path is :733, loading → 'timeout', after BLOCK_READY_TIMEOUT_MS = 10_000 (pageBlockHostLogic.ts:571).
A timeout is not a visible error card. hostRenderDecision.ts documents:
Every TERMINAL-FAILURE state collapses → 'collapse' (the host renders null; the slot takes no space — no visible broken card, no reserved gap). This covers: … 'timeout' (no BLOCK_READY in 10s) …
Who actually sends it
Not the SDK. @civitai/[email protected]'s runtime JS contains no BLOCK_READY at all — only a type in dist/blocks/messages.d.ts:467 and a README line. The sender is @civitai/blocks-react:
// @civitai/[email protected] — dist/internal/iframeTransport.js:311
this.dispatch('BLOCK_READY', { height: 0 });
What each template does
| template |
depends on blocks-react |
messages it sends |
reaches data-block-ready="true" |
page-money |
yes |
via IframeTransport |
yes |
page-vite |
no |
RESIZE_IFRAME only |
no |
static (default) |
no |
RESIZE_IFRAME only |
no |
Grepping the whole of internal/scaffold/templates/page-vite/ and .../static/ for BLOCK_ returns zero matches. Each contains exactly one postMessage call, sending RESIZE_IFRAME — which PageBlockHost.tsx does not reference at any line, so it is not treated as a liveness signal either.
All three manifests declare page + iframe, so all three route through the same host path. static is the default: civitai app init "My Block" with no --template produces it (internal/cmd/app_init.go:59).
Ruled out
- A server-injected ready shim. No bootstrap/shim injection found in the block-serving path; every server-side
BLOCK_READY reference is the analytics beacon (app-views.service.ts, track/block-render.ts), not a sender.
- A render mode that skips the handshake.
hostRenderDecision collapses on timeout regardless, and IframeHost.tsx:1956 gates data-block-ready on the same isReady.
Not verified
I have not loaded a scaffolded page-vite or static app in a real host browser and watched it time out — the tunnel endpoint is not exposed and the feature is kill-switched, the same limit recorded on #198. The evidence above is read directly from both sides of the contract (host source and template source), not observed end-to-end. Worth one browser confirmation before acting, since a fix touches the default template.
Possible directions
Not prescribing one — the trade-off is a product call:
- Emit
BLOCK_READY from the raw templates. A few lines of window.parent.postMessage({ type: 'BLOCK_READY', height: 0 }, '*') keeps them dependency-free. Needs care: IframeTransport freezes parentOrigin to the first sender and dedupes re-inits (iframeTransport.js:291-295), so a hand-rolled version should not re-emit on every render.
- Give them
@civitai/blocks-react. Correct by construction, but turns the "no-build" static template into a build.
- Document them as non-hosted previews and change the default to one that works.
Whichever way, a scaffold seam guard like internal/scaffold/dev_embed_contract_test.go — asserting each template that declares page emits the ready ack — would keep this from regressing silently.
Two of the three
civitai app inittemplates — including the default — appear to scaffold an app that can never signal readiness to the host, so the host times out and collapses it. This is the same symptom as #196 but a different cause, and the dev-tunnel preflight added in #198 deliberately does not catch it (it is gated to SDK projects).The contract
The host marks a block ready only on a
BLOCK_READYmessage. Incivitai/civitai,src/components/AppBlocks/PageBlockHost.tsxhas exactly one transition intoready, inside theBLOCK_READYhandler:Enumerating every
setStatus(in that file,:871is the only path to'ready'. The failure path is:733,loading → 'timeout', afterBLOCK_READY_TIMEOUT_MS = 10_000(pageBlockHostLogic.ts:571).A timeout is not a visible error card.
hostRenderDecision.tsdocuments:Who actually sends it
Not the SDK.
@civitai/[email protected]'s runtime JS contains noBLOCK_READYat all — only a type indist/blocks/messages.d.ts:467and a README line. The sender is@civitai/blocks-react:What each template does
blocks-reactdata-block-ready="true"page-moneyIframeTransportpage-viteRESIZE_IFRAMEonlystatic(default)RESIZE_IFRAMEonlyGrepping the whole of
internal/scaffold/templates/page-vite/and.../static/forBLOCK_returns zero matches. Each contains exactly onepostMessagecall, sendingRESIZE_IFRAME— whichPageBlockHost.tsxdoes not reference at any line, so it is not treated as a liveness signal either.All three manifests declare
page+iframe, so all three route through the same host path.staticis the default:civitai app init "My Block"with no--templateproduces it (internal/cmd/app_init.go:59).Ruled out
BLOCK_READYreference is the analytics beacon (app-views.service.ts,track/block-render.ts), not a sender.hostRenderDecisioncollapses ontimeoutregardless, andIframeHost.tsx:1956gatesdata-block-readyon the sameisReady.Not verified
I have not loaded a scaffolded
page-viteorstaticapp in a real host browser and watched it time out — the tunnel endpoint is not exposed and the feature is kill-switched, the same limit recorded on #198. The evidence above is read directly from both sides of the contract (host source and template source), not observed end-to-end. Worth one browser confirmation before acting, since a fix touches the default template.Possible directions
Not prescribing one — the trade-off is a product call:
BLOCK_READYfrom the raw templates. A few lines ofwindow.parent.postMessage({ type: 'BLOCK_READY', height: 0 }, '*')keeps them dependency-free. Needs care:IframeTransportfreezesparentOriginto the first sender and dedupes re-inits (iframeTransport.js:291-295), so a hand-rolled version should not re-emit on every render.@civitai/blocks-react. Correct by construction, but turns the "no-build"statictemplate into a build.Whichever way, a scaffold seam guard like
internal/scaffold/dev_embed_contract_test.go— asserting each template that declarespageemits the ready ack — would keep this from regressing silently.