first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+35
View File
@@ -0,0 +1,35 @@
# APP-4267: Mermaid render failures show an explicit callout
## Summary
Mermaid diagrams in rendered markdown surfaces should never leave users staring at an indefinite “Rendering Mermaid diagram…” placeholder. When rendering fails or remains pending too long, Warp replaces the loading state with a clear failure callout while preserving the underlying markdown source.
## Problem
The markdown viewer can show a large Mermaid placeholder that remains stuck on “Rendering Mermaid diagram…” indefinitely. Users cannot tell whether the diagram is still rendering, failed due to invalid Mermaid syntax, failed during SVG/image conversion, or hit an internal renderer hang.
## Goals
- Show an explicit, readable failure state for Mermaid diagrams that fail or time out.
- Preserve successful Mermaid rendering behavior.
- Preserve the authored Mermaid markdown for editing, copying, storage, and export.
- Keep the first iteration lightweight by reusing existing markdown/code-block styling.
## Non-goals
- Adding a dedicated Mermaid source editor.
- Adding retry, “copy raw,” or “open raw source” controls in this iteration.
- Changing Mermaid syntax support, diagram theme, layout algorithm, or rendered SVG fidelity.
- Changing ordinary image loading behavior outside the explicit implementation surface needed for Mermaid failures.
## Figma
Figma: none provided (use existing markdown/code block styling).
## Behavior
1. When a rendered markdown surface contains a fenced Mermaid code block and Mermaid rendering is enabled, Warp initially shows the existing pending state: “Rendering Mermaid diagram…”.
2. The pending state is temporary. A Mermaid render attempt enters the failure state if:
- Mermaid-to-SVG rendering returns an error.
- The SVG/image conversion pipeline returns an error.
- The diagram remains unresolved for longer than the Mermaid render timeout, initially 10 seconds.
3. When a Mermaid render attempt enters the failure state, Warp replaces “Rendering Mermaid diagram…” with a visible callout that says “Failed to render Mermaid diagram”.
4. The failure callout appears inside the same diagram/code-block container where the rendered diagram or loading placeholder would have appeared. It uses theme-derived text, border, and background colors consistent with existing rendered code/markdown blocks.
5. The failure callout is compact enough that a failed diagram does not reserve the large default placeholder height when Warp can determine the render has permanently failed. If the failure is only a UI timeout while the underlying render is still unresolved, the callout must still be visible in the existing placeholder area.
6. A successfully rendered Mermaid diagram continues to display as the rendered SVG, not as a callout.
7. If a render attempt times out but later resolves successfully, Warp replaces the timeout callout with the successfully rendered diagram. Warp must not switch back to the loading placeholder for the same unresolved render attempt.
8. If the underlying Mermaid source changes, Warp treats the new source as a new render attempt: the previous success, failure, or timeout state does not permanently carry over to the changed diagram.
9. Multiple Mermaid diagrams in the same markdown document are independent. A failure in one diagram does not change the rendering, loading, or failure state of any other diagram.
10. The authored Mermaid markdown remains the source of truth. Copying, storing, exporting, sharing, undo/redo, and editing behavior continue to operate on the original fenced Mermaid markdown, not on the failure callout text.
11. When Mermaid rendering is disabled or the surface is intentionally showing raw code blocks, existing raw Mermaid code block behavior is unchanged.
12. The callout has no interactive controls in this iteration, so it does not add keyboard focus stops. It must still be readable by text-based accessibility surfaces as ordinary visible text.
13. Selection behavior around the failed diagram remains consistent with rendered Mermaid diagrams today: users should not accidentally select or copy the failure message instead of the authored Mermaid markdown when operating on the block as markdown content.
14. The failure state should not log a user-visible toast or modal. The error is localized to the diagram block so the rest of the document remains readable.
+94
View File
@@ -0,0 +1,94 @@
# 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 as `CodeBlockType::Mermaid` when `FeatureFlag::MarkdownMermaid` is enabled.
- `crates/editor/src/content/edit.rs (671-726)` converts styled Mermaid code blocks into `LayoutTask::MermaidDiagram` and calls `mermaid_diagram_layout`.
- `crates/editor/src/content/edit.rs (1031-1067)` converts that layout task into `BlockItem::MermaidDiagram` while preserving the source block content length.
- `crates/editor/src/content/mermaid_diagram.rs:20` defines the current default pending height as 10 line-heights.
- `crates/editor/src/content/mermaid_diagram.rs (28-44)` creates an `AssetSource::Async` whose fetch future calls `mermaid_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 wrapping `Image::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 paints `before_load_element` for `Loading`, `Evicted`, and `FailedToLoad`; the shared image element has no separate failed-state element.
- `crates/warpui_core/src/assets/asset_cache.rs (284-330)` stores new async assets as `Loading` and spawns the fetch future once per async asset ID.
- `crates/warpui_core/src/assets/asset_cache.rs (415-486)` promotes async assets to `Loaded` or `FailedToLoad` only when the background future resolves.
There is already an attached implementation branch, `origin/oz-agent/APP-4267/mermaid-failure-callout`, that adds an `Image::on_load_failure` element and a compact Mermaid failure notice for `AssetState::FailedToLoad`. That direction fits the current architecture, but it should be tightened to cover the product-level timeout behavior: a truly stuck render can remain `AssetState::Loading` forever 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_MULTIPLIER` remains the pending/success fallback height.
- `FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER` should be 2 line-heights for known permanent failures.
Update `mermaid_diagram_layout` so it distinguishes:
- `Loaded` SVG asset: use intrinsic SVG aspect ratio, as today.
- `FailedToLoad`: use `max_width` and `base_line_height * FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER`.
- `Loading` or `Evicted`: 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 `layout` and `after_layout`, lay out both the before-load element and failed-load element when present.
- During `paint`, render `failed_to_load_element` for `AssetState::FailedToLoad(_)`; if none is provided, preserve current behavior by falling back to `before_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 example `pub 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 single `Image` instance because rich-text layout can rebuild the element before the timeout fires.
- When `paint` sees `AssetState::Loading`, initialize `load_started_at` if 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 generic `Image` timeout API feels too broad during implementation, keep the timeout state in `RenderableMermaidDiagram` instead. The important invariant is user-visible: Mermaid cannot show the loading text indefinitely. Avoid wrapping `render_mermaid_to_svg` with `warpui::r#async::FutureExt::with_timeout` as 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 `Image` with:
- `.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.rs` and import it from `mermaid_diagram.rs` with 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.rs` coverage for:
- `AssetState::FailedToLoad` renders the failed-load element when one is provided.
- `AssetState::FailedToLoad` falls back to `before_load_element` when 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 `Image` element for the same asset source.
- Keep the existing `test_layout_mermaid_block_uses_loaded_svg_aspect_ratio` coverage in `crates/editor/src/content/edit_tests.rs` passing 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 diagram` instead 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 `Image` failed-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.