Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
# Inline Markdown Images in AI Block List — Product Spec
|
||||
Linear: none provided
|
||||
Figma: none provided
|
||||
|
||||
## Summary
|
||||
Render supported Markdown images inline inside AI block list responses instead of always showing raw Markdown image syntax. The first release should support local file-backed images referenced by relative or absolute filesystem paths, plus Mermaid diagrams rendered on the fly in the same response flow.
|
||||
|
||||
This feature should make AI responses easier to read when they include screenshots, diagrams, or visual outputs, while preserving reliable copy behavior without requiring a broader block-list selection refactor in the first release. The presentation should now follow three distinct treatments from the approved mock:
|
||||
|
||||
- horizontal inline image rows for multiple image references that appear in the same Markdown line
|
||||
- a stacked “Images N” card for consecutive block-level image references
|
||||
- a framed Mermaid block with a titled header and white diagram canvas
|
||||
|
||||
Successfully rendered file-backed images and Mermaid diagrams should both reuse Warp’s existing fullscreen lightbox treatment for click-to-expand viewing.
|
||||
|
||||
When an image cannot be resolved or the format is unsupported, the block list should continue to show the original Markdown text rather than rendering a broken or partial image UI.
|
||||
|
||||
## Problem
|
||||
AI responses increasingly include Markdown image references and Mermaid diagrams. In the AI block list today, that content is more useful as source text than as rendered output, but it is much harder to read and scan when the user actually wants to view the visual result inline.
|
||||
|
||||
Warp already supports Markdown image blocks elsewhere in the app and already has Mermaid-to-SVG rendering for notebook Markdown. The AI block list currently lacks the equivalent presentation and interaction model. That creates three problems:
|
||||
|
||||
- visual content in AI responses is harder to understand because users see raw Markdown instead of the image or diagram
|
||||
- rendered output is inconsistent with other Markdown-capable surfaces in Warp
|
||||
- copy and selection behavior for mixed text-and-image content is undefined unless we specify it explicitly
|
||||
|
||||
## Goals
|
||||
- Render supported local Markdown images inline inside AI block list responses.
|
||||
- Support both absolute file paths and relative file paths.
|
||||
- Resolve relative file paths relative to the current working directory of the session associated with the AI block.
|
||||
- Support Mermaid diagrams in the same overall rollout, rendering them inline from their Markdown source.
|
||||
- Ensure the feature works across Warp’s supported platforms, including WASM.
|
||||
- Use the same supported image format set that Warp’s existing Markdown/image rendering stack already supports today: JPEG/JPG, PNG, GIF, WebP, and SVG.
|
||||
- Show the referenced file link below each successfully rendered file-backed image in the AI block list.
|
||||
- Preserve stable copy behavior for rendered images and Mermaid diagrams without introducing a new cross-renderer selection model in this first release.
|
||||
- Allow right-click copy on a rendered image or Mermaid diagram, copying the underlying Markdown source rather than image bytes.
|
||||
- Fall back cleanly to the raw Markdown source when the image cannot be found, cannot be resolved, or is not a supported format.
|
||||
- Gate block list image rendering behind a dedicated block-list-specific feature flag.
|
||||
- Respect the existing Mermaid rendering flag in addition to the new block list image flag, so Mermaid diagrams render in the AI block list only when both flags are enabled.
|
||||
- Reuse Warp’s existing fullscreen lightbox treatment for both rendered file-backed images and rendered Mermaid diagrams.
|
||||
|
||||
## Non-Goals
|
||||
- Supporting remote image URLs in this first release.
|
||||
- Supporting Markdown data URLs or other inline-encoded image sources in this first release.
|
||||
- Adding image editing, resizing controls, zoom controls, or image-specific toolbars.
|
||||
- Copying image bytes to the clipboard.
|
||||
- Adding drag-and-drop, save-image, or open-image actions.
|
||||
- Supporting additional image formats beyond the set Warp already renders today.
|
||||
- Shipping file-backed images first and Mermaid later; both are part of the same implementation sequence.
|
||||
|
||||
## User Experience
|
||||
|
||||
### Scope
|
||||
This feature applies to AI block list responses that render Markdown content.
|
||||
|
||||
It should work for:
|
||||
- newly streamed AI responses
|
||||
- previously rendered responses reopened from history
|
||||
- restored conversation views that display the same AI block content
|
||||
- all supported client platforms, including WASM
|
||||
|
||||
### Supported sources
|
||||
The first release should support two kinds of inline visual Markdown content:
|
||||
|
||||
1. Standard Markdown image references that point to local files.
|
||||
2. Mermaid diagrams authored in Markdown that Warp renders into an image representation at runtime.
|
||||
|
||||
For file-backed images, the initial supported path types are:
|
||||
- absolute filesystem paths
|
||||
- relative filesystem paths
|
||||
|
||||
Relative paths resolve against the current working directory of the session associated with the AI block, not against a notebook file path or any repository root heuristic.
|
||||
That working directory should be the one recorded with the AI block when the response was rendered. Relative file paths should be re-resolved against that recorded working directory when the block is later restored or re-rendered.
|
||||
|
||||
### Supported formats
|
||||
For file-backed images, the AI block list should support the same image formats Warp’s existing shared image renderer already supports today:
|
||||
- JPEG / JPG
|
||||
- PNG
|
||||
- GIF
|
||||
- WebP
|
||||
- SVG
|
||||
|
||||
If the referenced file exists but is not one of those supported formats, Warp should not attempt a degraded or best-effort render. It should show the original Markdown text exactly as it does today.
|
||||
|
||||
### File-backed image rendering
|
||||
When AI output contains a valid Markdown image whose source resolves to a supported local file, the image should render inline in the normal block list flow where the Markdown image appeared.
|
||||
|
||||
Rendered images should behave like block content in the response:
|
||||
- they appear between the surrounding text in source order
|
||||
- multiple images in one response all render in place
|
||||
- images can appear alongside normal paragraphs, lists, code blocks, and tables without changing the rendering behavior of those surrounding elements
|
||||
|
||||
There are two distinct file-backed image layouts:
|
||||
|
||||
1. Inline image runs
|
||||
When multiple parser-recognized Markdown image references appear on the same line, separated only by whitespace, Warp should render them as a horizontal image row:
|
||||
- each image appears above its own visible source-path label
|
||||
- each item preserves aspect ratio
|
||||
- the row stays left-aligned in the response flow
|
||||
- the path label under each image keeps the existing hover highlight and click-to-open behavior for file links
|
||||
- if that path points to a binary file such as PNG, JPG, GIF, or WebP, activating the path should use the platform-default opener instead of Warp's editor or the user's configured `$EDITOR`
|
||||
|
||||
2. Block-level image groups
|
||||
When Markdown images appear as standalone block items on separate lines, consecutive successfully rendered images should be grouped into a bordered card:
|
||||
- the card header should read `Images N`, where `N` is the number of rendered images in that grouped block
|
||||
- each row in the card should show a square thumbnail on the left and the source path on the right
|
||||
- the source path keeps the same hover highlight and click-to-open behavior as other file links in AI output
|
||||
- binary file paths in that row should open with the platform-default application rather than any editor target
|
||||
- unsupported or unresolved images should fall back to raw Markdown rather than appearing inside the grouped card
|
||||
|
||||
For file-backed images that render successfully, Warp should also show the referenced file link/path in the layout appropriate for that treatment:
|
||||
- the displayed link text should use the file path/source text from the Markdown image reference
|
||||
- it should participate in the normal link-highlighting and click behavior users already expect for file links in AI block content
|
||||
- binary file links should open with the operating system's default file opener; on macOS this means using `open` rather than routing through Warp's editor or `$EDITOR`
|
||||
- it should remain visible in restored conversations the same way it does for newly rendered output
|
||||
|
||||
If a Markdown image reference cannot be resolved to an existing local file, the block list should show the original Markdown image text instead of a rendered image.
|
||||
|
||||
Rendered file-backed images should also support click-to-expand behavior:
|
||||
- clicking a rendered image opens it in Warp’s existing fullscreen lightbox treatment
|
||||
- the lightbox should reuse the same fullscreen overlay treatment already used elsewhere in Warp rather than introducing a new one-off viewer
|
||||
- when the current AI message contains multiple successfully rendered visual items, the lightbox should allow previous/next navigation across those visuals in source order
|
||||
- for file-backed images, the fullscreen viewer should keep showing the image source/path as descriptive text
|
||||
|
||||
### Mermaid rendering
|
||||
Mermaid diagrams should be supported as part of the same feature and should render inline in the AI block list as visual content rather than raw Mermaid source.
|
||||
|
||||
Mermaid rendering in the AI block list requires two independent conditions to be true:
|
||||
- the existing global Mermaid rendering flag is enabled
|
||||
- the new AI block list image-rendering flag is enabled
|
||||
|
||||
If either flag is disabled, Mermaid content in the AI block list should continue to fall back to raw Markdown/source rendering.
|
||||
|
||||
From the user’s perspective:
|
||||
- a Mermaid diagram in AI Markdown should appear as a rendered diagram in a titled, bordered block within the response flow
|
||||
- the block should use a dark header treatment with a Mermaid-specific title and a white inner canvas for the rendered diagram
|
||||
- rendering may happen asynchronously
|
||||
- the UI must not block while the diagram is being generated
|
||||
- clicking a rendered Mermaid diagram should open that diagram in the same fullscreen lightbox treatment used for rendered block-list images
|
||||
- when there are multiple rendered visuals in the same AI message, the lightbox should support previous/next navigation between them in source order
|
||||
|
||||
If Mermaid rendering fails for any reason, the block list should fall back to showing the original Mermaid Markdown source rather than a broken image state.
|
||||
|
||||
### Layout and sizing
|
||||
Rendered file-backed images and rendered Mermaid diagrams should preserve aspect ratio and remain fully visible without cropping or distortion, but the exact sizing now depends on the treatment:
|
||||
|
||||
- inline image rows use medium-height thumbnails with per-image widths derived from aspect ratio and capped so the row remains visually balanced
|
||||
- block-level image groups use square thumbnail slots for each image row
|
||||
- Mermaid uses a framed block with a padded white canvas that contains the rendered diagram
|
||||
|
||||
The inline/block treatments remain the default in-flow presentation. Clicking a rendered image or Mermaid diagram should open the shared fullscreen lightbox overlay as a secondary viewing mode, without changing the inline layout itself.
|
||||
|
||||
### Fallback behavior
|
||||
Fallback behavior is important and should be predictable.
|
||||
|
||||
For file-backed images, if any of the following are true:
|
||||
- the path cannot be resolved
|
||||
- the file does not exist
|
||||
- the format is unsupported
|
||||
- the asset cannot be loaded successfully
|
||||
|
||||
then Warp should show the original Markdown image syntax inline, matching current behavior, instead of rendering an error-specific image container.
|
||||
|
||||
For Mermaid diagrams, if rendering fails, Warp should show the original Mermaid Markdown source instead of a rendered diagram.
|
||||
|
||||
### Selection and copy behavior
|
||||
This first release should keep the existing block-list selection behavior for text, code blocks, tables, and other already-supported content. It should not introduce a broader refactor to make rendered images and Mermaid diagrams participate in mixed drag-selection across multiple renderer types.
|
||||
|
||||
Instead, rendered visual content should support copy through explicit copy surfaces:
|
||||
- block-level copy actions should include the underlying Markdown source for rendered images and Mermaid diagrams
|
||||
- right-click copy on a rendered image or Mermaid diagram should copy the underlying Markdown source for that visual element
|
||||
|
||||
For this first release, it is acceptable for copy to serialize visual content back into raw Markdown rather than attempting rich HTML or image clipboard output.
|
||||
|
||||
### Right-click copy behavior
|
||||
Right-clicking a rendered image or Mermaid diagram should provide a copy action.
|
||||
|
||||
That copy action should place the underlying Markdown source on the clipboard:
|
||||
- for file-backed images, the original Markdown image reference
|
||||
- for Mermaid diagrams, the original Mermaid Markdown source
|
||||
|
||||
This copy action should not place image bytes on the clipboard in this release.
|
||||
|
||||
### Mixed content behavior
|
||||
The AI block list must support responses that contain:
|
||||
- only text
|
||||
- only one image
|
||||
- only one Mermaid diagram
|
||||
- multiple file-backed images
|
||||
- multiple Mermaid diagrams
|
||||
- mixed text, images, and Mermaid diagrams in one response
|
||||
|
||||
Each supported visual element should render independently in the correct source position. Unsupported or unresolved items should remain raw Markdown text without preventing supported neighbors from rendering.
|
||||
|
||||
### Streaming and restored behavior
|
||||
The feature should behave consistently for both streamed and restored responses.
|
||||
|
||||
For streamed responses:
|
||||
- file-backed images should render once enough Markdown is present to identify the image reference
|
||||
- Mermaid diagrams should render once enough source is present to identify and generate the diagram
|
||||
- rendering updates should not corrupt surrounding content or unexpectedly clear an active selection
|
||||
|
||||
For restored responses:
|
||||
- supported images and Mermaid diagrams should render the same way they do in the initial response view
|
||||
- the same fallback rules should apply if the asset is unavailable at restore time
|
||||
|
||||
## Success Criteria
|
||||
- A Markdown image in an AI block list response renders inline when it references a supported local file.
|
||||
- Relative file paths resolve against the session’s working directory rather than a notebook/document location.
|
||||
- Relative file paths are re-resolved against the working directory recorded with the AI block at render time when the block is restored or re-rendered.
|
||||
- Absolute file paths render correctly when they point to a supported local image.
|
||||
- The initial supported file format set is JPEG/JPG, PNG, GIF, WebP, and SVG.
|
||||
- Successfully rendered file-backed images show their source path in the mock-aligned position for their layout, with normal link highlighting behavior.
|
||||
- Activating the source path for a rendered binary asset opens it with the operating system's default app instead of Warp's editor or the user's configured `$EDITOR`.
|
||||
- Mermaid diagrams in AI Markdown render inline in the AI block list only when both the existing Mermaid flag and the new block list image-rendering flag are enabled.
|
||||
- Multiple image references on the same line render as a horizontal inline image row with one path label per image.
|
||||
- Consecutive standalone image references render as a grouped `Images N` card with thumbnail-plus-path rows.
|
||||
- Rendered images and Mermaid diagrams preserve aspect ratio and use the new mock-aligned sizing treatments.
|
||||
- If a file cannot be found or the file format is unsupported, Warp shows the original Markdown text rather than a broken rendered state.
|
||||
- If Mermaid rendering fails, Warp shows the original Mermaid Markdown source.
|
||||
- Existing block-list text/code/table selection behavior continues to work as it does today after image rendering is added.
|
||||
- Block-level copy actions write the underlying raw Markdown for rendered images and Mermaid diagrams into the clipboard.
|
||||
- Right-click copy on a rendered image or Mermaid diagram copies its underlying Markdown source.
|
||||
- Clicking a rendered file-backed image or Mermaid diagram opens the shared fullscreen lightbox treatment.
|
||||
- The fullscreen lightbox supports previous/next navigation across the renderable visual items in the same AI message, in source order.
|
||||
- Multiple supported images in one AI response render independently and do not interfere with surrounding text.
|
||||
- Restored AI blocks and newly streamed AI blocks follow the same rendering and fallback rules.
|
||||
- The implementation behaves correctly on all supported platforms, including WASM.
|
||||
- The feature has automated verification coverage in addition to manual validation.
|
||||
|
||||
## Validation
|
||||
- Unit tests for markdown/image section parsing and flag-gating behavior, including Mermaid requiring both flags.
|
||||
- Unit tests for relative-path resolution against block metadata that stores the original working directory.
|
||||
- Unit tests for fallback behavior when a file is missing, unsupported, or Mermaid rendering is disabled or fails.
|
||||
- Automated tests covering block-level copy serialization for mixed content that includes text, images, Mermaid diagrams, code blocks, and other special Markdown-rendered content.
|
||||
- Integration tests that exercise AI block list rendering end to end, including restored-block behavior and mixed-content copy behavior.
|
||||
- Manual validation with an AI response containing a relative-path PNG image.
|
||||
- Manual validation with an AI response containing an absolute-path JPEG image.
|
||||
- Manual validation with one example each for SVG, GIF, and WebP image references.
|
||||
- Manual validation that an inline image run renders as a horizontal row with the path label under each image and the expected link highlighting.
|
||||
- Manual validation that consecutive standalone image lines render as an `Images N` card with thumbnail-plus-path rows.
|
||||
- Manual validation that clicking the source path for a rendered PNG or other binary image opens it with the platform-default app rather than Warp's editor.
|
||||
- Manual validation that a missing local file falls back to the original Markdown image syntax.
|
||||
- Manual validation that an unsupported format falls back to the original Markdown image syntax.
|
||||
- Manual validation that a Mermaid diagram renders in the new framed block treatment with a white inner canvas.
|
||||
- Manual validation that Mermaid in the AI block list does not render unless both required flags are enabled.
|
||||
- Manual validation that a Mermaid render failure falls back to raw Mermaid Markdown.
|
||||
- Manual validation of a response containing text, one image, more text, and a Mermaid diagram.
|
||||
- Manual validation of a response containing multiple images in sequence.
|
||||
- Manual validation that clicking a rendered inline image opens the shared fullscreen lightbox with the expected image selected.
|
||||
- Manual validation that clicking a rendered Mermaid diagram opens the same fullscreen lightbox treatment.
|
||||
- Manual validation that previous/next navigation in the lightbox walks the rendered visuals from the same AI message in source order.
|
||||
- Manual validation that Escape and scrim click dismiss the lightbox and return the user to the block list.
|
||||
- Manual validation that existing text/code/table selection behavior is unchanged after image rendering is added.
|
||||
- Manual validation that block-level copy produces the expected text plus raw Markdown image or Mermaid source in document order.
|
||||
- Manual validation that right-click copy on a rendered image or Mermaid diagram copies the underlying Markdown source.
|
||||
- Manual validation that the same response renders correctly after the conversation is restored from history.
|
||||
- Regression validation that disabling the feature flag restores the current raw-Markdown behavior.
|
||||
@@ -0,0 +1,388 @@
|
||||
# Inline Markdown Images in AI Block List — Tech Spec
|
||||
Product spec: `specs/zachlloyd/inline-markdown-images-in-blocklist/PRODUCT.md`
|
||||
|
||||
## Problem
|
||||
The AI block list does not currently support the product behavior described in `PRODUCT.md`: rendering supported Markdown images and Mermaid diagrams inline inside AI responses with the approved inline-row, grouped-image, and Mermaid-card treatments.
|
||||
|
||||
Warp already has the underlying primitives needed to build this behavior:
|
||||
|
||||
- local image asset loading
|
||||
- responsive image sizing
|
||||
- Mermaid-to-SVG rendering
|
||||
- block-list rendering of other markdown section types like code and tables
|
||||
|
||||
Today, block-list markdown is split into plain text, code, and table sections. Plain text is rendered with `FormattedTextElement`, which intentionally treats `FormattedTextLine::Image` as a line break rather than a visual block. As a result, Markdown image syntax degrades to raw text in the AI block list, and Mermaid fences continue to render through the existing code-path behavior rather than the product's visual treatment.
|
||||
|
||||
The implementation should add support for the required file-backed image and Mermaid behaviors while keeping the change narrowly scoped:
|
||||
|
||||
- no new cross-renderer selection model
|
||||
- no broad markdown-renderer refactor
|
||||
- no new dependency edge from the block list into a rendering stack it does not already use
|
||||
|
||||
This should be a surgical extension of the existing section-based block-list renderer, with explicit copy behavior for rendered visuals, a visible file-link label below successfully rendered file-backed images, raw-Markdown fallback when rendering is unavailable, and reuse of Warp’s existing fullscreen lightbox treatment when the user clicks a rendered visual.
|
||||
|
||||
## Relevant Code
|
||||
- `specs/zachlloyd/inline-markdown-images-in-blocklist/PRODUCT.md` — approved product behavior
|
||||
- `app/src/ai/agent/util.rs (24-186)` — `parse_markdown_into_text_and_code_sections`; current block-list markdown section splitter
|
||||
- `app/src/ai/agent/mod.rs (1281-1361)` — `AgentOutputText`, `AgentOutputTable`, and `AIAgentTextSection`
|
||||
- `app/src/ai/agent/mod.rs (405-505)` — `AIAgentOutput::all_text`; current message traversal used by rendering and link detection
|
||||
- `app/src/ai/agent/mod.rs (1547-1628)` — `Display for AIAgentOutputMessage`; block/conversation copy formatting for markdown sections
|
||||
- `app/src/ai/agent/mod.rs (2722-2779)` — `AIAgentExchange::format_output_for_copy` and `format_for_copy`
|
||||
- `app/src/ai/blocklist/block/view_impl/output.rs (246-345)` — message-level block-list output rendering loop
|
||||
- `app/src/ai/blocklist/block/view_impl/common.rs (869-952)` — `render_text_sections`; current section-to-UI mapping
|
||||
- `app/src/ai/blocklist/block/view_impl/common.rs (986-1084)` — `render_rich_text_output_text_section`; current plain-text markdown renderer
|
||||
- `app/src/ai/blocklist/block/view_impl/common.rs (1461-1646)` — `render_code_output_section`; current code-block renderer
|
||||
- `app/src/workspace/lightbox_view.rs` — shared fullscreen lightbox view with Escape and left/right navigation
|
||||
- `crates/ui_components/src/lightbox.rs` — reusable lightbox component and navigation controls
|
||||
- `app/src/workspace/action.rs` — workspace actions for opening and updating the shared lightbox
|
||||
- `app/src/workspace/view.rs` — workspace-level lightbox lifecycle and focus handling
|
||||
- `app/src/ai/blocklist/block.rs (2230-2246)` — per-section handle allocation for code and tables
|
||||
- `app/src/ai/blocklist/block.rs (4313-4476)` — `selected_text`, `clear_all_selections`, and `clear_other_selections`; current selection model
|
||||
- `app/src/ai/blocklist/block.rs (4946-5002)` — block-level copy scope
|
||||
- `app/src/ai/blocklist/controller.rs (62-86)` — `SessionContext`; current working-directory capture
|
||||
- `app/src/ai/blocklist/persistence.rs (24-31)` — persisted exchange `working_directory`
|
||||
- `app/src/ai/blocklist/history_model.rs (2102-2154)` — restored query history carries working directory
|
||||
- `app/src/ai/blocklist/block.rs (906-919)` — `AIBlock::new` accepts `current_working_directory` and `shell_launch_data`
|
||||
- `crates/markdown_parser/src/markdown_parser.rs (286-326)` — `parse_image`; current image recognition behavior
|
||||
- `crates/markdown_parser/src/markdown_parser.rs (466-475)` — `parse_inline_markdown`; current inline parser surface
|
||||
- `crates/markdown_parser/src/markdown_parser_test.rs (2171-2320)` — current parser coverage for images
|
||||
- `crates/warpui_core/src/elements/formatted_text_element.rs (1580-1591, 1661-1671)` — images are currently treated as line breaks in the block-list rich-text path
|
||||
- `crates/editor/src/content/text.rs (280-362)` — image markdown round-trip behavior in the editor stack
|
||||
- `crates/editor/src/content/text.rs (526-579)` — `CodeBlockType::Mermaid` gating
|
||||
- `crates/editor/src/content/edit.rs (56-91)` — native/WASM asset-source resolution rules
|
||||
- `crates/editor/src/content/mermaid_diagram.rs (20-67)` — in-memory Mermaid SVG asset generation and sizing
|
||||
- `crates/warpui_core/src/image_cache.rs` — supported shared image types (JPEG, PNG, GIF, WebP, SVG)
|
||||
- `crates/warp_features/src/lib.rs (525-540, 817-858)` — markdown-related feature flags
|
||||
- `app/src/lib.rs (2462-2471)` — app-side feature-flag wiring for markdown tables and Mermaid
|
||||
- `app/Cargo.toml (667-746)` — compile-time feature declarations
|
||||
- `app/src/ai/agent/util_tests.rs` — block-list markdown parser/unit test pattern
|
||||
- `crates/editor/src/content/edit_tests.rs (293-404)` — Mermaid layout test pattern
|
||||
- `crates/editor/src/content/text_tests.rs (101-127)` — Mermaid block-type gating tests
|
||||
- `crates/integration/tests/INTEGRATION_TESTING.md` — integration test registration flow
|
||||
|
||||
## Current State
|
||||
|
||||
### Markdown parsing in the AI block list
|
||||
The block list currently parses AI markdown with a line-oriented splitter in `app/src/ai/agent/util.rs (24-186)`. It recognizes:
|
||||
|
||||
- plain markdown text
|
||||
- fenced code blocks, including linked-code metadata
|
||||
- GFM tables
|
||||
|
||||
It does not emit explicit image or Mermaid section types today. Standard Markdown image syntax therefore remains embedded inside `AIAgentTextSection::PlainText`, and Mermaid code fences remain ordinary `Code` sections.
|
||||
|
||||
### Current Markdown semantics vs current parser behavior
|
||||
Standard Markdown/CommonMark uses `` as image syntax. There is not a separate standard syntax that means “this should be shown as a literal file path instead of rendered as an image.” If the author wants literal text, they escape the Markdown or place it in code spans/blocks.
|
||||
|
||||
Our current `markdown_parser` does not fully implement that inline image semantic yet. `crates/markdown_parser/src/markdown_parser.rs (286-326)` only recognizes images when they occupy a standalone line; inline paragraph images like `text  more text` are currently treated as plain text by this parser. For this redesign, the block list still should not invent a generic “render some arbitrary inline image-looking text” rule. The only extension should be a narrow line parser for runs of parser-compatible image references separated only by whitespace so we can distinguish:
|
||||
|
||||
- consecutive standalone image lines, which stay block-level
|
||||
- multiple image references on the same line, which become an inline image row
|
||||
|
||||
### Why images disappear today
|
||||
Plain-text markdown sections are rendered with `render_rich_text_output_text_section` in `app/src/ai/blocklist/block/view_impl/common.rs (986-1084)`, which delegates to `FormattedTextElement`. In `crates/warpui_core/src/elements/formatted_text_element.rs (1580-1591, 1661-1671)`, `FormattedTextLine::Image(_)`, `FormattedTextLine::Embedded(_)`, and `FormattedTextLine::HorizontalRule` are all treated as line-break-like layout items rather than renderable content. That is the immediate reason that block-list Markdown images never show up.
|
||||
|
||||
### Asset and Mermaid support already exists elsewhere
|
||||
Warp already has the low-level capabilities this feature needs:
|
||||
|
||||
- shared image format support in `crates/warpui_core/src/image_cache.rs`
|
||||
- asset-source resolution, including WASM-safe behavior, in `crates/editor/src/content/edit.rs (56-91)`
|
||||
- Mermaid SVG generation and sizing in `crates/editor/src/content/mermaid_diagram.rs (20-67)`
|
||||
- Mermaid code-block identification in `crates/editor/src/content/text.rs (526-579)`
|
||||
|
||||
The app crate already depends on `warp_editor`, and the block list already embeds editor-backed code blocks. Reusing editor Mermaid/image helpers therefore does not introduce a new crate dependency edge.
|
||||
|
||||
Warp also already has a reusable fullscreen lightbox path at the workspace layer. `WorkspaceAction::OpenLightbox` / `UpdateLightboxImage` drive `LightboxView`, which already supports Escape dismissal, left/right keyboard navigation, and previous/next buttons. The block-list visual renderer should reuse that path rather than inventing a new fullscreen viewer.
|
||||
|
||||
### Working-directory metadata already exists
|
||||
The product requirement for resolving relative paths against the working directory captured when the AI block rendered is already compatible with existing data flow:
|
||||
|
||||
- session context captures `current_working_directory` in `app/src/ai/blocklist/controller.rs (62-86)`
|
||||
- persisted exchanges store `working_directory` in `app/src/ai/blocklist/persistence.rs (24-31)`
|
||||
- restored history loads it back in `app/src/ai/blocklist/history_model.rs (2102-2154)`
|
||||
- `AIBlock::new` already receives both `current_working_directory` and `shell_launch_data`
|
||||
|
||||
We do not need a new persistence field for this feature; we need to thread existing metadata into the visual-section renderers.
|
||||
|
||||
### Selection is intentionally out of scope
|
||||
The current selection model in `app/src/ai/blocklist/block.rs (4313-4476)` is fragmented across:
|
||||
|
||||
- block-level text selection
|
||||
- child-view-native selection for code editors and other special renderers
|
||||
|
||||
That architecture does not support a continuous mixed-content selection model across text, code, tables, images, and Mermaid. This feature should not attempt to fix that. Instead, it should:
|
||||
|
||||
- preserve existing text/code/table selection behavior
|
||||
- add explicit copy affordances for rendered images and Mermaid diagrams
|
||||
- ensure block-level copy/export includes the correct raw Markdown for visual sections
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. Add a dedicated block-list image feature flag
|
||||
Add a new feature flag dedicated to AI block-list markdown visuals, e.g. `BlocklistMarkdownImages`.
|
||||
|
||||
Implementation points:
|
||||
- add `BlocklistMarkdownImages` to `crates/warp_features/src/lib.rs`
|
||||
- add `blocklist_markdown_images = []` to `app/Cargo.toml`
|
||||
- wire it in `app/src/lib.rs` alongside the existing markdown table/Mermaid flags
|
||||
- enable it in `DOGFOOD_FLAGS`, but leave it out of `PREVIEW_FLAGS` until the surface is stable
|
||||
|
||||
Mermaid rendering in the block list should require:
|
||||
- `FeatureFlag::BlocklistMarkdownImages`
|
||||
- `FeatureFlag::MarkdownMermaid`
|
||||
|
||||
File-backed images in the block list should require only `BlocklistMarkdownImages`.
|
||||
|
||||
I do not recommend reusing the dormant generic `MarkdownImages` flag for this launch. It is not currently wired through `app/src/lib.rs` or `app/Cargo.toml`, and this feature needs rollout control specific to the AI block list.
|
||||
|
||||
### 2. Extend the AI markdown section model with visual sections
|
||||
Extend `AIAgentTextSection` in `app/src/ai/agent/mod.rs` with explicit visual section variants:
|
||||
|
||||
- `Image { image: AgentOutputImage }`
|
||||
- `MermaidDiagram { diagram: AgentOutputMermaidDiagram }`
|
||||
|
||||
Proposed payloads:
|
||||
- `AgentOutputImage`
|
||||
- `alt_text: String`
|
||||
- `source: String`
|
||||
- `markdown_source: String`
|
||||
- `layout: AgentOutputImageLayout`
|
||||
- `AgentOutputMermaidDiagram`
|
||||
- `source: String`
|
||||
- `markdown_source: String`
|
||||
|
||||
The key design choice is to preserve Markdown source on the section payload rather than reconstructing it later from rendered state. That keeps:
|
||||
|
||||
- block-level copy/export exact
|
||||
- right-click copy trivial
|
||||
- fallback rendering simple
|
||||
|
||||
For images, `markdown_source` can initially be canonicalized to `` because the parser only returns `alt_text` and `source`, not byte-accurate source spans. That is consistent with the editor stack’s current image markdown round-trip behavior.
|
||||
|
||||
### 3. Teach the section splitter to extract images and Mermaid explicitly
|
||||
Update `parse_markdown_into_text_and_code_sections` in `app/src/ai/agent/util.rs` so that it can distinguish inline image runs from block-level image lines instead of treating every extracted image section identically.
|
||||
|
||||
The change should remain incremental:
|
||||
- keep current code-fence boundary detection so linked-code metadata parsing stays unchanged
|
||||
- keep current table extraction behavior
|
||||
- for plain-text regions, continue scanning line-by-line
|
||||
- keep using `markdown_parser` for standalone image-line extraction
|
||||
- add a narrow helper that recognizes lines containing two or more parser-compatible image references separated only by whitespace and emits them as `AIAgentTextSection::Image` sections tagged with `AgentOutputImageLayout::Inline`
|
||||
- emit standalone image lines as `AIAgentTextSection::Image` tagged with `AgentOutputImageLayout::Block`
|
||||
- when a fenced code block language resolves to Mermaid, emit `AIAgentTextSection::MermaidDiagram` instead of `Code`
|
||||
- leave everything else in `PlainText`
|
||||
|
||||
Two important constraints:
|
||||
- image extraction should still stay narrow and markdown-driven rather than becoming a generic loose regex over arbitrary prose
|
||||
- Mermaid section extraction should not be gated at parse time; the section payload should still preserve Mermaid source even when the runtime flags are off, so the renderer can fall back cleanly to raw Markdown without reparsing restored conversations
|
||||
|
||||
### 4. Keep the existing section renderer and add visual sections surgically
|
||||
Do not replace the current block-list markdown rendering architecture for this feature. Instead, extend `render_text_sections` in `app/src/ai/blocklist/block/view_impl/common.rs (869-952)` with grouped visual renderers:
|
||||
|
||||
- inline image-row rendering for consecutive `AgentOutputImageLayout::Inline` sections
|
||||
- grouped block-image row rendering for consecutive `AgentOutputImageLayout::Block` sections
|
||||
- a Mermaid card renderer that uses the new framed treatment
|
||||
|
||||
These should follow the same overall section-rendering pattern already used for:
|
||||
- code sections
|
||||
- table sections
|
||||
|
||||
This keeps the implementation targeted:
|
||||
- no unified markdown content view
|
||||
- no mixed-content selection refactor
|
||||
- no change to `AIBlock::selected_text` semantics beyond whatever is needed for explicit image/Mermaid copy affordances
|
||||
|
||||
As part of that renderer pass, build a source-ordered collection of the successfully renderable visual sections in the current AI message. That shared collection should include both successfully rendered file-backed images and successfully rendered Mermaid diagrams. Clicking any rendered image or Mermaid diagram should dispatch `WorkspaceAction::OpenLightbox` with that shared collection and the clicked section’s initial index, so previous/next navigation works across the other renderable visuals from the same message.
|
||||
|
||||
### 5. Reuse existing editor helpers without a new dependency edge
|
||||
The new visual-section renderers should reuse existing editor helpers rather than inventing a separate rendering stack.
|
||||
|
||||
For file-backed images:
|
||||
- extract or adapt the existing asset-resolution helper in `crates/editor/src/content/edit.rs (56-91)` so the block list can resolve relative paths against a base directory rather than a document path
|
||||
- pass the AI block’s stored working directory as that base directory
|
||||
- on native platforms, canonicalize when possible
|
||||
- on WASM, preserve the existing no-canonicalization behavior
|
||||
|
||||
For Mermaid:
|
||||
- use `mermaid_asset_source` from `crates/editor/src/content/mermaid_diagram.rs (20-67)`
|
||||
- derive the rendered max width from the loaded asset's intrinsic size rather than calling a separate Mermaid layout helper from the block-list renderer
|
||||
- honor `FeatureFlag::MarkdownMermaid` in addition to the new block-list image flag
|
||||
|
||||
For actual rendering:
|
||||
- use a block-list-local renderer in the existing section flow
|
||||
- use WarpUI image elements and existing block-list spacing/styling conventions
|
||||
- preserve the existing right-click-to-copy Markdown behavior on every rendered visual
|
||||
- for inline image runs, render medium-height image tiles with per-image labels below
|
||||
- for grouped block images, render stacked thumbnail-plus-path rows using the full source path text
|
||||
- for Mermaid, render a bordered card shell with a titled header and a white inner canvas that contains the diagram
|
||||
- on left click, open the shared workspace lightbox instead of a block-list-specific fullscreen modal for both rendered file-backed images and rendered Mermaid diagrams
|
||||
- keep the path labels on the shared link-detection pipeline so hover/click behavior remains unchanged
|
||||
- allow the implementation to harden shared image/text primitives where needed for this surface, including guarding non-finite image rects, exposing intrinsic SVG dimensions through shared image metadata, and constraining single-line text hit testing to valid Y bounds
|
||||
|
||||
This follows the same broad pattern as the block list’s existing special markdown sections: section-specific render helpers living inside the current block-list message renderer, with reusable lower-level helpers coming from crates the app already depends on.
|
||||
|
||||
### 6. Render fallback content instead of broken visuals
|
||||
The renderer should decide per visual section whether it can render as a visual block or must fall back to raw Markdown.
|
||||
|
||||
For file-backed images:
|
||||
- if `BlocklistMarkdownImages` is disabled, render `markdown_source`
|
||||
- if the path cannot be resolved, render `markdown_source`
|
||||
- if the asset fails to load or is an unsupported image type, render `markdown_source`
|
||||
- only include images in an inline row or grouped block row set when that individual image succeeds; unsupported or missing images fall back to raw Markdown in source order
|
||||
|
||||
For Mermaid:
|
||||
- if either `BlocklistMarkdownImages` or `MarkdownMermaid` is disabled, render `markdown_source`
|
||||
- if Mermaid SVG generation fails, render `markdown_source`
|
||||
|
||||
This keeps fallback behavior local to rendering and avoids mutating stored section payloads based on transient failures.
|
||||
|
||||
### 7. Make block-level copy and export section-aware
|
||||
Update the markdown copy/export paths in `app/src/ai/agent/mod.rs (1547-1628, 2722-2779)` so that `Image` and `MermaidDiagram` sections serialize from their preserved Markdown source.
|
||||
|
||||
Rules:
|
||||
- block-level output copy uses `markdown_source` for image and Mermaid sections
|
||||
- conversation export continues to be markdown, not image bytes or HTML
|
||||
- right-click copy on a rendered image or Mermaid diagram writes that section’s `markdown_source`
|
||||
|
||||
This gives us correct image/Mermaid copy behavior without changing the existing selection architecture.
|
||||
|
||||
### 8. Render the file-link label with local link detection
|
||||
The new file-link label under rendered file-backed images should reuse the existing text/link rendering helpers rather than introducing a special-case link widget.
|
||||
|
||||
Implementation shape:
|
||||
- keep the current global output link-detection pipeline unchanged for the underlying markdown section data
|
||||
- in the successful image-render path, render the link text in the layout-specific position required by the product behavior
|
||||
- keep using the shared `DetectedLinksState` keyed by the section index so the path labels get normal file-link highlighting and click behavior without a second link-detection system
|
||||
- keep binary-file link opening on the standard file-open path so those labels resolve to the platform default app rather than `$EDITOR`
|
||||
- keep find/highlight behavior scoped to the existing global text pipeline rather than refactoring search indexing for this label in the same change
|
||||
|
||||
This keeps the change narrowly targeted while still giving users a visible, interactive file link under rendered file-backed images.
|
||||
|
||||
### 9. Preserve existing selection behavior
|
||||
Do not add a new drag-selection model for rendered visuals in this change.
|
||||
|
||||
The expected behavior for this implementation is:
|
||||
- existing text/code/table selection behavior remains unchanged
|
||||
- rendered images and Mermaid diagrams expose explicit copy behavior through right-click/context-menu affordances
|
||||
- block-level copy/export includes the right raw Markdown for visual sections
|
||||
|
||||
That is the intended “minimal change” scope for this feature.
|
||||
|
||||
### 10. Keep restored conversations deterministic
|
||||
Restored conversations should not depend on reparsing with different metadata than the original render.
|
||||
|
||||
The renderer should use:
|
||||
- the section payload stored on the conversation output
|
||||
- the `working_directory` already persisted with the exchange/block
|
||||
- current runtime flags to decide whether to render a visual or raw fallback
|
||||
|
||||
That means restored blocks will:
|
||||
- resolve relative file images against the original captured working directory
|
||||
- render Mermaid only when both flags are currently enabled
|
||||
- still show raw Markdown if the file is gone or the platform cannot load it
|
||||
|
||||
## End-to-End Flow
|
||||
1. The AI backend streams markdown text into an `AIAgentOutputMessageType::Text`.
|
||||
2. `parse_markdown_into_text_and_code_sections` in `app/src/ai/agent/util.rs` identifies plain text, code, tables, parser-recognized images, and Mermaid sections.
|
||||
3. The parsed output stores explicit `Image` and `MermaidDiagram` sections alongside their raw Markdown source.
|
||||
4. `app/src/ai/blocklist/block/view_impl/output.rs` renders the new visual sections through the existing section-based block-list renderer alongside plain text, code, and tables.
|
||||
5. The visual-section renderers receive the AI block’s stored `current_working_directory` and `shell_launch_data`.
|
||||
6. File-backed image sections resolve their source path relative to that working directory.
|
||||
7. Mermaid sections build an in-memory SVG asset source with `mermaid_asset_source`.
|
||||
8. The block-list renderer derives a source-ordered lightbox collection from the renderable visual sections in that message.
|
||||
9. Each visual section either renders as an image block or falls back to a plain markdown block depending on feature flags and asset/render success.
|
||||
10. When a file-backed image renders successfully, the renderer also shows a clickable/highlighted file link beneath or beside it:
|
||||
- inline image rows show the basename / final path segment
|
||||
- block image rows show the full source path
|
||||
11. Clicking a rendered image or Mermaid diagram dispatches `WorkspaceAction::OpenLightbox` with that shared visual collection and the clicked section’s initial index.
|
||||
12. Right-click copy on a rendered image or Mermaid diagram writes the section’s `markdown_source`.
|
||||
13. Clicking a rendered file link continues to use the shared file-open flow, which means binary file labels open in the platform default app instead of `$EDITOR`.
|
||||
14. Block-level copy and conversation export continue to emit markdown, using `markdown_source` for visual sections.
|
||||
15. When the conversation is restored later, the same section payload renders again using the stored working-directory metadata and current flags.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
### Risk: selection expectations drift beyond the implementation
|
||||
The current AI block architecture does not support unified mixed-content selection across renderer types. If the specs or implementation drift back toward that requirement, this feature would expand substantially.
|
||||
|
||||
Mitigation:
|
||||
- keep the product and tech specs explicit that this change does not add a new cross-renderer selection model
|
||||
- keep the scope focused on rendering, explicit copy affordances, and block-level copy/export correctness
|
||||
|
||||
### Risk: parser scope does not match full Markdown image semantics
|
||||
Standard Markdown treats images as inline syntax, but our current parser only recognizes standalone-image lines, and this redesign needs a narrow same-line image-row case.
|
||||
|
||||
Mitigation:
|
||||
- keep the new same-line parser limited to lines that consist entirely of parser-compatible image references separated only by whitespace
|
||||
- do not treat arbitrary prose containing `![...]` as inline-renderable image content
|
||||
- treat fuller CommonMark-style inline image support as follow-up parser work
|
||||
|
||||
### Risk: relative path resolution diverges between notebooks and AI blocks
|
||||
The existing editor helper resolves relative image paths against a document path, while AI blocks need to resolve against the captured session working directory.
|
||||
|
||||
Mitigation:
|
||||
- factor path resolution into a base-directory-oriented helper
|
||||
- update notebook and AI-block callers to use it explicitly
|
||||
- add native and WASM coverage
|
||||
|
||||
### Risk: regressions to existing section rendering
|
||||
Adding two more section types to the current renderer could affect section indexing, link-detection offsets, or restored-block bookkeeping.
|
||||
|
||||
Mitigation:
|
||||
- keep section ordering explicit in `AIAgentOutput::all_text` and render traversal
|
||||
- follow existing per-section bookkeeping patterns for code/table sections
|
||||
- add mixed-content coverage that includes ordinary code blocks and tables, not just images
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Unit tests
|
||||
- extend `app/src/ai/agent/util_tests.rs` with:
|
||||
- standalone image extraction
|
||||
- same-line inline image-run extraction
|
||||
- standalone multi-line block-image extraction
|
||||
- image + table + text ordering
|
||||
- Mermaid section extraction
|
||||
- add block-list markdown copy tests covering:
|
||||
- image sections serialize to ``
|
||||
- Mermaid sections serialize to fenced mermaid markdown
|
||||
- add native/WASM unit tests around relative path resolution against a base directory
|
||||
- add focused rendering/helper coverage for:
|
||||
- section-index preservation across skipped empty text sections
|
||||
- lightbox index mapping in source order
|
||||
- inline-image basename labeling
|
||||
- visual width guard rails for invalid sizes
|
||||
- add shared primitive regression tests for:
|
||||
- invalid image rect rejection
|
||||
- SVG intrinsic image sizing
|
||||
- single-line text hit testing respecting Y bounds
|
||||
|
||||
### Integration tests
|
||||
- A reasonable first integration test is a single manual-observation test, `test_restored_ai_block_renders_mermaid_and_local_images`, in `crates/integration/src/test/agent_mode.rs`.
|
||||
- That test would restore a synthetic `ConversationData` through `load_conversation_from_tasks`, set `InputContext.directory` to `crates/warpui_core/test_data`, and capture a real-display screenshot of one AI response that contains same-line local image markdown plus a Mermaid fence.
|
||||
- It would be intentionally narrow: it would prove the restored AI block list can render both surfaces through the real conversation-hydration path without introducing protobuf fixtures or test-only UI hooks.
|
||||
- Recommended follow-up integration coverage remains:
|
||||
- an AI response containing plain text, a linked code block, a markdown table, a relative-path image, and a Mermaid diagram
|
||||
- block-level copy serialization for mixed content
|
||||
- restored-conversation rendering and fallback behavior
|
||||
- disabled-flag fallback to raw Markdown
|
||||
- shared-lightbox initial selection and previous/next navigation in message source order
|
||||
|
||||
### Manual validation
|
||||
- relative and absolute local image paths for PNG, JPEG, GIF, WebP, and SVG
|
||||
- inline same-line image rows with path labels below each image
|
||||
- grouped standalone block-image rows with thumbnail-plus-path content
|
||||
- missing-file fallback
|
||||
- unsupported-format fallback
|
||||
- Mermaid render success and Mermaid render failure fallback in the new framed treatment
|
||||
- click-to-expand from both rendered images and Mermaid diagrams into the shared lightbox
|
||||
- previous/next keyboard and button navigation inside the lightbox
|
||||
- right-click copy on both file-backed images and Mermaid diagrams
|
||||
- block-level copy on a response containing text, code, table, image, and Mermaid content
|
||||
- restored conversation rendering after reopening history
|
||||
- WASM sanity pass to verify path handling and fallback behavior do not panic
|
||||
|
||||
## Follow-ups
|
||||
- full CommonMark-style inline image support if we decide to expand `markdown_parser` beyond its current standalone-image behavior
|
||||
- unified mixed-content selection across block-list renderer types
|
||||
- consolidating `MarkdownImages` and `BlocklistMarkdownImages` if Warp later ships a broader app-wide markdown-image rollout
|
||||
- richer image interactions such as open/save/zoom, if product wants them later
|
||||
@@ -0,0 +1,212 @@
|
||||
# Markdown Table Styling Consistency — Product Spec
|
||||
Linear: none provided
|
||||
Figma: House of Agents — https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7451-99490&t=NvrWl7bhDEC5kpKF-1
|
||||
Reference styling baseline: PR #23908 — https://github.com/warpdotdev/warp-internal/pull/23908
|
||||
|
||||
## Summary
|
||||
Make rendered Markdown tables use the same blockless visual treatment across every Warp surface that renders Markdown tables.
|
||||
|
||||
The block list styling introduced in PR #23908 should become the visual baseline for Markdown tables everywhere they render in Warp, including Markdown notebooks and the Markdown editor. This is a consistency and presentation change, not a redesign of Markdown table semantics or editing behavior.
|
||||
|
||||
## Problem
|
||||
Warp currently renders the same Markdown table with noticeably different visual treatments depending on where the user sees it.
|
||||
|
||||
After PR #23908, AI block list tables use a lighter, blockless presentation based on the House of Agents design: no outer container, no full border, no vertical dividers, no filled header row, and more typography-driven hierarchy. Other Markdown-rendering surfaces still use the older table treatment, which creates visible inconsistency in:
|
||||
|
||||
- border and container chrome
|
||||
- row density
|
||||
- header emphasis
|
||||
- background fills
|
||||
- overall visual weight
|
||||
|
||||
This makes the same Markdown content feel like different components in different parts of the product. Users moving between notebooks, the Markdown editor, AI documents, comment editors, file notebooks, and AI output should not have to mentally re-parse the same table because Warp styled it differently in each surface.
|
||||
|
||||
## Goals
|
||||
- Make rendered Markdown tables in every current Warp Markdown-rendering surface match the visual treatment now used in the AI block list.
|
||||
- Treat the merged block list styling from PR #23908 as the visual source of truth for this work.
|
||||
- Preserve existing Markdown table semantics, supported inline formatting, alignment rules, and editability.
|
||||
- Preserve existing selection, cursor, link, copy, and scrolling behavior unless a change is required to achieve the styling consistency.
|
||||
- Ensure the same Markdown table feels like the same component across all Warp Markdown-rendering surfaces.
|
||||
- Ensure future Warp surfaces that adopt Markdown table rendering inherit this same style by default rather than introducing another table treatment.
|
||||
|
||||
## Non-goals
|
||||
- Changing Markdown table parsing rules or supported syntax.
|
||||
- Introducing new Markdown table editing capabilities.
|
||||
- Changing block list table styling again as part of this work.
|
||||
- Redesigning non-table Markdown blocks.
|
||||
- Converting non-table text into tables.
|
||||
- Broadly restyling every table-like UI in Warp outside Markdown-rendered tables.
|
||||
|
||||
## Figma / design references
|
||||
- Figma: House of Agents — https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7451-99490&t=NvrWl7bhDEC5kpKF-1
|
||||
- PR baseline: https://github.com/warpdotdev/warp-internal/pull/23908
|
||||
- Existing related spec: `specs/blocklist-markdown-table-rendering/PRODUCT.md`
|
||||
|
||||
The Figma node shows the blockless table treatment that now exists in the AI block list and should be reused for Markdown tables across Warp.
|
||||
|
||||
## User experience
|
||||
|
||||
### Scope
|
||||
This feature applies to rendered GitHub Flavored Markdown tables in:
|
||||
- Markdown notebooks
|
||||
- the Markdown editor
|
||||
- file notebooks and other rendered Markdown file views
|
||||
- AI documents and other notebook-backed planning/editor surfaces
|
||||
- Markdown comment editors and other lightweight rendered Markdown editors
|
||||
- any other current or future Warp surface that renders Markdown tables through the shared Markdown rendering stack or through a dedicated Markdown table renderer
|
||||
|
||||
If a Warp surface renders a valid Markdown table as a table, it should adopt the updated styling described here.
|
||||
|
||||
This feature does not change the appearance of raw Markdown source when the user is viewing or editing literal source text rather than a rendered table presentation.
|
||||
|
||||
### Visual consistency rule
|
||||
The user-visible rule is simple:
|
||||
|
||||
The same valid Markdown table should look materially the same in the AI block list and in every other Warp surface that renders Markdown tables.
|
||||
|
||||
The goal is not approximate similarity. The goal is one shared visual treatment for Markdown-rendered tables across these surfaces.
|
||||
|
||||
### Styling invariants
|
||||
Rendered Markdown tables in all Warp Markdown-rendering surfaces should adopt the same structural styling introduced for block list tables in PR #23908.
|
||||
|
||||
That means:
|
||||
|
||||
- the table renders directly in the Markdown flow rather than inside a card-like or boxed container
|
||||
- the table does not show a rounded outer container
|
||||
- the table does not show a full perimeter border
|
||||
- the table does not show vertical divider lines between columns
|
||||
- the header row does not use a filled background distinct from the table body
|
||||
- body rows do not use alternating zebra-striping backgrounds
|
||||
- the header row uses stronger typography than body rows
|
||||
- header text uses primary text color
|
||||
- body text uses secondary text color
|
||||
- row separators are thin horizontal dividers only
|
||||
- row spacing matches the block list treatment, with a more open vertical rhythm than the older notebook/editor style
|
||||
- the table should feel visually lighter and less boxed-in than the previous notebook/editor treatment
|
||||
|
||||
The blockless Figma version is the intended design target.
|
||||
|
||||
### Typography and spacing
|
||||
The table should continue to use the surrounding Markdown typography system for font family and sizing, but with the same hierarchy used in the block list treatment:
|
||||
|
||||
- headers are bold or semibold and visually prominent
|
||||
- body cells are regular weight
|
||||
- body text uses the same size as surrounding Markdown body text
|
||||
- vertical padding should match the block list style closely, approximately 12px per row
|
||||
|
||||
If theme-specific token values differ across surfaces, the relationship must still remain the same:
|
||||
|
||||
- header text is visually stronger than body text
|
||||
- divider lines are subtle
|
||||
- the table has no filled container or boxed background treatment
|
||||
|
||||
### Functional behavior that must remain unchanged
|
||||
This restyle should not change what Markdown tables mean or how users interact with them.
|
||||
|
||||
The following behaviors should remain unchanged unless a specific implementation constraint requires an adjustment:
|
||||
|
||||
- valid table detection
|
||||
- column alignment behavior
|
||||
- inline Markdown rendering inside cells
|
||||
- text selection within a cell
|
||||
- text selection across cells and rows
|
||||
- cursor placement and text editing behavior in editable contexts
|
||||
- link rendering and interaction
|
||||
- copy behavior
|
||||
- horizontal overflow handling for wide tables
|
||||
- surrounding Markdown flow before and after the table
|
||||
|
||||
This is a styling consistency feature, not a behavior change feature.
|
||||
|
||||
### Alignment and inline formatting parity
|
||||
All currently supported Markdown table content should continue to render correctly after the restyle.
|
||||
|
||||
At minimum, every affected Markdown-rendering surface must continue to support:
|
||||
|
||||
- left, center, and right column alignment
|
||||
- bold text
|
||||
- italic text
|
||||
- bold-italic text
|
||||
- inline code
|
||||
- strikethrough
|
||||
- links
|
||||
- escaped pipe characters rendered as literal pipes
|
||||
- multiline cell content, if already supported today
|
||||
|
||||
Changing the visual style must not collapse centered or right-aligned columns back to left alignment.
|
||||
|
||||
### Editing contexts
|
||||
Some Markdown table surfaces are editable and some are read-only. The restyle must not make editing feel worse anywhere editing is supported today.
|
||||
|
||||
If a user can place a cursor in a rendered table cell today, they should still be able to do so after the change.
|
||||
|
||||
If a user can:
|
||||
|
||||
- click into a cell
|
||||
- move the cursor with keyboard navigation
|
||||
- select text across table content
|
||||
- type, delete, or paste within the table
|
||||
|
||||
those interactions must continue to work with the new styling.
|
||||
|
||||
The visual update must not reduce hit targets, make selections harder to read, or create ambiguity about which cell is active.
|
||||
|
||||
### Wide tables
|
||||
Wide tables should remain readable and usable.
|
||||
|
||||
If a table requires horizontal overflow handling, the updated styling must preserve the user’s ability to:
|
||||
|
||||
- access off-screen columns
|
||||
- read header and cell content clearly
|
||||
- select text after horizontally scrolling
|
||||
- edit content in editable surfaces if that behavior is supported today
|
||||
|
||||
This feature should not introduce clipping, truncation, or layout breakage that did not exist before.
|
||||
|
||||
### Invalid or unsupported table-like content
|
||||
This feature does not change fallback behavior.
|
||||
|
||||
If content is not a valid Markdown table and currently falls back to normal Markdown text rendering, it should continue to do so. Likewise, table-looking content inside code blocks should continue to render as code, not as a table.
|
||||
|
||||
### Existing and newly rendered content
|
||||
The updated style should apply consistently to:
|
||||
|
||||
- newly created notebooks and documents
|
||||
- existing notebooks and documents when reopened
|
||||
- tables already present in saved content
|
||||
- new tables inserted after the feature ships
|
||||
- any current or future surface that renders Markdown tables once it adopts the shared renderer or shared style source
|
||||
|
||||
Users should not have to migrate or rewrite table Markdown to get the new treatment.
|
||||
|
||||
## Success criteria
|
||||
- A Markdown table rendered in any current Warp Markdown-rendering surface uses the same blockless styling pattern as the AI block list table from PR #23908.
|
||||
- Warp no longer shows two competing visual treatments for rendered Markdown tables across its current Markdown surfaces.
|
||||
- Other Markdown-rendering surfaces no longer show the older table chrome such as a full outer border, vertical dividers, zebra striping, or a filled header background.
|
||||
- Header rows remain visually distinct through typography and text color rather than heavy background treatment.
|
||||
- Body rows use secondary text color and subtle horizontal separators, matching the block list treatment.
|
||||
- The same Markdown table shown in the AI block list and in other Warp Markdown-rendering surfaces looks materially identical aside from surface-specific typography or layout constraints.
|
||||
- Alignment behavior remains correct for left-, center-, and right-aligned columns.
|
||||
- Inline Markdown inside cells continues to render correctly.
|
||||
- Editing and selection behavior in editable Markdown surfaces does not regress.
|
||||
- Wide tables remain usable and readable.
|
||||
- Invalid Markdown table input continues to fall back to existing non-table behavior.
|
||||
|
||||
## Validation
|
||||
- Manual validation with the same Markdown table rendered in the AI block list and in multiple editor-backed surfaces, including:
|
||||
- AI block list
|
||||
- Markdown notebook
|
||||
- Markdown editor
|
||||
- Spot-check validation in other current Markdown-rendering surfaces that use the shared rich-text Markdown renderer, such as AI documents, file notebooks, and comment editors where practical.
|
||||
- Visual comparison against the Figma node and the current AI block list implementation from PR #23908.
|
||||
- Manual validation that affected Markdown-rendering surfaces no longer show an outer border, vertical dividers, zebra striping, or a filled header row.
|
||||
- Manual validation that header typography, body text color, divider treatment, and row spacing match the block list treatment closely.
|
||||
- Manual validation of left-, center-, and right-aligned columns.
|
||||
- Manual validation of inline formatting inside cells: bold, italic, bold-italic, inline code, strikethrough, links, and escaped pipes.
|
||||
- Manual validation of cursor placement, selection, typing, deletion, and paste behavior in editable contexts.
|
||||
- Manual validation of wide tables that require horizontal scrolling.
|
||||
- Regression validation that malformed table-like content and fenced code blocks continue to avoid table rendering.
|
||||
- Screenshot-based review confirming that the editor-backed result is visually consistent with the merged block list result.
|
||||
|
||||
## Open questions
|
||||
None currently. New Warp surfaces that render Markdown tables should inherit this style by default.
|
||||
@@ -0,0 +1,255 @@
|
||||
# Markdown Table Styling Consistency — Tech Spec
|
||||
Product spec: `specs/zachlloyd/markdown-table-consistency/PRODUCT.md`
|
||||
|
||||
## Problem
|
||||
PR #23908 updated AI block list Markdown tables to the new blockless visual treatment, but the rest of Warp’s Markdown table renderers still use the older boxed table styling. On the current branch, Markdown table appearance is owned by two separate rendering paths:
|
||||
|
||||
- editor-backed Markdown surfaces use `RichTextStyles.table_style`
|
||||
- AI block list tables build a separate WarpUI `TableConfig` inline
|
||||
|
||||
That split creates two technical problems:
|
||||
|
||||
1. the shared editor-backed renderer still paints outer borders, vertical dividers, filled header backgrounds, and alternating row backgrounds
|
||||
2. future surfaces can drift again because there is no single shared source for Markdown table chrome
|
||||
|
||||
The implementation should make every current Markdown table renderer inherit the same structural style while preserving surface-specific typography and existing editing/selection behavior.
|
||||
|
||||
## Relevant code
|
||||
- `specs/zachlloyd/markdown-table-consistency/PRODUCT.md` — approved product behavior
|
||||
- `specs/blocklist-markdown-table-rendering/TECH.md` — prior AI block list table implementation plan
|
||||
- `app/src/notebooks/editor/mod.rs (145-215)` — `rich_text_styles`; current shared editor-backed table style source
|
||||
- `crates/editor/src/render/model/mod.rs (424-568)` — `RichTextStyles` and `TableStyle`
|
||||
- `crates/editor/src/render/element/table.rs:1-220` — editor-backed Markdown table painting path
|
||||
- `app/src/ai/blocklist/block/view_impl/common.rs (1140-1270)` — `render_table_section`; current AI block list table renderer and inline `TableConfig`
|
||||
- `crates/warpui_core/src/elements/table/mod.rs (117-239)` — shared `TableConfig`, `RowBackground`, and `TableVerticalSizing`
|
||||
- `crates/warpui_core/src/elements/table/mod.rs (537-759)` — intrinsic width measurement logic
|
||||
- `crates/warpui_core/src/elements/table/mod.rs (1013-1211)` — viewported row layout and painting behavior
|
||||
- `app/src/notebooks/file/mod.rs (230-278)` — file notebook view using `rich_text_styles`
|
||||
- `app/src/ai/ai_document_view.rs (292-321)` — AI document view fallback editor using `rich_text_styles`
|
||||
- `app/src/ai/document/ai_document_model.rs (783-801)` — AI document model creating notebook-backed editors with `rich_text_styles`
|
||||
- `app/src/code/editor/comment_editor.rs (505-544)` — comment editor using `rich_text_styles`
|
||||
- `app/src/code/editor/view.rs (2357-2381)` — `code_text_styles`; clones `rich_text_styles` before overriding non-table text settings
|
||||
|
||||
## Current state
|
||||
|
||||
### Editor-backed Markdown tables
|
||||
Most rendered Markdown surfaces in the app are backed by `NotebooksEditorModel` and the shared editor renderer. Their table appearance comes from `rich_text_styles()` in `app/src/notebooks/editor/mod.rs`, which currently sets:
|
||||
|
||||
- `border_color = theme.surface_3()`
|
||||
- `header_background = theme.surface_2()`
|
||||
- `cell_background = theme.background()`
|
||||
- `alternate_row_background = Some(theme.surface_2())`
|
||||
- `cell_padding = 6.`
|
||||
|
||||
That style is then consumed by `RenderableTable` in `crates/editor/src/render/element/table.rs`. The renderer currently always paints:
|
||||
|
||||
- row backgrounds for header and body
|
||||
- an outer top/bottom/left/right border
|
||||
- horizontal row dividers
|
||||
- vertical column dividers
|
||||
|
||||
This is the old notebook/editor table treatment that the new product spec wants to replace.
|
||||
|
||||
### Surfaces that inherit the editor-backed table style
|
||||
The old table style is not limited to notebooks. The same `rich_text_styles()` entry point is reused by multiple surfaces:
|
||||
|
||||
- notebooks
|
||||
- file notebooks
|
||||
- AI documents / planning docs
|
||||
- Markdown comment editors
|
||||
|
||||
In addition, `code_text_styles()` starts from `rich_text_styles()` before overriding paragraph-level settings, so any code-editor surface that ends up rendering Markdown tables also inherits the same table style unless it explicitly overrides it.
|
||||
|
||||
This means a change to the shared editor-backed style source will propagate to multiple surfaces automatically.
|
||||
|
||||
### AI block list tables
|
||||
The AI block list no longer uses the old style. `render_table_section()` in `app/src/ai/blocklist/block/view_impl/common.rs` builds a WarpUI `Table` with an inline `TableConfig` that already matches the desired structural treatment:
|
||||
|
||||
- `outer_border: false`
|
||||
- `column_dividers: false`
|
||||
- `row_dividers: true`
|
||||
- transparent header/background fills
|
||||
- `cell_padding: 12.`
|
||||
- `TableVerticalSizing::ExpandToContent`
|
||||
- `measure_body_cells_for_intrinsic_widths: true`
|
||||
|
||||
That renderer is already the visual baseline from PR #23908, but it is defined inline inside the block list path rather than shared with editor-backed renderers.
|
||||
|
||||
### Ownership problem
|
||||
Today there is no shared “Markdown table appearance” abstraction. The editor-backed path and block list path both describe the same visual decisions in different structures:
|
||||
|
||||
- `TableStyle` for the editor renderer
|
||||
- `TableConfig` plus per-cell text styling for WarpUI `Table`
|
||||
|
||||
As a result:
|
||||
|
||||
- the two renderers can drift
|
||||
- future surfaces may pick one path and forget to copy the latest style
|
||||
- structural decisions like “no outer border” or “no column dividers” are not represented in the editor-backed style model yet
|
||||
|
||||
## Proposed changes
|
||||
|
||||
### 1. Introduce a shared Markdown table appearance helper
|
||||
Add a small shared helper at the app layer that defines the canonical Markdown table chrome for the new blockless treatment.
|
||||
|
||||
The helper should represent the structural and color decisions that must stay aligned across renderers:
|
||||
|
||||
- divider color
|
||||
- header text color
|
||||
- body text color
|
||||
- transparent vs filled backgrounds
|
||||
- whether outer borders are shown
|
||||
- whether column dividers are shown
|
||||
- whether row dividers are shown
|
||||
- row striping behavior
|
||||
- cell padding
|
||||
|
||||
Typography should remain surface-specific. The helper should define the table chrome and text-color hierarchy, not force every surface to use the same font family or font size.
|
||||
|
||||
This gives us one source of truth that both renderers can map from.
|
||||
|
||||
### 2. Extend `TableStyle` so the editor renderer can express the blockless treatment
|
||||
`crates/editor/src/render/model/mod.rs` currently cannot represent some of the structural choices already used by the block list renderer.
|
||||
|
||||
Extend `TableStyle` with the missing structural controls needed by the product spec:
|
||||
|
||||
- `outer_border: bool`
|
||||
- `column_dividers: bool`
|
||||
- `row_dividers: bool`
|
||||
|
||||
Keep the existing color and typography fields. The important change is letting the editor-backed renderer express “only horizontal separators, no outer border, no vertical dividers” directly instead of baking those assumptions into painting logic.
|
||||
|
||||
### 3. Update the editor-backed table painter to honor the expanded style model
|
||||
Update `crates/editor/src/render/element/table.rs` so painting follows `TableStyle` rather than hardcoded table chrome assumptions.
|
||||
|
||||
Specifically:
|
||||
|
||||
- `paint_backgrounds()` should respect transparent header/body backgrounds and the absence of alternating row backgrounds
|
||||
- `paint_borders()` should paint only the borders/dividers enabled by `TableStyle`
|
||||
- the renderer should continue using the existing text layout, selection, cursor, and alignment logic unchanged
|
||||
|
||||
This keeps the behavioral parts of the editor renderer stable while changing only the visual treatment.
|
||||
|
||||
### 4. Make `rich_text_styles()` produce the new Markdown table style
|
||||
Update `app/src/notebooks/editor/mod.rs` so `rich_text_styles()` returns the blockless Markdown table style from the new shared helper.
|
||||
|
||||
That means the shared editor-backed path should move from the current boxed style to:
|
||||
|
||||
- no outer border
|
||||
- no column dividers
|
||||
- row dividers only
|
||||
- transparent header background
|
||||
- transparent body background
|
||||
- no alternating row backgrounds
|
||||
- header/body text colors matching the block list hierarchy
|
||||
- cell padding aligned with the block list treatment
|
||||
|
||||
This change is the main propagation point for notebooks, file notebooks, AI documents, comment editors, and any other surface using the shared editor-backed Markdown renderer.
|
||||
|
||||
### 5. Refactor the block list table renderer to consume the same shared appearance helper
|
||||
Update `app/src/ai/blocklist/block/view_impl/common.rs` so the block list no longer constructs its structural table chrome entirely inline.
|
||||
|
||||
The block list should still keep its surface-specific text settings where needed:
|
||||
|
||||
- AI font family
|
||||
- AI font size
|
||||
- AI font weight
|
||||
- AI selection color
|
||||
|
||||
But the structural table configuration should come from the same shared appearance helper used by `rich_text_styles()`. That keeps the existing block list result visually unchanged while preventing future drift.
|
||||
|
||||
### 6. Preserve surface-specific typography
|
||||
The product goal is shared styling, not identical typography across unrelated surfaces.
|
||||
|
||||
The shared appearance helper should therefore be mapped differently by each renderer:
|
||||
|
||||
- editor-backed surfaces continue using their surface’s `font_family` / `font_size` in `TableStyle`
|
||||
- block list continues using its AI-output text settings
|
||||
|
||||
What must remain shared is the blockless table chrome and the text hierarchy relationship, not every literal font token.
|
||||
|
||||
### 7. Treat the shared helper as the default for future Markdown table renderers
|
||||
Document in the code by naming and placement that this helper is the default source for Markdown table appearance in Warp.
|
||||
|
||||
The goal is that a new Markdown-rendering surface should not invent its own `TableConfig` or `TableStyle` values for tables unless it has a clear product reason to diverge.
|
||||
|
||||
## End-to-end flow
|
||||
1. A surface creates a rendered Markdown editor or table view.
|
||||
2. If it is editor-backed, it obtains `RichTextStyles` from `rich_text_styles()` or `code_text_styles()`.
|
||||
3. `rich_text_styles()` builds `table_style` from the shared Markdown table appearance helper.
|
||||
4. The editor renderer lays out and paints table content using the updated `TableStyle`, which now supports the blockless chrome.
|
||||
5. If it is the AI block list path, `render_table_section()` builds its WarpUI `Table` using the same shared appearance helper, while keeping block-list-specific typography and selection settings.
|
||||
6. The user sees the same structural Markdown table treatment across surfaces, with existing interaction behavior preserved.
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
### Risk: editor-backed behavior regressions
|
||||
Changing `RenderableTable` painting could accidentally affect selection visibility, cursor readability, or perceived cell hit areas.
|
||||
|
||||
Mitigation:
|
||||
- keep layout, selection, cursor, and alignment code unchanged
|
||||
- scope the change to styling and border/background painting
|
||||
- validate editable surfaces manually after the visual update
|
||||
|
||||
### Risk: block list drifts again later
|
||||
If the block list keeps hand-authoring its own `TableConfig`, the two renderers can diverge again even after this change.
|
||||
|
||||
Mitigation:
|
||||
- refactor both paths to read from the same shared appearance helper
|
||||
- avoid leaving structural table chrome duplicated inline
|
||||
|
||||
### Risk: future surfaces bypass the shared style
|
||||
Even after current surfaces are fixed, a new renderer could hardcode another table style.
|
||||
|
||||
Mitigation:
|
||||
- make the helper discoverable and clearly named as the canonical Markdown table appearance
|
||||
- reference it directly from both existing rendering paths so future work sees the pattern
|
||||
|
||||
### Risk: typography becomes unintentionally identical everywhere
|
||||
If the shared helper carries too much font data, it could flatten legitimate surface-specific typography differences.
|
||||
|
||||
Mitigation:
|
||||
- keep the helper focused on chrome and text hierarchy
|
||||
- let each renderer keep its own font family, size, and weight choices where appropriate
|
||||
|
||||
## Testing and validation
|
||||
|
||||
### Shared-style tests
|
||||
- Add focused tests for the shared Markdown table appearance helper so its structural defaults are explicit:
|
||||
- no outer border
|
||||
- no column dividers
|
||||
- row dividers enabled
|
||||
- transparent backgrounds
|
||||
- no alternating row striping
|
||||
- updated padding
|
||||
|
||||
### Editor-backed renderer coverage
|
||||
- Add or update tests around editor-backed Markdown tables to ensure table rendering still occurs and existing Markdown table behavior does not regress.
|
||||
- Keep existing geometry/selection-oriented table tests in `crates/editor/src/render/element/table_tests.rs` passing after the style changes.
|
||||
|
||||
### Surface propagation checks
|
||||
- Manual validation in:
|
||||
- Markdown notebook
|
||||
- Markdown editor
|
||||
- file notebook
|
||||
- AI document / planning document
|
||||
- Markdown comment editor
|
||||
- AI block list
|
||||
- Confirm these surfaces all show the same blockless table chrome.
|
||||
|
||||
### Behavior regression checks
|
||||
- Manual validation of:
|
||||
- left/center/right alignment
|
||||
- inline formatting inside cells
|
||||
- cursor placement in editable contexts
|
||||
- selection within and across cells
|
||||
- link rendering and interaction
|
||||
- wide-table overflow handling
|
||||
|
||||
### Visual validation
|
||||
- Screenshot-based comparison against the Figma node and the AI block list implementation from PR #23908
|
||||
- Manual confirmation that the older notebook/editor chrome is gone everywhere in scope
|
||||
|
||||
## Follow-ups
|
||||
- If additional Markdown table renderers appear outside the current editor-backed path and block list path, route them through the shared appearance helper rather than creating another table style definition.
|
||||
- If we later want stricter visual parity between surfaces, we can consider a deeper shared text-style adapter, but that should be a follow-up after the chrome is unified.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Restore Fast Forward State Across Sessions — Product Spec
|
||||
|
||||
Figma: none provided
|
||||
|
||||
## Summary
|
||||
|
||||
Persist the last state of the fast-forward button for agent conversations across app restarts using Warp's existing session restoration flow. If a restored conversation had fast forward enabled before quit, it should come back enabled after launch; if it was disabled before quit, it should come back disabled.
|
||||
|
||||
## Problem
|
||||
|
||||
Fast forward is currently a per-conversation control during an active session, but that choice is lost when Warp restarts. Users who intentionally enabled or disabled fast forward for a conversation have to remember and re-apply that choice after session restoration, which makes restored agent conversations feel inconsistent and unreliable.
|
||||
|
||||
## Goals
|
||||
|
||||
- Preserve fast-forward state for restored agent conversations across app restarts.
|
||||
- Keep the behavior per conversation rather than turning it into a global setting.
|
||||
- Make the restored state reflect the user's last explicit choice, even if they toggled it shortly before quitting.
|
||||
- Ship the behavior behind a feature flag that is enabled by default for dogfood builds.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing the user's default autonomy settings or global fast-forward preference.
|
||||
- Changing the visibility or semantics of the existing fast-forward button.
|
||||
- Persisting state for brand-new conversations that were never created/restored through the normal session restoration flow.
|
||||
- Syncing this preference across machines or through server-side conversation storage.
|
||||
|
||||
## Figma
|
||||
|
||||
Figma: none provided
|
||||
|
||||
## User Experience
|
||||
|
||||
### Core behavior
|
||||
|
||||
- Fast-forward state is remembered per agent conversation.
|
||||
- Session restoration is the source of truth for when this behavior applies. If Warp restores a conversation on launch, the restored conversation should use the same fast-forward state it had when the app last saved session state.
|
||||
- The remembered state should apply whether the restored conversation is shown in the standard terminal/blocklist UI or reopened into fullscreen agent view.
|
||||
|
||||
### State rules
|
||||
|
||||
- If fast forward was on for a conversation before quit, it is on after launch.
|
||||
- If fast forward was off for a conversation before quit, it is off after launch.
|
||||
- Different restored conversations can come back with different fast-forward states.
|
||||
- New conversations that do not yet have persisted state continue to use the current default behavior until the user explicitly toggles fast forward.
|
||||
|
||||
### Timing expectations
|
||||
|
||||
- If the user toggles fast forward and then quits or restarts Warp without taking another conversation action, the newly selected state should still be restored.
|
||||
- The user should not need to send another message, receive another response, or otherwise advance the conversation for the new fast-forward state to stick.
|
||||
|
||||
### Feature flag behavior
|
||||
|
||||
- The behavior is gated by a dedicated feature flag for remembering fast-forward state across sessions.
|
||||
- That flag is enabled by default for dogfood builds.
|
||||
- If the flag is disabled, restored conversations fall back to current behavior instead of honoring the persisted fast-forward override.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. A restored conversation that had fast forward enabled before app restart still shows fast forward enabled after restore.
|
||||
2. A restored conversation that had fast forward disabled before app restart still shows fast forward disabled after restore.
|
||||
3. Two restored conversations with different pre-restart fast-forward states each restore their own state correctly.
|
||||
4. Toggling fast forward immediately before restart is enough to preserve the new state.
|
||||
5. Restoring a conversation into fullscreen agent view preserves the same fast-forward state.
|
||||
6. Conversations persisted before this feature ships continue to restore safely and default to the existing non-fast-forward behavior.
|
||||
7. Disabling the remember-state feature flag restores the current behavior without breaking conversation restoration.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests covering persistence and restoration of the per-conversation fast-forward state.
|
||||
- A restoration-focused test showing that a persisted conversation round-trips through SQLite-backed restore with the expected fast-forward mode.
|
||||
- Manual verification:
|
||||
- enable fast forward, restart Warp, confirm it stays enabled
|
||||
- disable fast forward, restart Warp, confirm it stays disabled
|
||||
- repeat with multiple restored conversations and fullscreen agent view
|
||||
|
||||
## Open Questions
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,200 @@
|
||||
# Restore Fast Forward State Across Sessions — Tech Spec
|
||||
|
||||
Product spec: `specs/zachlloyd/restore-fast-forward-state/PRODUCT.md`
|
||||
|
||||
## Problem
|
||||
|
||||
Warp already restores agent conversations across app restarts, but it does not persist the conversation's `autoexecute_override` state. The fast-forward button therefore only affects the in-memory session: after restart, the conversation is restored, but its fast-forward state falls back to the default instead of the user's last choice.
|
||||
|
||||
This feature should use the existing SQLite-backed session restoration flow rather than adding a separate persistence mechanism.
|
||||
|
||||
## Relevant code
|
||||
|
||||
- `app/src/ai/blocklist/block/view_impl/common.rs:723` — renders the fast-forward button and dispatches `TerminalAction::ToggleAutoexecuteMode`.
|
||||
- `app/src/terminal/view.rs:23933` — handles `ToggleAutoexecuteMode`, accepts any pending blocked diff, and forwards the toggle to the AI context model.
|
||||
- `app/src/ai/blocklist/context_model.rs:801` — resolves the effective autoexecute mode for the pending query / selected conversation.
|
||||
- `app/src/ai/blocklist/context_model.rs:818` — toggles the active or follow-up conversation's autoexecute mode.
|
||||
- `app/src/ai/blocklist/history_model.rs:1654` — mutates the conversation-level autoexecute override.
|
||||
- `app/src/ai/agent/conversation.rs:174` — `AIConversation` stores `autoexecute_override`.
|
||||
- `app/src/ai/agent/conversation.rs:2621` — `write_updated_conversation_state` persists conversation metadata to SQLite.
|
||||
- `app/src/ai/agent/conversation.rs:232` — `AIConversation::new_restored(...)` reconstructs persisted conversations during restore.
|
||||
- `app/src/ai/agent/conversation.rs:3480` — `AIConversationAutoexecuteMode`.
|
||||
- `crates/persistence/src/model.rs:969` — `AgentConversationData`, the JSON payload stored in `agent_conversations.conversation_data`.
|
||||
- `app/src/persistence/mod.rs:316` — `ModelEvent::UpdateMultiAgentConversation`.
|
||||
- `app/src/persistence/sqlite.rs:653` — SQLite writer dispatch for conversation upserts.
|
||||
- `app/src/persistence/agent.rs` — writes and reads `agent_conversations.conversation_data`.
|
||||
- `app/src/pane_group/pane/terminal_pane.rs:359` — session snapshots already record `conversation_ids_to_restore` and `active_conversation_id`.
|
||||
- `app/src/app_state.rs:180` — `TerminalPaneSnapshot` stores the conversation IDs used for restore.
|
||||
- `app/src/persistence/sqlite.rs:2367` — terminal pane restore reads those IDs back from SQLite.
|
||||
- `app/src/ai/restored_conversations.rs:22` — restored conversations are staged globally on startup.
|
||||
- `app/src/ai/blocklist/history_model/conversation_loader.rs:59` — persisted conversations are converted back into `AIConversation`.
|
||||
- `app/src/terminal/view/load_ai_conversation.rs:91` — startup restoration path loads restored conversations into terminal views.
|
||||
- `crates/warp_features/src/lib.rs:337` — existing `FastForwardAutoexecuteButton` flag.
|
||||
- `crates/warp_features/src/lib.rs (806-855)` — `DOGFOOD_FLAGS` and other rollout lists.
|
||||
|
||||
## Current state
|
||||
|
||||
The fast-forward button is already conversation-scoped in memory:
|
||||
|
||||
1. The UI dispatches `ToggleAutoexecuteMode`.
|
||||
2. `TerminalView` forwards the action to `BlocklistAIContextModel`.
|
||||
3. The context model toggles the selected conversation through `BlocklistAIHistoryModel`.
|
||||
4. `BlocklistAIHistoryModel::toggle_autoexecute_override(...)` mutates the `AIConversation`.
|
||||
|
||||
Session restoration already persists and restores the conversation itself:
|
||||
|
||||
- terminal pane snapshots store which conversation IDs should be restored
|
||||
- app startup loads persisted `multi_agent_conversations` from SQLite
|
||||
- those rows are converted back into `AIConversation`
|
||||
- terminal views reattach the restored conversations during startup
|
||||
|
||||
The gap is that `AgentConversationData` does not currently include `autoexecute_override`, and `AIConversation::new_restored(...)` always reconstructs the conversation with `autoexecute_override: Default::default()`. As a result, restored conversations lose fast-forward state even though the rest of the conversation state survives.
|
||||
|
||||
There is a second gap: the toggle path only mutates in-memory state and emits `UpdatedAutoexecuteOverride`; it does not currently force an immediate `write_updated_conversation_state(...)`. Even if we add the field to persistence, a user could still lose the change if they toggle fast forward and restart before some other persistence-triggering conversation update occurs.
|
||||
|
||||
## Proposed changes
|
||||
|
||||
### 1. Add a dedicated feature flag
|
||||
|
||||
Add a new runtime feature flag in `crates/warp_features/src/lib.rs` for remembering fast-forward state across restored sessions, separate from `FeatureFlag::FastForwardAutoexecuteButton`.
|
||||
|
||||
Proposed name: `FeatureFlag::RememberFastForwardState`
|
||||
|
||||
Why separate it:
|
||||
|
||||
- `FastForwardAutoexecuteButton` controls whether the button exists
|
||||
- this feature controls whether local session restoration remembers the per-conversation override
|
||||
- separating them lets us roll out persistence independently from the button itself
|
||||
|
||||
Add the new flag to `DOGFOOD_FLAGS`, but not to preview or release by default.
|
||||
|
||||
### 2. Persist the conversation-level override in `AgentConversationData`
|
||||
|
||||
Extend `crates/persistence/src/model.rs` so `AgentConversationData` includes a new optional field for the persisted fast-forward mode.
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- add a persistence-owned enum or equivalent serialized field representing:
|
||||
- `RespectUserSettings`
|
||||
- `RunToCompletion`
|
||||
- annotate it with serde defaults so older rows that do not contain the field deserialize successfully
|
||||
|
||||
This should live in the persistence model layer instead of directly serializing `AIConversationAutoexecuteMode`, because `AgentConversationData` lives outside the app crate and already acts as the stable JSON contract for `agent_conversations.conversation_data`.
|
||||
|
||||
### 3. Write the new field from `AIConversation::write_updated_conversation_state`
|
||||
|
||||
Update `app/src/ai/agent/conversation.rs` so `write_updated_conversation_state(...)` includes the current `autoexecute_override` in the `AgentConversationData` payload sent via `ModelEvent::UpdateMultiAgentConversation`.
|
||||
|
||||
This keeps the new state on the same persistence path as the rest of the conversation metadata and does not require any schema migration, because the JSON payload is already opaque text in SQLite.
|
||||
|
||||
### 4. Restore the field in `AIConversation::new_restored`
|
||||
|
||||
Update `AIConversation::new_restored(...)` to map the persisted field back into `AIConversationAutoexecuteMode`.
|
||||
|
||||
Backward-compatibility rules:
|
||||
|
||||
- missing field => `AIConversationAutoexecuteMode::RespectUserSettings`
|
||||
- malformed / unknown value => log and fall back to `RespectUserSettings`
|
||||
|
||||
Feature-flag behavior:
|
||||
|
||||
- if `FeatureFlag::RememberFastForwardState` is enabled, honor the persisted value
|
||||
- if it is disabled, continue restoring `RespectUserSettings` regardless of any stored value
|
||||
|
||||
Gating restore-time behavior instead of the underlying storage keeps the change easy to roll forward/back while remaining compatible with any rows written during dogfood.
|
||||
|
||||
### 5. Persist immediately when the user toggles fast forward
|
||||
|
||||
Update `BlocklistAIHistoryModel::toggle_autoexecute_override(...)` so that after mutating the `AIConversation`, it also calls `conversation.write_updated_conversation_state(ctx)` before emitting `UpdatedAutoexecuteOverride`.
|
||||
|
||||
That ensures this sequence works correctly:
|
||||
|
||||
1. user toggles fast forward
|
||||
2. Warp saves the updated conversation JSON immediately
|
||||
3. user quits or restarts before any other agent event
|
||||
4. session restoration reloads the new fast-forward state
|
||||
|
||||
This is the minimum change needed to make the remembered state reliable.
|
||||
|
||||
### 6. Reuse the existing session restoration pipeline unchanged
|
||||
|
||||
No terminal pane snapshot schema change is required.
|
||||
|
||||
The existing pipeline already stores:
|
||||
|
||||
- which conversations belong to each terminal pane
|
||||
- which conversation should be active when fullscreen agent view is restored
|
||||
|
||||
Once the restored `AIConversation` carries the right `autoexecute_override`, the existing UI state resolution path (`pending_query_autoexecute_override`, active conversation restore, fullscreen agent view restore) will naturally render the correct fast-forward state.
|
||||
|
||||
## End-to-end flow
|
||||
|
||||
1. User clicks the fast-forward button.
|
||||
2. `TerminalAction::ToggleAutoexecuteMode` routes the action to the selected conversation.
|
||||
3. `BlocklistAIHistoryModel` toggles the conversation's `autoexecute_override`.
|
||||
4. The updated conversation is immediately written to SQLite through `UpdateMultiAgentConversation`.
|
||||
5. On the next app launch, terminal pane restoration requests the saved conversation IDs.
|
||||
6. Persisted `AgentConversationData` is converted back into `AIConversation`, including the remembered autoexecute mode.
|
||||
7. The restored terminal / agent view reads that conversation state and shows the fast-forward button in the correct state.
|
||||
|
||||
## Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[User toggles fast forward] --> B[TerminalAction::ToggleAutoexecuteMode]
|
||||
B --> C[BlocklistAIContextModel]
|
||||
C --> D[BlocklistAIHistoryModel::toggle_autoexecute_override]
|
||||
D --> E[AIConversation.autoexecute_override]
|
||||
E --> F[write_updated_conversation_state]
|
||||
F --> G[(agent_conversations.conversation_data)]
|
||||
G --> H[Startup session restoration]
|
||||
H --> I[AIConversation::new_restored]
|
||||
I --> J[Restored conversation UI / agent view]
|
||||
```
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
### Backward compatibility with existing persisted rows
|
||||
|
||||
Older rows will not contain the new field. Using a serde-defaulted optional field and defaulting missing values to `RespectUserSettings` avoids migration risk.
|
||||
|
||||
### Restore behavior diverging from flag rollout
|
||||
|
||||
If we reused `FastForwardAutoexecuteButton`, we would couple persistence rollout to button visibility. A separate flag keeps rollout narrow and reversible.
|
||||
|
||||
### Persisted value written but not flushed soon enough
|
||||
|
||||
If persistence only happens on later conversation updates, the user could still lose the toggle on restart. Writing immediately from the toggle path closes that gap.
|
||||
|
||||
### Over-specifying pane restoration
|
||||
|
||||
This feature does not need a new pane or window snapshot field. Reusing the existing conversation restoration mechanism keeps the scope small and avoids duplicate sources of truth.
|
||||
|
||||
## Testing and validation
|
||||
|
||||
### Persistence model tests
|
||||
|
||||
- Add tests for `AgentConversationData` serialization/deserialization with the new field.
|
||||
- Add a backward-compatibility test showing that JSON without the field still deserializes and restores `RespectUserSettings`.
|
||||
|
||||
### Conversation restoration tests
|
||||
|
||||
- Add a test for `AIConversation::new_restored(...)` or `convert_persisted_conversation_to_ai_conversation_with_metadata(...)` showing that a persisted `RunToCompletion` override round-trips into the restored `AIConversation`.
|
||||
- Add a test showing that the feature flag disabled path ignores the persisted override and restores the default mode.
|
||||
|
||||
### Toggle persistence tests
|
||||
|
||||
- Add a test around `BlocklistAIHistoryModel::toggle_autoexecute_override(...)` proving that toggling emits a persistence update containing the new override value.
|
||||
- If there is already a SQLite writer test pattern for `UpdateMultiAgentConversation`, extend it to assert the new JSON field is written.
|
||||
|
||||
### Manual verification
|
||||
|
||||
- Start an agent conversation, enable fast forward, restart Warp, verify the restored conversation still shows fast forward enabled.
|
||||
- Repeat with fast forward disabled.
|
||||
- Repeat after toggling immediately before quit.
|
||||
- Repeat with fullscreen agent view restored.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- If we later want this behavior to survive beyond local session restoration, we can decide separately whether the server-side conversation model should also carry the override.
|
||||
- If the feature launches broadly, we can later remove the dedicated remember-state flag once rollout is complete.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Wide Markdown Table Scrolling — Product Spec
|
||||
Linear: none provided
|
||||
Figma: none provided
|
||||
Related specs:
|
||||
- `specs/zachlloyd/markdown-table-consistency/PRODUCT.md`
|
||||
- `specs/APP-3076/PRODUCT.md`
|
||||
|
||||
## Summary
|
||||
Make rendered Markdown tables scroll horizontally inside their own viewport when they are wider than the surrounding surface, instead of forcing the entire notebook editor or AI block list to scroll horizontally. Selection, cursor, copy, and keyboard navigation continue to work correctly while a table is horizontally scrolled.
|
||||
|
||||
## Problem
|
||||
Warp already renders Markdown tables, but wide-table overflow behavior is not consistently defined across the product. Editor-backed notebook tables render in document flow with no table-local horizontal scroll, so wide tables push their containing surface wider. AI block list structured tables do scroll horizontally, but the contract — and the expected selection behavior while scrolled — is not written down. The result is that wide tables are hard to read and selection/cursor behavior while scrolling is easy to regress silently.
|
||||
|
||||
## Behavior
|
||||
|
||||
### Scope
|
||||
1. This feature applies to rendered Markdown tables in notebook/editor-backed Markdown surfaces and in AI block list table sections created from Markdown table output. Other Warp surfaces that inherit the same editor-backed Markdown table renderer pick up the behavior automatically, but notebook/editor and AI block list are the required launch surfaces.
|
||||
|
||||
### Overflow rule
|
||||
2. When a rendered Markdown table fits within the available content width, it renders normally with no horizontal scrolling behavior. Nothing changes for tables that already fit.
|
||||
3. When a rendered Markdown table is wider than the available content width:
|
||||
- The table remains in normal document flow.
|
||||
- The visible table region is constrained to the surface width available to that table.
|
||||
- The user can horizontally scroll the table to reveal off-screen columns.
|
||||
- The table exposes a standard horizontal scrolling affordance (scrollbar) when overflow exists.
|
||||
- The rest of the notebook or block list does not need to scroll horizontally to access the wide table.
|
||||
|
||||
### Horizontally scrollable container exception
|
||||
4. When the surrounding surface itself provides horizontal scrolling over the full content area (for example, a code editor that lays out all content at its intrinsic width), wide Markdown tables must not introduce their own table-local horizontal scrolling:
|
||||
- The table renders at its full intrinsic width.
|
||||
- No table-local horizontal scrollbar, thumb, or overflow affordance is shown.
|
||||
- Horizontal scrolling gestures over the table fall through to the surrounding editor.
|
||||
- Horizontal overflow is reached entirely through the surrounding editor's existing horizontal scroll.
|
||||
|
||||
This mirrors the vertical scrolling rule: the feature only introduces a nested scrolling axis when the surrounding surface does not already own that axis.
|
||||
|
||||
### Vertical scrolling
|
||||
5. Wide-table support must not introduce nested vertical scrolling. The notebook or surrounding editor continues to own vertical scrolling in editor contexts, the AI block list continues to own vertical scrolling in block list contexts, and the table itself only introduces horizontal overflow behavior.
|
||||
|
||||
### Readability while scrolled
|
||||
6. When horizontally scrolled, cells remain fully rendered — no ellipses, truncation, or degraded rendering solely because content is off-screen. Header and body alignment remain correct. Inline Markdown formatting inside cells continues to render correctly. The table keeps the same styling treatment it would have without overflow.
|
||||
|
||||
### Per-cell maximum content width
|
||||
7. Individual table cells have a maximum content width. A cell whose content fits within the maximum sizes to its content as it does today. A cell whose content exceeds the maximum is laid out at the maximum width and soft-wraps onto additional lines. Alignment, inline Markdown formatting, and header/body styling continue to apply to the wrapped content.
|
||||
8. The per-cell maximum width applies regardless of whether the surrounding surface owns horizontal scrolling — it is a readability cap on intrinsic cell width, not a scroll affordance. One long cell never dominates the intrinsic width of the entire table.
|
||||
|
||||
### Mouse, trackpad, and scrollbar
|
||||
9. Horizontal scrolling gestures over an overflowing table move the table horizontally. Direct interaction with the horizontal scrollbar (drag, click in track) moves the table horizontally. Normal document-level vertical scrolling continues to work for the surrounding notebook or block list.
|
||||
10. Mixed-axis trackpad gestures resolve cleanly to either local horizontal table scrolling or surrounding vertical document scrolling, without visible vertical jitter while the table is being panned horizontally.
|
||||
11. When the table is already pinned at its horizontal scroll edge and the user generates a horizontal wheel delta that would push further in that direction, the event propagates to the surrounding scroller so the user can continue scrolling the document instead of getting stuck at the table edge.
|
||||
12. The `MouseMoved` event is not consumed by the table scrollbar thumb: even while the pointer hovers over the scrollbar thumb, downstream handlers (hover-link detection, cursor changes) continue to receive the event.
|
||||
|
||||
### Selection while horizontally scrolled
|
||||
13. Clicking inside a horizontally scrolled table places the caret or starts the selection at the correct cell content. Double-clicking a visible word selects the word in the hovered cell — not text from another column in the same row. Dragging across visible table content selects the correct text after the scroll offset is applied.
|
||||
14. An existing selection remains stable when the user horizontally scrolls the table: the visible highlight moves with the selected content rather than staying pinned to stale viewport coordinates. Selection highlight rectangles stay visually aligned with the selected text while scrolled.
|
||||
15. Copying a selection copies the selected rendered text, not hidden text outside the selection and not the raw Markdown source. Copying a partial selection copies exactly the selected visible text — it does not convert into a whole-table copy.
|
||||
16. In block list contexts, selection and copy behavior apply to read-only text selection. In editor contexts, they apply to both selection and caret behavior.
|
||||
|
||||
### Editable notebook behavior
|
||||
17. In editable notebook/editor contexts, existing editing behavior continues to work inside wide tables. The user can still place the caret inside a cell, drag to select text within and across cells, move the caret with the keyboard, and type / delete / paste / otherwise edit table cell content. Horizontal overflow does not make editable tables feel like read-only snapshots.
|
||||
|
||||
### Caret visibility during keyboard movement
|
||||
18. In editable contexts, when keyboard navigation or editing moves the active caret or active selection into an off-screen part of an overflowing table, the table horizontally scrolls just enough to keep the active caret or selection visible. Reveal happens at the table level, not by scrolling the whole surface. This applies to arrow-key movement, Shift+arrow extension, and other common navigation and editing flows.
|
||||
|
||||
### Scroll-state stability
|
||||
19. While the notebook or block list remains open, ordinary rerenders do not reset a table's horizontal scroll position.
|
||||
20. If the user has an active selection inside a horizontally scrolled table, rerenders while that selection remains active preserve the visual alignment between the highlight and the selected content.
|
||||
21. If the table content or layout changes so that overflow no longer exists, the table resets back to its leftmost position and no horizontal overflow affordance remains.
|
||||
|
||||
### Narrow tables and non-table content
|
||||
22. This feature does not change the behavior of:
|
||||
- Tables that already fit within the available width.
|
||||
- Non-table Markdown blocks.
|
||||
- Malformed table-like content that falls back to non-table rendering today.
|
||||
@@ -0,0 +1,188 @@
|
||||
# Wide Markdown Table Scrolling — Tech Spec
|
||||
Product spec: `specs/zachlloyd/wide-markdown-table-scrolling/PRODUCT.md`
|
||||
|
||||
## Context
|
||||
The branch implements the behavior described in the sibling `PRODUCT.md`: wide Markdown tables scroll horizontally inside their own viewport, and selection / caret / hit-testing / copy stay attached to content while the table or any clipped surface is scrolled.
|
||||
|
||||
Investigation against `crates/markdown_parser/examples/table-demo/all_test_cases.md` surfaced two classes of bugs that both needed to be addressed:
|
||||
|
||||
- Editor-backed tables did not yet own a horizontal viewport, so wide tables simply overflowed the surrounding editor.
|
||||
- Once a cell contained inline Markdown (`**Bold**`, `*Italic*`, links), source offsets and rendered offsets were mixed in selection and copy paths, producing drift even before the wide-table work.
|
||||
|
||||
Separately, the shared clipped-scrollable path could re-use stale screen-space coordinates after a scroll, which showed up as wrong word selection and wrong copied text.
|
||||
|
||||
### Relevant code
|
||||
- `crates/editor/src/render/model/mod.rs` — `LaidOutTable` (horizontal scroll state, reveal logic, character bounds) and `RenderState::autoscroll` (table-aware reveal)
|
||||
- `crates/editor/src/render/element/table.rs` — `RenderableTable` painting, clipping, and event dispatch for scroll/drag/hover
|
||||
- `crates/editor/src/render/model/location.rs` — hit-testing that accounts for local table scroll
|
||||
- `crates/editor/src/render/model/table_offset_map.rs` — `TableOffsetMap` (table-wide) and `TableCellOffsetMap` (per-cell source↔rendered)
|
||||
- `crates/editor/src/render/layout.rs` — `TextLayout::with_container_scrolls_horizontally` carries the carve-out flag
|
||||
- `crates/editor/src/content/text.rs` — `BufferBlockStyle::Table` variant, `TableCache` / `TableBlockCache`, and `table_cell_offset_maps`
|
||||
- `crates/editor/src/content/edit.rs` — `layout_table_block`, `measure_table_cells`, per-cell size clamp
|
||||
- `crates/editor/src/content/buffer.rs` — table-aware clipboard text extraction and partial-table HTML filtering
|
||||
- `crates/warpui_core/src/elements/shared_scrollbar.rs` — shared `ScrollbarAppearance` / `ScrollbarGeometry` and scrollbar math
|
||||
- `crates/warpui_core/src/elements/clipped_scrollable.rs` — selection anchor (`anchor_and_adjust_selection_for_scroll`)
|
||||
- `crates/warpui_core/src/elements/new_scrollable/mod.rs` — dispatches selection APIs through the anchor helper
|
||||
- `crates/warpui_core/src/elements/formatted_text_element.rs` — horizontal bounds check for smart selection
|
||||
- `app/src/notebooks/editor/mod.rs` — notebook table appearance (scrollbar colors, etc.)
|
||||
|
||||
## Proposed changes
|
||||
|
||||
### Shared scrollbar primitives in `warpui_core`
|
||||
`crates/warpui_core/src/elements/shared_scrollbar.rs` is the single source of truth for:
|
||||
- `ScrollbarAppearance` / `ScrollbarGeometry` (overlay scrollbar geometry, thumb bounds, track bounds)
|
||||
- Minimum thumb sizing (`MIN_SCROLLBAR_THUMB_LENGTH`)
|
||||
- `compute_scrollbar_geometry(...)`
|
||||
- `project_scroll_delta_by_sensitivity(...)` for axis projection of mixed-axis gestures
|
||||
- `scroll_delta_for_pointer_movement(...)` for pointer-drag-to-scroll translation
|
||||
|
||||
Both the editor table renderer and the existing `new_scrollable` utilities call through these helpers. The goal is not to nest a `NewScrollable` inside tables but to share scrollbar math and mixed-axis resolution.
|
||||
|
||||
### Editor tables own a local horizontal viewport
|
||||
`LaidOutTable` in `crates/editor/src/render/model/mod.rs` gains local horizontal scroll state:
|
||||
- `scroll_left: Cell<Pixels>`
|
||||
- `TableScrollbarInteractionState` (drag state, hovered)
|
||||
- `viewport_width()`, `max_scroll_left()`, `set_scroll_left()`, `scroll_horizontally()`
|
||||
- `reveal_offset()` for keyboard/caret autoscroll
|
||||
- `horizontal_scroll_allowed` flag (see carve-out below)
|
||||
|
||||
`RenderableTable::paint` in `crates/editor/src/render/element/table.rs`:
|
||||
- Computes a clipped viewport rectangle for the visible region.
|
||||
- Translates painted content by `scroll_left`.
|
||||
- Paints inside a clipped layer.
|
||||
- Derives the thumb via `compute_scrollbar_geometry()` and paints it using notebook theme colors from `MarkdownTableAppearance` / `TableStyle`.
|
||||
|
||||
`RenderableTable::dispatch_event` handles `LeftMouseDown` (thumb + gutter), `LeftMouseDragged`, `LeftMouseUp`, `MouseMoved`, and `ScrollWheel`. Two event-propagation rules worth calling out:
|
||||
- The `ScrollWheel` handler returns the boolean result of `scroll_horizontally()` rather than unconditionally consuming the event. When the table is pinned at an edge, the event falls through to the surrounding vertical scroller (PRODUCT invariant 11).
|
||||
- The `MouseMoved` handler returns `false` regardless of whether the pointer is over the thumb; scrollbar hover state is still updated via `set_scrollbar_hovered()` and `ctx.notify()`, but the event continues to propagate to downstream handlers (PRODUCT invariant 12).
|
||||
|
||||
### Horizontally scrollable container carve-out
|
||||
Table-local horizontal scrolling only applies when the surrounding surface doesn't own horizontal scroll. In code editors using `WidthSetting::InfiniteWidth`, wide tables render at full intrinsic width:
|
||||
- `TextLayout` carries a `container_scrolls_horizontally` flag, set via `TextLayout::with_container_scrolls_horizontally(...)`.
|
||||
- `RenderState::container_scrolls_horizontally()` returns `true` when `width_setting == WidthSetting::InfiniteWidth`; `RenderState::layout_context()` and `TextLayout::from_layout_context(...)` propagate the flag.
|
||||
- `layout_table_block()` reads the flag and sets `horizontal_scroll_allowed` on the resulting `LaidOutTable` to its inverse.
|
||||
- When `horizontal_scroll_allowed == false`: `viewport_width()` returns the full table width, `max_scroll_left()` returns zero, and `scroll_left()` / `scroll_horizontally()` / `reveal_offset()` / `set_scroll_left()` become no-ops. Paint widens the clip layer to the full table, `table_scrollbar()` returns `None`, and scrollbar drag/hover handling short-circuits. Wheel events fall through to the surrounding editor.
|
||||
|
||||
AI block list contexts keep the default `false`, preserving existing behavior.
|
||||
|
||||
### Per-cell source↔rendered offset mapping
|
||||
`TableCellOffsetMap` in `crates/editor/src/render/model/table_offset_map.rs` tracks source↔rendered ranges per inline fragment. For each fragment it records rendered start/end, visible source start/end, and source end including Markdown markers. It supports `rendered_length()`, `source_length()`, `rendered_to_source()`, and `source_to_rendered()`.
|
||||
|
||||
`TableCellOffsetMap::from_inline_and_source(source, inline)` derives spans by walking the raw cell `source` character-by-character alongside each fragment's rendered text, rather than reconstructing marker lengths from style flags. For each fragment:
|
||||
- Advance through source until the fragment's first rendered character is found. Intervening source characters are attributed to this fragment as markers.
|
||||
- Consume the remaining rendered characters from source, treating `\<punct>` as a single-character escape (two source chars → one rendered char).
|
||||
|
||||
This replaces an earlier hardcoded `fragment_source_marker_lengths` helper and therefore:
|
||||
- Handles backslash-escaped punctuation correctly (previously silently drifted).
|
||||
- Stays correct if the Markdown parser ever changes marker syntax, because we walk actual source rather than regenerating it.
|
||||
- Handles nested styles where adjacent fragments share outer markers (e.g. `**a *b* c**`), attributing each marker once.
|
||||
|
||||
`content::text::table_cell_offset_maps(table, source)` receives the parsed `FormattedTable` and the raw tab/newline-separated source, splits the source into rows/cells, and passes each cell's source through the builder. Synthetic cells added by `normalize_shape` get an empty source string and produce empty maps.
|
||||
|
||||
An in-code `TODO` above `impl TableCellOffsetMap` notes that moving cell/row boundaries into the `SumTree` with new `BufferText` marker types would let editable tables derive per-cell offsets by seeking to boundaries rather than reparsing. That refactor is deferred to the editable-tables workstream.
|
||||
|
||||
### Table layout is source-based, rendering stays rendered-text-based
|
||||
`layout_table_block()` in `crates/editor/src/content/edit.rs`:
|
||||
- Parses the table into formatted inline fragments (via the cache below when available).
|
||||
- Builds `TableCellOffsetMap`s for every cell.
|
||||
- Builds the table-wide `TableOffsetMap` from cell **source** lengths.
|
||||
- Preserves `content_length` from the original text block source.
|
||||
|
||||
`LaidOutTable` stores the per-cell maps and uses them whenever a source offset touches rendered geometry (coordinate→offset conversion, link lookup, relative character bounds, selection highlight ranges, cursor placement).
|
||||
|
||||
The rule: editor-facing APIs stay source-based; text layout, frame widths, and painting stay rendered-text-based; conversions happen explicitly at the cell boundary.
|
||||
|
||||
### Hit-testing, selection, and caret geometry
|
||||
- `crates/editor/src/render/model/location.rs` adds `scroll_left` before calling `coordinate_to_offset()`.
|
||||
- `LaidOutTable::character_bounds()` subtracts `scroll_left` when returning screen-space bounds.
|
||||
- `RenderState::autoscroll()` calls `reveal_autoscroll_offsets_in_tables()` so keyboard movement horizontally reveals the active caret/selection inside the table.
|
||||
- `softwrap_point_to_offset()` for tables resolves to the first visible character in the row instead of using raw row starts.
|
||||
- `crates/editor/src/render/element/mod.rs` hit-testing uses the editor element's own layer bounds rather than the clipped child layer bounds, so clicks inside a wide table are not rejected before block-level hit-testing runs.
|
||||
|
||||
### Per-cell maximum content width
|
||||
`crates/editor/src/content/edit.rs` introduces:
|
||||
- `MAX_TABLE_CELL_CONTENT_WIDTH_PX` = 500.0 — maximum content width (exclusive of cell padding).
|
||||
- `maximum_table_cell_width(table_style)` helper, mirroring `minimum_table_cell_width(..)`.
|
||||
|
||||
In `measure_table_cells`, per-cell measured widths are clamped with both `.max(minimum_table_cell_width(..))` and `.min(maximum_table_cell_width(..))` before they fold into the shared column width. The second layout pass in `layout_table_block` lays each cell at `cell_content_width = column_width - cell_padding * 2.0`, and the existing text layout soft-wraps the clamped cells without further changes.
|
||||
|
||||
The cap is applied unconditionally — including when `horizontal_scroll_allowed == false` — so infinite-width containers still get readability benefits. Single very long unbreakable tokens (e.g. long URLs) may still render wider than the cap because soft-wrap can't break them; the column width stays clamped and the token visually overflows inside the clipped table region.
|
||||
|
||||
### Clipboard behavior is table-aware
|
||||
`crates/editor/src/content/buffer.rs`:
|
||||
- Plain-text copy walks selected block segments and routes table segments through `clipboard_table_text_in_range()`.
|
||||
- Table copy rebuilds the formatted table (from cache when available), computes source-based cell ranges, converts the selected source span in each cell to rendered offsets, and slices visible cell text accordingly.
|
||||
- Partial table selections return rendered plain text with tabs/newlines preserved.
|
||||
- HTML export in `selected_text_as_html` filters only the ranges that contain a partial table selection, serializing the remaining clean ranges to HTML normally. Only when every range is a partial-table range does it return `None`.
|
||||
|
||||
### Clipped scrollables keep selections anchored to content
|
||||
`crates/warpui_core/src/elements/clipped_scrollable.rs`:
|
||||
- `ClippedScrollStateHandle` stores a `selection_scroll_anchor` (original selection + scroll position at the time it was observed).
|
||||
- `anchor_and_adjust_selection_for_scroll(selection, axis)` either records the anchor (first time) or shifts the selection by the delta between current scroll and the anchored scroll. Doc comment spells out the three branches (None clears anchor; unmatched Selection installs a new anchor; matched Selection returns a scroll-compensated copy).
|
||||
- `clear_selection_scroll_anchor()` resets that state when a fresh mouse-down starts a new interaction.
|
||||
|
||||
`NewScrollable` routes selection APIs through the anchor helper during paint, in `get_selection()`, and in `calculate_clickable_bounds()`. New mouse-down clears the anchor.
|
||||
|
||||
### Smart selection respects visible horizontal bounds
|
||||
`FormattedTextElement::smart_select()` returns `None` when the click point is outside the visible horizontal bounds of the text frame (not just outside vertical bounds). This prevents double-click word selection from targeting text that is off-screen after horizontal scrolling.
|
||||
|
||||
### Lazy cache on the `BufferBlockStyle::Table` marker
|
||||
Both the clipboard copy path and the layout path previously parsed each table block on every invocation. That parse is deterministic from the block's plain text plus `alignments`, so it is lifted onto the marker:
|
||||
|
||||
- `crates/editor/src/content/text.rs` introduces `TableBlockCache` (owns parsed `FormattedTable`, `Vec<Vec<TableCellOffsetMap>>`, and `TableOffsetMap`) and a `TableCache` newtype wrapping `Arc<OnceLock<TableBlockCache>>`.
|
||||
- `BufferBlockStyle::Table` gains a `cache: TableCache` field. A `BufferBlockStyle::table(alignments)` helper constructs the variant with an empty cache.
|
||||
- `TableCache` implements `PartialEq` / `Eq` / `Hash` as no-ops so `BufferBlockStyle` equality and hashing remain a function of `alignments`. `Clone` stays cheap because the field is a shared `Arc`.
|
||||
- `TableCache::get_or_populate(text, alignments)` runs the parse once and returns `&TableBlockCache` on subsequent calls. Marker clones share the same `OnceLock`.
|
||||
- `clipboard_table_text_in_range` and `layout_table_block` both route through the cache. A defensive fallback in `layout_table_block` builds an owned cache on the stack when the style isn't `Table`.
|
||||
|
||||
The staleness trade-off is the same one that already applies to `alignments`: if a cell were edited in place without replacing the block marker, the cache could go stale. This matters only when editable tables land; for read-only tables, marker replacement on edit is sufficient.
|
||||
|
||||
## Testing and validation
|
||||
|
||||
Each numbered invariant in `PRODUCT.md` maps to at least one test or verification step below.
|
||||
|
||||
### Automated tests
|
||||
- PRODUCT invariants 2–3 (overflow rule), 9 (scrollbar interaction): `render::element::table::tests::table_scrollbar_uses_shared_overlay_geometry`, `table_scrollbar_drag_state_survives_renderable_recreation`, `table_scrollbar_pointer_movement_matches_drag_and_gutter_behavior`.
|
||||
- PRODUCT invariant 4 (container carve-out): exercised via `horizontal_scroll_allowed == false` code paths in model tests; manual pass in a code editor surface configured with `WidthSetting::InfiniteWidth`.
|
||||
- PRODUCT invariants 7–8 (per-cell max width): `content::edit::tests::test_layout_table_block_clamps_cell_width_to_max`.
|
||||
- PRODUCT invariant 11 (wheel edge propagation): covered by the `ScrollWheel` handler returning `scroll_horizontally()`'s boolean; manual trackpad pass confirms fall-through.
|
||||
- PRODUCT invariant 12 (`MouseMoved` not consumed): covered by the handler returning `false`; manual hover-link check confirms downstream handlers still fire.
|
||||
- PRODUCT invariants 13–15 (selection and copy while scrolled): `content::buffer::tests::test_selected_table_copy_uses_visible_plain_text`, `test_partial_table_selection_does_not_export_html`, `test_partial_table_selection_still_exports_html_for_non_table_ranges`, `test_clipboard_table_copy_uses_source_offsets_for_later_formatted_cells`.
|
||||
- PRODUCT invariants 13–14 (selection anchor stability): `warpui_core::elements::new_scrollable::scrollable_test` regressions for viewport-coordinate selection APIs, re-anchoring existing selections across horizontal scroll, and clearing the anchor on new mouse-down interactions.
|
||||
- PRODUCT invariant 13 (double-click word selection across columns): `smart_select_returns_none_when_point_is_outside_horizontal_bounds` (formatted_text_element_tests).
|
||||
- PRODUCT invariant 18 (caret reveal during keyboard movement): `render::model::location_tests` (hit-testing after horizontal scroll) and `render::model::mod_tests` (reveal/autoscroll for offsets inside tables).
|
||||
- PRODUCT invariants 6 and 13–15 as applied to formatted cells (inline Markdown correctness): `render::model::table_offset_map::tests::test_table_cell_offset_map_handles_bold_and_links`, `test_table_cell_offset_map_handles_backslash_escaped_punctuation`, `test_table_cell_offset_map_handles_nested_styles`.
|
||||
|
||||
### Manual validation
|
||||
- `crates/markdown_parser/examples/table-demo/all_test_cases.md` opened in a notebook for a visual pass covering PRODUCT invariants 2–3, 6, 7–8, 9–15, 19–21.
|
||||
- AI block list responses containing a wide Markdown table — trackpad scroll, direct scrollbar interaction, double-click word selection across columns after scroll, partial-range text copy, and an existing selection that remains visually anchored across further horizontal scroll.
|
||||
- Regression pass confirming vertical scrolling stays owned by the surrounding notebook or block list (PRODUCT invariant 5).
|
||||
- Regression pass confirming narrow tables and non-table Markdown remain unchanged (PRODUCT invariant 22).
|
||||
|
||||
### Pre-merge gates
|
||||
- `cargo fmt`
|
||||
- `cargo clippy --workspace --all-targets --all-features --tests -- -D warnings`
|
||||
- `cargo check -p warp_editor` and `cargo check -p warpui_core`
|
||||
- Targeted `cargo nextest run --no-fail-fast --workspace ...` for the regressions listed above.
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
### Two offset spaces drift again
|
||||
If any table path assumes source and rendered offsets are interchangeable, formatted cells will reintroduce selection/copy drift. Mitigation: keep `TableCellOffsetMap` as the single translation layer; build the table-wide `TableOffsetMap` from source lengths only; route hit-testing, cursor, selection, and copy through explicit source↔rendered conversions.
|
||||
|
||||
### Selection pinned to viewport coordinates
|
||||
If clipped scrollables reuse raw selection rectangles after scroll changes, highlights and copied text target the wrong content. Mitigation: anchor selections via `ClippedScrollStateHandle`, clear the anchor on fresh mouse interactions, and route selection APIs through `anchor_and_adjust_selection_for_scroll()`.
|
||||
|
||||
### Scrollbar behavior diverges between surfaces
|
||||
If the editor table path reintroduces bespoke thumb sizing or drag math, it will drift from the rest of WarpUI. Mitigation: keep geometry and pointer/scroll conversion in `shared_scrollbar.rs` and call the shared helpers from both the editor table renderer and the `new_scrollable` utilities.
|
||||
|
||||
### Clipped child layers interfere with editor hit-testing
|
||||
If editor bounds checks use the clipped child layer instead of the editor element bounds, clicks can be rejected before block-level hit-testing runs. Mitigation: use the editor element's own layer bounds in `crates/editor/src/render/element/mod.rs`.
|
||||
|
||||
### Stale cache on in-place cell edits
|
||||
`TableCache` is keyed by marker instance. If a future change edits a cell in place without replacing the block marker, the cache will be stale. Mitigation (current): marker replacement on edit is sufficient for read-only tables. Mitigation (longer-term): the SumTree follow-up below invalidates per-cell state naturally.
|
||||
|
||||
## Follow-ups
|
||||
- If additional inline Markdown styles are supported in table cells, add a focused `TableCellOffsetMap::from_inline_and_source` regression alongside the parser change — the source-walking algorithm should handle them automatically, but a test pins the behavior.
|
||||
- Other clipped horizontal surfaces adopting the same selection APIs inherit the `ClippedScrollStateHandle` behavior automatically, but they should get a manual regression pass when adopting new selection UX.
|
||||
- When editable tables or other complex table operations land, evaluate moving cell/row boundaries into the `SumTree` with new `BufferText` marker types so per-cell offsets can be derived by seeking instead of reparsing. See the in-code `TODO` above `impl TableCellOffsetMap`.
|
||||
Reference in New Issue
Block a user