Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
# Block List Markdown Table Rendering — 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
## Summary
Render valid GitHub Flavored Markdown tables in AI block list responses as formatted inline tables instead of raw pipe-delimited text. The rendered table should appear directly in the response flow, match the table rendering capabilities already supported in notebooks, and preserve correct text selection behavior.
## Problem
AI responses often include Markdown tables to compare options, summarize data, or present structured results. In the AI block list, those tables are currently much harder to read because the response is shown as plain text table markup rather than as a formatted table. That makes scanning difficult, weakens visual hierarchy, and creates an inconsistent experience with notebooks, where tables already support richer rendering behavior.
This is especially problematic when table cells include inline Markdown such as bold text, inline code, strikethrough, or links. When those constructs are not rendered as formatted content inside the table, the response becomes noisy and less useful.
Selection behavior is also critical. Users need to be able to select text from rendered tables naturally, including partial selections, cross-cell selections, and selections that begin before or after the table in surrounding prose.
## Goals
- Render valid GFM pipe tables inline within AI block list responses.
- Make rendered block list tables visually and behaviorally consistent with the Markdown table primitives already supported in notebooks.
- Support the same inline formatting features inside table cells that notebook tables support.
- Preserve normal text selection and copy behavior for rendered table content.
- Keep surrounding AI response content unchanged before and after the table.
- Fall back gracefully to normal text rendering when the content is not a valid table.
## Non-goals
- Adding table editing capabilities to the AI block list.
- Supporting non-GFM table syntaxes.
- Expanding notebook table feature support as part of this work.
- Introducing a block-list-specific table feature set that diverges from notebooks.
- Converting arbitrary aligned plain text into a rendered table when it is not valid Markdown table syntax.
## User Experience
### Scope
This feature applies to Markdown-formatted AI responses rendered in the AI block list.
If a response contains a valid GitHub Flavored Markdown pipe table, that table should render inline as a formatted table in the response body. Content before and after the table should continue to render as normal Markdown text in the same response.
This behavior applies equally to:
- newly streamed AI responses
- previously rendered responses reopened from history
- transcript or restored views that display the same block list content
### Table detection
A response section should render as a table only when it is a valid GFM pipe table.
A valid table must:
- include a header row
- include a separator row
- have a consistent column structure for rows that belong to the table
If content does not form a valid table, it should remain plain Markdown text. The product should prefer not rendering a table over rendering an incorrect or partially broken table.
Tables inside fenced code blocks must continue to render as code, not as tables.
### Inline rendering behavior
When a valid table is detected, it should render as a formatted table directly in the AI block list rather than as a raw text block containing `|` characters and alignment markers.
The rendered table should behave as part of the normal response flow:
- it appears between the preceding and following response content
- it uses block list styling appropriate for AI output
- it does not require opening a notebook or a separate viewer
- it does not introduce a separate interaction mode
The baseline visual target for inline AI block list tables is the House of Agents design linked above, specifically node `7451:99490`.
The table may use horizontal scrolling when necessary to remain readable within the available block width. If horizontal overflow occurs, the table should remain fully usable and readable without truncating content unpredictably.
Wide tables should have their own horizontal scrollbar so users can scroll sideways within the table itself.
Tall tables should not introduce a separate vertical scrollbar. Vertical movement through table content should happen through the normal block list scroll behavior so the table remains part of the surrounding response flow rather than becoming a nested vertically scrolling region.
### Visual design requirements
Inline AI block list tables should match the House of Agents design:
- the table renders directly in the response flow rather than inside a card-like container
- there is no rounded outer container and no filled table background
- the header row uses bold text and primary text color while body rows use secondary text color
- table cell text uses the same font family and font size as the surrounding block list response content
- rows are separated by thin horizontal dividers
- columns do not show vertical divider lines
- the table does not show a full outer border around the perimeter
- row spacing should match the designs more open presentation, approximately 12px vertical cell padding
These presentation rules do not change the underlying Markdown, copy-response behavior, or the supported inline Markdown formatting inside cells.
### Rendering parity with notebooks
Rendered block list tables should support the same table rendering primitives already supported in notebooks. The block list should not invent a reduced or alternate table dialect.
At minimum, if notebooks support these constructs within table cells, the AI block list should render them the same way:
- left, center, and right column alignment
- bold text
- italic text
- bold-italic text
- inline code
- strikethrough
- links
- escaped pipe characters rendered as literal pipes
Alignment must remain visibly correct even when a cell's content is narrower than its column. Center-aligned and right-aligned columns should render centered and right-justified within the computed column width rather than collapsing back to left alignment.
More generally, the user-visible rule is:
If a piece of Markdown table content is supported and rendered in notebooks, it should render equivalently in the AI block list unless there is a clear product reason not to. This spec assumes parity is the default.
### Links inside table cells
Links inside table cells should render as links, using the same interaction model users already get for links in other AI Markdown output.
Single-click behavior should match normal link behavior in the block list.
Text selection must still work correctly in cells that contain links:
- click-and-drag should create a text selection rather than unexpectedly activating the link
- selecting across linked and non-linked text should behave the same as selection elsewhere in the response
### Selection behavior
Rendered tables must support normal text selection. This is a core requirement, not a best-effort enhancement.
Selection should work for:
- text within a single cell
- text spanning multiple cells in a row
- text spanning multiple rows
- selections that begin before the table and continue into the table
- selections that begin in the table and continue into following prose
- selections in horizontally scrolled tables
Selection should operate on rendered text content, not on table chrome such as borders, padding, or layout spacing.
When text is copied from a rendered table selection, the copied result should reflect the selected textual content in reading order. It should not include visual-only layout artifacts.
This feature should not regress existing selection behavior elsewhere in the block list.
### Streaming behavior
Because AI responses stream into the block list over time, table rendering should behave predictably during streaming.
The intended experience is:
- once the content is sufficient to identify a valid table, the table renders as a table
- as additional table rows stream in, the rendered table extends naturally
- streaming updates should not unexpectedly clear an active text selection
- the transition from plain incoming text to rendered table should feel stable rather than visually noisy
If the response later stops matching a valid table boundary, subsequent content should render as normal response content after the table rather than corrupting the rendered table.
### Multiple tables and surrounding content
A single AI response may contain:
- multiple tables
- prose before, between, and after tables
- code blocks adjacent to tables
- headings, lists, or other Markdown outside the table
Each table should render independently in the correct place within the response. Non-table content should continue to use its existing rendering behavior.
### Invalid or malformed table content
If a candidate table is malformed, ambiguous, or incomplete, the UI should fall back to rendering the original content as ordinary Markdown text rather than attempting a degraded table rendering.
Examples of content that should not render as a formatted table include:
- lines with pipe characters but no valid separator row
- content inside fenced code blocks
- structures that look table-like but do not resolve into a consistent table section
When a valid table ends, the next non-table content should resume normal rendering immediately after it.
### Large and wide tables
Some AI responses will contain wide tables or cells with long text.
For these cases:
- the table should remain legible inside the block list
- horizontal overflow should be handled gracefully with a table-local horizontal scrollbar
- the user should still be able to select and copy text from off-screen columns by scrolling
- vertical overflow should be handled by the block list's normal scrolling rather than by a table-local vertical scrollbar
- wide content should not break surrounding layout or cause the rest of the response to render incorrectly
### Interaction model
Rendered tables in the block list are read-only presentation of AI output.
Users should not be able to directly edit the rendered table in place. Existing higher-level actions on the AI response should continue to behave as they do today unless explicitly changed by a follow-up spec.
### Copy behavior
Block-level “copy response” behavior should copy the original Markdown source for the AI response, not a rendered or reformatted table export.
This means:
- a copied response preserves the original Markdown table syntax the model returned
- rendering a table inline in the block list does not change what block-level copy response returns
- manual text selection and copy from the rendered output may still copy the selected rendered text content, but response-level copy should preserve the original Markdown source
## Success Criteria
- A valid GFM table in an AI response renders inline in the block list as a formatted table instead of raw pipe-delimited text.
- Column alignment in the block list matches the alignment expressed by the Markdown and matches notebook table behavior.
- Inline Markdown that notebooks support inside table cells also renders correctly inside block list tables.
- Links inside cells render and behave like links without breaking text selection.
- Users can select text naturally within and across rendered table cells and rows.
- Users can extend a selection across table boundaries into surrounding prose and vice versa.
- Wide tables remain usable through a table-local horizontal scrollbar without breaking layout or selection.
- Tall tables do not create a nested vertical scrolling region and instead scroll with the surrounding block list content.
- Block-level copy response preserves the original Markdown source, including original table syntax.
- Invalid table-like content falls back to normal text rendering rather than rendering an incorrect table.
- Multiple tables in one response render independently in the correct order.
- Streamed responses and restored responses render the same table content consistently.
## Validation
- Manual validation with a simple two-column GFM table in an AI response.
- Manual validation with alignment coverage: left, center, and right aligned columns in the same table.
- Manual validation with cell formatting coverage: bold, italic, bold-italic, inline code, strikethrough, links, and escaped pipes.
- Manual validation that text can be selected inside one cell, across multiple cells, across multiple rows, and across table-to-prose boundaries.
- Manual validation that click-and-drag on linked text selects text rather than unexpectedly opening the link.
- Manual validation of a wide table that requires horizontal scrolling, confirming the table shows its own horizontal scrollbar and that readability and selection still work.
- Manual validation of a tall table, confirming it does not show its own vertical scrollbar and instead scrolls with the surrounding block list.
- Manual validation of a response containing prose, a table, more prose, and a second table.
- Manual validation that a fenced code block containing table-looking Markdown still renders as code.
- Manual validation that malformed or incomplete table syntax falls back to plain Markdown text.
- Manual validation that block-level copy response returns the original Markdown source for a response containing a table.
- Regression validation that the same Markdown table content renders equivalently in notebooks and in the AI block list wherever the notebook renderer already supports that content.
+300
View File
@@ -0,0 +1,300 @@
# Block List Markdown Table Rendering — Tech Spec
Product spec: `specs/APP-3076/PRODUCT.md`
## Problem
The AI block list already detects GFM-style tables, but it degrades them into a single preformatted text blob. That loses the structured table model we already have elsewhere in the repo, prevents reuse of notebook-style inline cell formatting, and makes copy semantics awkward because the same string is currently used for rendering and clipboard export.
The implementation needs to satisfy four constraints at once:
- reuse shared Markdown table parsing rather than maintaining a second table parser
- render with a read-only UI component appropriate for the block list rather than the editor-specific notebook renderer
- preserve source Markdown for block-level copy actions
- support block-list-native interaction rules: horizontal-only local scrolling, no nested vertical scrolling, and correct text selection
## Relevant Code
- `specs/APP-3076/PRODUCT.md` — approved product behavior
- `app/Cargo.toml` — compile-time feature declarations for feature-flagged app functionality
- `app/src/lib.rs (2363-2561)` — compile-time-to-runtime `FeatureFlag` wiring for app features
- `app/src/ai/agent/util.rs:24-145``parse_markdown_into_text_and_code_sections`; current AI-output section splitter
- `app/src/ai/agent/mod.rs (1281-1300)``AIAgentTextSection`; current table payload is `content: String`
- `app/src/ai/agent/mod.rs (1547-1588)``Display for AIAgentOutputMessage`; current copy/export path prints table `content`
- `app/src/ai/blocklist/block.rs (4941-5002)` — AI block copy helpers
- `app/src/ai/blocklist/block.rs (5923-5937)``Copy`, `CopyOutput`, and related clipboard actions
- `app/src/ai/blocklist/block/view_impl/common.rs (1139-1188)``render_table_section`; current table renderer is a `Text` element inside a horizontal scroller
- `app/src/ai/blocklist/block/find.rs:70-90` — find currently searches table `content` directly
- `markdown_parser/src/markdown_parser.rs:106-189``parse_markdown` and `parse_markdown_with_gfm_tables`
- `markdown_parser/src/markdown_parser.rs (333-458)` — structured GFM table parsing (`parse_table`, separator parsing, inline cell parsing)
- `markdown_parser/src/lib.rs (338-438)``FormattedTable` and `TableAlignment`
- `editor/src/content/text.rs (61-109)``parse_table_cell_markdown_inline`
- `editor/src/content/text.rs:261-279``table_from_internal_format_with_inline_markdown`; current notebook-side table reconstruction helper
- `editor/src/render/model/mod.rs:454-476` — notebook `TableStyle`
- `editor/src/render/model/mod.rs (1140-1234)``LaidOutTable`; notebook table layout and selection model
- `editor/src/render/element/table.rs:1-220` — notebook table painting path
- `ui/src/elements/table/mod.rs (117-240)` — shared `Table`, `TableConfig`, and column sizing API
- `ui/src/elements/table/mod.rs (517-980)``Table` layout behavior, including intrinsic measurement and viewport sizing
- `ui/src/elements/table/mod.rs (1002-1478)` — selection and scroll behavior for the shared table element
- `warp_core/src/features.rs``FeatureFlag` definitions and dogfood/default channel enablement
## Current State
### AI block list path
The block list does not use the shared Markdown table model. `parse_markdown_into_text_and_code_sections` in `app/src/ai/agent/util.rs:24-145` uses a custom helper from `ai::gfm_table` to detect a table-shaped region while scanning the response line-by-line. Once it finds one, it emits `AIAgentTextSection::Table { content: String }`, where `content` is a normalized pipe-delimited string rather than a structured table object.
Rendering then happens in `render_table_section` in `app/src/ai/blocklist/block/view_impl/common.rs (1139-1188)`, which places that string into a single `Text` node wrapped in a horizontal scroller. This satisfies the current “readable monospace dump” behavior, but it does not preserve:
- structured column alignment
- notebook-style inline formatting inside cells
- a clean separation between source Markdown and rendered representation
Because `AIAgentTextSection::Table` only stores the rendered string, copy/export and find also operate on that same string.
### Notebook path
The notebook/editor stack already has a real structured table model:
- `markdown_parser` can parse GFM tables into `FormattedTable`
- `editor/src/content/text.rs` has helpers for reconstructing a `FormattedTable` from the editors internal representation while preserving inline Markdown styles
- `editor/src/render/model/mod.rs` and `editor/src/render/element/table.rs` lay out and paint tables with alignment, padding, selection mapping, and per-cell text frames
That renderer is not a good direct fit for the block list because it is tied to the editor buffer/render model, offset maps, and editor-specific interaction flow.
### Shared UI table component
The shared `ui::elements::Table` is much closer to what the block list needs: it supports arbitrary cell elements, per-column sizing, selection delegation, and independent composition with surrounding UI.
However, its current defaults do not match the product requirements for the block list:
- it assumes an internal vertical viewport and implements vertical scrolling itself
- selection over virtualized rows is intentionally incomplete when rows are off-screen
- intrinsic width measurement only looks at headers, not row content
Those defaults are reasonable for a general-purpose scrollable table, but not for a block-list table that must stay vertically integrated with the surrounding response.
## Proposed Changes
### 0. Gate the feature behind `BlocklistMarkdownTableRendering`
Add a dedicated feature flag named `BlocklistMarkdownTableRendering` and wire it through the normal Warp feature-flag plumbing:
- add `blocklist_markdown_table_rendering` to `app/Cargo.toml`
- map that Cargo feature to `FeatureFlag::BlocklistMarkdownTableRendering` in `app/src/lib.rs`
- add the new enum variant in `warp_core/src/features.rs`
- enable it by default for dogfood builds via `DOGFOOD_FLAGS`
The new structured table rendering should only activate when this flag is enabled. When disabled, AI block list responses should continue to detect tables and render them with the pre-feature monospace scrollable table block.
### 1. Replace string-backed table sections with a structured payload
Introduce a dedicated AI-output table type in `app/src/ai/agent/mod.rs`, for example:
```rust
pub struct AgentOutputTable {
pub markdown_source: String,
pub table: FormattedTable,
}
```
Then change `AIAgentTextSection::Table` from:
```rust
Table { content: String }
```
to:
```rust
Table { table: AgentOutputTable }
```
This gives the block list two representations of the same table:
- `markdown_source` for response-level copy/export
- `FormattedTable` for rendering and selection-aware display
This is the key ownership boundary for the feature. We should not try to derive clipboard Markdown back from the rendered UI.
### 2. Reuse `markdown_parser` for table parsing
Stop using the custom `ai::gfm_table::maybe_parse_gfm_table` path as the source of truth for parsed table structure.
Instead, add a small shared helper in `markdown_parser` that parses a contiguous GFM table block into `FormattedTable`. The block list section splitter in `app/src/ai/agent/util.rs` should continue to own boundary detection between:
- plain text
- fenced code blocks with metadata
- tables
but once it has collected a candidate table block, it should hand the raw Markdown to `markdown_parser`, not re-parse inline cell content itself.
Concretely:
- keep the existing line-oriented section splitter in `app/src/ai/agent/util.rs` so code-block metadata parsing remains unchanged
- replace the current custom table-formatting helper with a new shared parser entry point from `markdown_parser`
- store the exact raw table Markdown in `markdown_source`, preserving spacing and source syntax for copy/export
This reuses the repos actual GFM table parsing logic, including:
- alignment parsing
- inline Markdown in cells
- links, inline code, bold, italic, and strikethrough handling
### 3. Preserve source Markdown in copy/export flows
Update the copy/export paths that currently rely on `AIAgentOutputMessage` display formatting to use the tables `markdown_source` rather than a rendered or normalized text serialization.
The affected flows are the existing AI block and conversation export paths in:
- `app/src/ai/agent/mod.rs (1547-1588)`
- `app/src/ai/agent/conversation.rs:1082`
- `app/src/ai/blocklist/block.rs (4941-5002, 5923-5937)`
The rule is:
- block-level copy actions use `markdown_source`
- rendered-text selection continues to come from the UI layer
This keeps block-level copy behavior aligned with the product spec without complicating the table renderer.
### 4. Add a block-list table renderer built on the shared UI `Table`
Add a new renderer for AI-output Markdown tables in the block list, either as a helper in `app/src/ai/blocklist/block/view_impl/common.rs` or as a dedicated view/component in the same module tree.
The renderer should:
- take `&AgentOutputTable`
- build a `ui::elements::Table`
- create one header element per `FormattedTable::headers` entry
- create one row per `FormattedTable::rows` entry
- wrap the table in the existing horizontal `NewScrollable` / `ClippedScrollStateHandle` composition used for block-list tables today
Each cell should be rendered with `FormattedTextElement`, using a one-line `FormattedText` built from the cells `FormattedTextInline` fragments. This preserves the same inline Markdown primitives already supported by notebooks without embedding the editor renderer.
This renderer should intentionally stay read-only. No editor state, offset map, or notebook-specific block model should be introduced into the block list.
### 5. Extend the shared UI `Table` for block-list usage
The shared UI table needs one opt-in mode for this feature.
Add an explicit vertical sizing mode to `ui/src/elements/table/mod.rs` rather than another boolean toggle. The shared `TableConfig` should expose an enum that distinguishes the default viewported behavior from a full-content mode, e.g. `TableVerticalSizing::Viewported` vs `TableVerticalSizing::ExpandToContent`.
In `ExpandToContent` mode:
- the table expands to its full content height
- table-local vertical scrolling is disabled
- the parent scroll container owns vertical scrolling
This keeps the API clear about the underlying layout model rather than asking callers to infer semantics from a boolean.
- existing behavior remains the default `Viewported` mode
- the block list opts into `ExpandToContent`
- layout measures all rows, not just visible rows
- the element reports full content height
- `ScrollableElement` does not capture vertical wheel scrolling for the table
- the table no longer behaves like its own vertical viewport
This change is necessary for two reasons:
1. it enforces the product rule that tall tables scroll with the block list, not inside a nested scroller
2. it removes the current virtualization-related selection limitation for off-screen rows
### 6. Make intrinsic widths account for row content in block-list mode
If we render block-list tables with the current `ui::elements::Table` intrinsic sizing behavior, only header content contributes to intrinsic widths. That is likely to produce visibly different results from notebook tables when body cells are wider than their headers.
To keep the block-list result visually close to notebook tables, add an opt-in width measurement path for the shared `Table` so intrinsic column widths can include body cells when desired.
This should be scoped narrowly:
- preserve current default behavior for existing `Table` users
- enable body-cell-aware intrinsic sizing only for block-list Markdown tables in `TableVerticalSizing::ExpandToContent` mode
Because block-list tables will already be in `ExpandToContent` mode, we can avoid a separate measurement-only render pass: render each row once in the full-content layout path, measure unconstrained intrinsic widths from those already-instantiated body cells, then lay those same row elements out with the final computed column widths. This removes the extra `render_fn` pass for intrinsic body-width measurement while keeping the change scoped to the block-list path.
### 7. Separate find/search text from source Markdown
After the table payload becomes structured, the block list should not use `markdown_source` for find matching. That would make the find surface operate on Markdown syntax instead of rendered text.
Add a helper on `AgentOutputTable` that flattens the parsed table into plain find/selection text in row-major order, using tab-separated cells and newline-separated rows. Then update `app/src/ai/blocklist/block/find.rs:70-90` to search that derived plain text instead of the raw Markdown source.
This keeps find behavior aligned with the rendered content while leaving clipboard export source-accurate.
## End-to-End Flow
1. The AI response streams in as Markdown text.
2. `parse_markdown_into_text_and_code_sections` in `app/src/ai/agent/util.rs` continues scanning line-by-line.
3. When it encounters a candidate table region, it collects the raw Markdown block and hands it to a shared `markdown_parser` helper.
4. The parser returns a `FormattedTable`.
5. The block list stores that as `AIAgentTextSection::Table { table: AgentOutputTable { markdown_source, table } }`.
6. The block renderer sees the table section and builds a read-only WarpUI `Table`.
7. The WarpUI table renders inline cell formatting via `FormattedTextElement`.
8. The block list wraps the table in a horizontal scroller only.
9. Vertical scrolling stays with the surrounding block list.
10. Block-level copy actions export `markdown_source`; selection copy comes from the rendered table elements.
## Implementation Plan
### Phase 1: Data model and parsing
- Add `BlocklistMarkdownTableRendering` feature-flag plumbing and gate the block-list table behavior behind it
- Add `AgentOutputTable` and update `AIAgentTextSection`
- Add a shared GFM-table parsing helper in `markdown_parser`
- Update `app/src/ai/agent/util.rs` to emit structured table sections with preserved `markdown_source`
- Remove or stop using the custom `ai::gfm_table` helper
### Phase 2: Copy/find behavior
- Update AI output formatting and block/conversation copy flows to use `markdown_source`
- Add a plain-text flattening helper for find
- Update `app/src/ai/blocklist/block/find.rs` to search rendered table text rather than raw Markdown
### Phase 3: UI table support
- Replace the expand-to-content boolean with an explicit `TableVerticalSizing` enum on `TableConfig`
- Extend `ui::elements::Table` with an `ExpandToContent` mode that disables local vertical scrolling
- Extend intrinsic measurement so body cells can participate when requested, using the single full-content layout pass in `ExpandToContent` mode
- Keep existing behavior as the default for current users of the component
### Phase 4: Block-list rendering
- Replace the current monospace `render_table_section` with a structured renderer built on WarpUI `Table`
- Reuse current horizontal scroll handle plumbing
- Match notebook table styling as closely as practical via block-list table theme helpers
## Risks and Mitigations
### Risk: nested vertical scrolling or incomplete selection
Using the shared `Table` without modification would keep the current vertical viewport and virtualization behavior, which conflicts with the product spec.
Mitigation:
- add an explicit `TableVerticalSizing::ExpandToContent` mode for block-list tables
- disable table-local vertical scrolling in that mode
### Risk: copy/export regressions
Today the table sections rendered string is also what gets copied. Moving to structured tables could accidentally change clipboard output.
Mitigation:
- make `markdown_source` a first-class field on the table payload
- route copy/export through that field explicitly
- add unit coverage for block-level and conversation-level copy
### Risk: visual mismatch with notebook tables
If block-list tables use header-only intrinsic sizing or different theme tokens, they may look noticeably different from notebook tables.
Mitigation:
- add body-cell-aware intrinsic sizing for block-list mode
- define a small style translation helper that mirrors notebook table border, padding, alternating-row, and header treatments as closely as practical
### Risk: performance on very large tables
Expanding to full height and measuring all rows is more expensive than a virtualized viewport, even after removing the extra intrinsic-width render pass.
Mitigation:
- accept the tradeoff for the first version because AI-response tables are typically modest in size
- keep the expand-to-content mode opt-in and local to this feature
- revisit with profiling only if large-table responses become a real issue
## Testing and Validation
### Parser and data-model tests
- Add `markdown_parser` tests for the new shared table-block parser:
- simple tables
- alignment parsing
- inline formatting in cells
- links
- strikethrough
- escaped pipes
- invalid/non-table input
- Add `app/src/ai/agent/util_tests.rs` coverage that verifies:
- table sections preserve exact `markdown_source`
- code blocks that contain table-looking text are not parsed as tables
- prose before/after a table still produces the correct section ordering
### Copy and find tests
- Add tests covering `AIAgentOutputMessage` / exchange formatting to verify block-level copy uses original Markdown table syntax
- Add block-list find tests to verify searches match rendered cell text rather than Markdown syntax
### UI table tests
- Add WarpUI table tests for the new expand-to-content mode:
- no local vertical scroll behavior
- full content height is returned
- selection spans all rows because no rows are virtualized away
- Add table sizing tests covering body-cell-aware intrinsic measurement
### Block-list rendering validation
- Manual validation that wide tables get a local horizontal scrollbar
- Manual validation that tall tables scroll with the block list and do not show a nested vertical scrollbar
- Manual validation that selection works within cells, across cells, across rows, and across prose/table boundaries
- Manual validation that Markdown links in cells remain clickable while click-drag still selects text
- Manual validation that rendered output visually matches notebook tables closely for alignment, padding, borders, and row treatment
## Follow-ups
- Generalize the block-list table renderer into a reusable read-only Markdown table view if other surfaces need it
- Consider adding autodetected file-path/URL highlighting inside table cells if we decide block-list tables should match plain-text-section link detection behavior as well
- If future AI outputs include very large tables, revisit whether the shared `Table` should support a hybrid mode that preserves block-list vertical scrolling while still reducing layout cost