11 KiB
APP-4267: Mermaid render failures show an explicit callout — Tech Spec
Product spec: specs/APP-4267/PRODUCT.md
Linear issue: https://linear.app/warpdotdev/issue/APP-4267/show-failure-callout-when-mermaid-diagram-rendering-gets-stuck
Context
Mermaid rendering is implemented as an async image asset layered on top of the rich-text markdown renderer.
crates/editor/src/content/text.rs (721-729)classifies fenced Mermaid blocks asCodeBlockType::MermaidwhenFeatureFlag::MarkdownMermaidis enabled.crates/editor/src/content/edit.rs (671-726)converts styled Mermaid code blocks intoLayoutTask::MermaidDiagramand callsmermaid_diagram_layout.crates/editor/src/content/edit.rs (1031-1067)converts that layout task intoBlockItem::MermaidDiagramwhile preserving the source block content length.crates/editor/src/content/mermaid_diagram.rs:20defines the current default pending height as 10 line-heights.crates/editor/src/content/mermaid_diagram.rs (28-44)creates anAssetSource::Asyncwhose fetch future callsmermaid_to_svg::render_mermaid_to_svg.crates/editor/src/content/mermaid_diagram.rs (47-66)computes Mermaid block dimensions from the loaded SVG and falls back to the default placeholder height while the asset is not loaded.crates/editor/src/render/element/mermaid.rs (33-66)renders a Mermaid block by wrappingImage::new(...).contain().before_load(...)with the “Rendering Mermaid diagram…” placeholder.crates/editor/src/render/element/mermaid.rs (68-112)paints the code-block-like rounded background/border and then paints the image element into the content rect.crates/warpui_core/src/elements/image.rs (316-358)currently paintsbefore_load_elementforLoading,Evicted, andFailedToLoad; the shared image element has no separate failed-state element.crates/warpui_core/src/assets/asset_cache.rs (284-330)stores new async assets asLoadingand spawns the fetch future once per async asset ID.crates/warpui_core/src/assets/asset_cache.rs (415-486)promotes async assets toLoadedorFailedToLoadonly when the background future resolves. There is already an attached implementation branch,origin/oz-agent/APP-4267/mermaid-failure-callout, that adds anImage::on_load_failureelement and a compact Mermaid failure notice forAssetState::FailedToLoad. That direction fits the current architecture, but it should be tightened to cover the product-level timeout behavior: a truly stuck render can remainAssetState::Loadingforever because the asset cache only transitions when the fetch future resolves.
Proposed changes
1. Add a Mermaid-specific failed height
In crates/editor/src/content/mermaid_diagram.rs, add a compact failed-height multiplier next to the existing pending-height multiplier:
DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIERremains the pending/success fallback height.FAILED_MERMAID_HEIGHT_LINE_MULTIPLIERshould be 2 line-heights for known permanent failures. Updatemermaid_diagram_layoutso it distinguishes:LoadedSVG asset: use intrinsic SVG aspect ratio, as today.FailedToLoad: usemax_widthandbase_line_height * FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER.LoadingorEvicted: keep the existing default pending height. Keep the helper focused on layout sizing; do not add render-element state to the content model.
2. Add explicit failed-load rendering to Image
In crates/warpui_core/src/elements/image.rs, add an optional failed-state element to Image:
- Store
failed_to_load_element: Option<Box<dyn Element>>. - Add
pub fn on_load_failure(mut self, element: Box<dyn Element>) -> Self. - During
layoutandafter_layout, lay out both the before-load element and failed-load element when present. - During
paint, renderfailed_to_load_elementforAssetState::FailedToLoad(_); if none is provided, preserve current behavior by falling back tobefore_load_element. This keeps all existing image callers behavior-compatible while allowing Mermaid to provide a distinct error UI.
3. Add a loading-timeout path for Mermaid
AssetState::FailedToLoad only covers futures that resolve with an error. To satisfy PRODUCT.md Behavior 2 for stuck renders, add a Mermaid render timeout without making every image in Warp time out.
Preferred shape:
- Add an optional timeout API to
Image, for examplepub fn on_load_timeout(mut self, timeout: Duration, element: Box<dyn Element>) -> Self. - Track load start time by stable asset-source key inside the image element module. Do not reuse the existing animation
started_at, and do not store the timeout start only on a singleImageinstance because rich-text layout can rebuild the element before the timeout fires. - When
paintseesAssetState::Loading, initializeload_started_atif needed, schedule a repaint for the timeout deadline, and paint the before-load element until the timeout expires. - Once the timeout expires, paint the timeout element instead of the before-load element while the asset remains
Loading. - If the asset later becomes
Loaded, paint the image normally and clear backup elements as the existing loaded path does. - If the asset later becomes
FailedToLoad, prefer the failed-load element. If a genericImagetimeout API feels too broad during implementation, keep the timeout state inRenderableMermaidDiagraminstead. The important invariant is user-visible: Mermaid cannot show the loading text indefinitely. Avoid wrappingrender_mermaid_to_svgwithwarpui::r#async::FutureExt::with_timeoutas the only timeout mechanism; that helper only times out while the wrapped future yields, and Mermaid rendering is currently a synchronous call inside the async fetch body.
4. Wire the Mermaid failure and timeout callouts
In crates/editor/src/render/element/mermaid.rs:
- Build the existing loading placeholder as today, using
model.styles().placeholder_color. - Build a failure callout text element with the exact string
Failed to render Mermaid diagram, the code text font family, font size, line-height ratio, and theme-derived placeholder or secondary text color. - Create the
Imagewith:.contain().before_load(loading_placeholder).on_load_failure(failure_callout)- the Mermaid timeout API from §3, using a 10 second timeout and the same visible failure callout text.
Keep the existing rounded background, border, selection overlay, and cursor painting in
RenderableMermaidDiagram::paint.
5. Preserve source and selection semantics
Do not change BlockItem::MermaidDiagram content length, markdown serialization, copy behavior, hidden block handling, or editor selection semantics. The failure callout is a render-state presentation for the diagram block, not new document content.
6. Logging
Do not add new user-visible toasts. Existing AssetCache warning logs for fetch/conversion failures are sufficient for actual failed loads. If timeout logging is added, use at most a debug-level log so a pathological document with many invalid diagrams does not create noisy client logs.
Testing and validation
Use cargo nextest run --no-fail-fast --workspace <test identifier> for focused Rust tests in this repo.
Unit tests
- Add
crates/editor/src/content/mermaid_diagram_tests.rsand import it frommermaid_diagram.rswith a#[cfg(test)]path module. Test that a failed Mermaid asset uses the compact failed height while loading still uses the default pending height. - Extend or add
crates/warpui_core/src/elements/image_tests.rscoverage for:AssetState::FailedToLoadrenders the failed-load element when one is provided.AssetState::FailedToLoadfalls back tobefore_load_elementwhen no failed-load element is provided.- A loading image switches from before-load element to timeout element after the configured timeout.
- Timeout start time survives rebuilding an
Imageelement for the same asset source.
- Keep the existing
test_layout_mermaid_block_uses_loaded_svg_aspect_ratiocoverage incrates/editor/src/content/edit_tests.rspassing for successful diagrams.
Integration/manual validation
- In a markdown viewer or editable plan, render a valid Mermaid diagram and confirm it still becomes an SVG.
- Render invalid Mermaid syntax and confirm the block shows
Failed to render Mermaid diagraminstead of staying on the loading placeholder. - Simulate a stuck Mermaid render by using a test-only async asset source that never resolves, or a temporary local patch in
mermaid_asset_source, and confirm the loading text is replaced after 10 seconds. - Confirm editing the Mermaid source creates a new render attempt and does not permanently preserve the previous failure state.
- Confirm two Mermaid diagrams in the same document can independently show success and failure states.
- Confirm selecting/copying/exporting the document still preserves the authored fenced Mermaid markdown, not the failure callout text.
Commands
- Focused editor tests:
cargo nextest run --no-fail-fast --workspace mermaid - Focused image tests:
cargo nextest run --no-fail-fast --workspace image - Compile check after implementation:
cargo check
Risks and mitigations
Timeout does not cancel underlying work
A UI-level timeout prevents an indefinite placeholder, but it does not necessarily cancel a synchronous Mermaid render already running on the background executor. Keep the timeout scoped to presentation for this issue. If stuck renders consume executor capacity in practice, follow up with renderer-level cancellation or process isolation.
Shared Image behavior regresses other callers
The new failure and timeout behavior must be opt-in. Existing callers without on_load_failure or timeout configuration should behave exactly as they do today.
Layout height for timed-out loading assets
Known FailedToLoad assets can use compact layout on the next layout pass. A UI timeout while the asset remains Loading may still occupy the pending placeholder height unless implementation adds model-level timeout state and requests relayout. The first iteration prioritizes replacing the indefinite loading text; compact timeout layout can be a follow-up if needed.
Parallelization
- One agent can implement the shared
Imagefailed-load and timeout behavior plus unit tests. - Another agent can wire Mermaid-specific layout and render behavior plus Mermaid-focused tests.
- Manual UI validation should wait until both code paths are integrated.
Follow-ups
- Add a retry affordance for failed diagrams if users need to retry the same source without editing or reopening the document.
- Add a “copy Mermaid source” affordance if the failure state needs an explicit raw-source escape hatch.
- Investigate renderer-level cancellation or isolation if stuck Mermaid renders are confirmed to consume background executor capacity.