Skip to content

spec: <details>/<summary> support in markdown rendering (#10259)#13345

Open
seobew wants to merge 5 commits into
warpdotdev:masterfrom
seobew:spec/gh10259-details-summary
Open

spec: <details>/<summary> support in markdown rendering (#10259)#13345
seobew wants to merge 5 commits into
warpdotdev:masterfrom
seobew:spec/gh10259-details-summary

Conversation

@seobew

@seobew seobew commented Jul 3, 2026

Copy link
Copy Markdown

Closes #10259

What

Product + tech spec for #10259 (ready-to-spec), under specs/GH10259/ in the product.md + tech.md format from CONTRIBUTING.md.

  • product.md — 12 testable behavior invariants: collapsible rendering with GFM semantics, deterministic malformed-input degradation (missing/duplicate/unclosed <summary>), hard resource limits (nesting depth 8, 512 widgets/doc — constants, not heuristics), collapsed-state-as-view-state (copy/raw_text always full content), accessibility contract with renderer-generated IDs, round-trip serialization, and streaming behavior.
  • tech.md — grounded in the current parser layout: a FormattedTextLine::Details container variant following the FormattedTable precedent (single line variant, flat document model preserved), a parse_details branch in parse_markdown's block chain, explicit handling in the imported-HTML path (html_parser.rs), editor BufferBlockStyle mapping, and a per-invariant test matrix. Tradeoffs (start/end marker lines, tree-shaped model) are discussed and rejected with reasons.

Relation to prior work

#10462 proposed a spec for the same issue; Oz's reviews there flagged non-deterministic resource caps, undefined malformed-<summary> handling, implementation pointers not matching the checkout, and unscoped preview surfaces. This spec addresses those points directly and follows the two-file spec format. Happy to converge with the earlier author if they're still active.

Open questions for maintainers

Called out in tech.md: whether the disclosure widget reuses the existing block-folding machinery or gets a dedicated component, and the confirmed enumeration of non-interactive preview call sites for invariant 11.

🤖 Generated with Claude Code

… rendering

Adds specs/GH10259/product.md (12 testable behavior invariants covering
collapsible rendering, malformed-input degradation, deterministic
depth/count limits, accessibility, round-trip serialization, and
streaming) and tech.md (FormattedTextLine::Details container variant
following the Table precedent, parse_markdown block branch, imported-HTML
path, editor mapping, and a per-invariant test matrix).

Issue: warpdotdev#10259

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cla-bot

cla-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the users @seobew on file. In order for us to review and merge your code, each contributor must visit https://cla.warp.dev to read and agree to our CLA. Once you have done so, please comment @cla-bot check to trigger another check.

@oz-for-oss

oz-for-oss Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@seobew

Every PR must be linked to a same-repo issue before Oz can review it.

Next step: open or find a same-repo issue describing this change, then link it to this PR by adding Closes #123 to the PR description (or using the "Development" sidebar on GitHub). A maintainer will mark the issue ready-to-spec when it is ready. Once it is marked, comment /oz-review to re-trigger review.

See the contribution guidelines for the full readiness model.

Powered by Oz

@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label Jul 3, 2026
oz-for-oss[bot]
oz-for-oss Bot previously requested changes Jul 3, 2026

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seobew

Every PR must be linked to a same-repo issue before Oz can review it.

Next step: open or find a same-repo issue describing this change, then link it to this PR by adding Closes #123 to the PR description (or using the "Development" sidebar on GitHub). A maintainer will mark the issue ready-to-spec when it is ready. Once it is marked, comment /oz-review to re-trigger review.

See the contribution guidelines for the full readiness model.

Powered by Oz

@seobew

seobew commented Jul 3, 2026

Copy link
Copy Markdown
Author

/oz-review

@oz-for-oss

oz-for-oss Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@seobew

I'm re-reviewing this spec-only pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@seobew

seobew commented Jul 3, 2026

Copy link
Copy Markdown
Author

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label Jul 3, 2026
@cla-bot

cla-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

The cla-bot has been summoned, and re-checked this pull request!

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds product and technical specs for rendering HTML5 <details>/<summary> blocks in Warp markdown surfaces. The specs cover the core behavior, parser shape, serialization, malformed-input fallback, resource bounds, and test matrix, but several requirements are still underspecified enough to create incompatible implementations.

Concerns

  • The product spec does not define deterministic handling for malformed <summary> tags, even though malformed summary behavior is part of the stated scope.
  • The editor-buffer mapping does not define how details body boundaries and nesting survive in the flat buffer model, which is necessary for reliable markdown serialization.
  • Product invariant 11 says non-interactive preview surfaces are enumerated in the tech spec, but the tech spec leaves that enumeration as an open question.

Verdict

Found: 0 critical, 3 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH10259/product.md Outdated
1. A block-level `<details>` element whose content is markdown renders as a collapsible section: a summary line with a disclosure indicator (▸/▾), followed by the body when expanded.
2. The section is collapsed by default. If the `<details>` tag carries the standard `open` attribute, it renders expanded initially.
3. Clicking the summary line (or pressing Enter/Space while it is focused) toggles the section. The toggle target is keyboard-focusable and exposed to accessibility APIs as an activatable disclosure control. Any accessibility IDs are renderer-generated; they are never derived from input attributes.
4. The first `<summary>` child provides the summary line, rendered with inline markdown styling. If no `<summary>` is present, the summary line is the localized literal "Details". Any additional `<summary>` siblings are treated as ordinary body content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Define deterministic handling for malformed <summary> tags, such as an unclosed <summary> inside <details>. The PR description calls this out, but the spec only covers missing and duplicate summaries, leaving fallback behavior ambiguous.

Comment thread specs/GH10259/tech.md Outdated

In `crates/editor/src/content/markdown.rs`:

- New `BufferBlockStyle::Details { default_open }` mapping, with the summary as the block's first line and body lines following, mirroring how `Table` blocks carry structured payloads.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Specify how the flat editor buffer records details body boundaries and nested body-line ownership. Without explicit boundary or nesting metadata, to_markdown cannot reliably know which following lines belong before emitting </details>.

Comment thread specs/GH10259/tech.md Outdated
- Serialization back to markdown (`to_markdown`) emits `<details>`/`<summary>` markup, adding ` open` when `default_open` (invariant 10).
- The disclosure widget itself (collapse/expand, focus, click, Enter/Space handling, renderer-generated accessibility IDs) lives in the block renderer layer. Open/collapsed is per-view UI state initialized from `default_open`, not part of `FormattedDetails` equality — so toggling does not dirty the text delta.

**Open question for maintainers:** whether the renderer should reuse the existing block-folding interaction machinery (as used for command blocks) or introduce a dedicated disclosure component; the spec constrains behavior (product invariants 1-3, 11), not the component choice. Similarly, the exact list of non-interactive preview call sites (invariant 11) needs a maintainer-confirmed enumeration; candidates are the conversation/agent-history preview renderers that already render markdown without hit-testing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Product invariant 11 says the non-interactive preview surfaces are enumerated in the tech spec, but this leaves them as an open question. Enumerate the exact call sites or narrow the invariant so implementation and validation target the same surfaces.

…trip, surface enumeration

- product.md: deterministic rules for malformed <summary> (unclosed,
  stray closer, attributes, inline-only content); invariant 11 rewritten
  as two-tier rendering with static fallback as the default.
- tech.md: buffer round-trip concretized via DETAILS_BLOCK_MARKDOWN_LANG
  internal code blocks following the TABLE_BLOCK_MARKDOWN_LANG precedent
  (boundaries via fences, nesting as literal markdown re-parsed under the
  depth cap); rendering surfaces enumerated per call site (interactive:
  agent conversation path; static fallback: all other parse_markdown
  consumers); test matrix extended for 8d-8f and internal-format
  round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@seobew

seobew commented Jul 3, 2026

Copy link
Copy Markdown
Author

Addressed all three review points in 2d2b79f:

  1. Malformed <summary> — product invariant 8 now covers unclosed <summary> (consumes remainder of the details body; body empty; content preserved), stray </summary> inside a body (plain text), and attribute/inline-only rules (line breaks collapse, block constructs inside summary render as literal inline text). Test matrix rows 8d-8f added.
  2. Buffer boundaries/nesting — tech spec §4 now specifies the FormattedTable mechanism verbatim: a DETAILS_BLOCK_MARKDOWN_LANG = "warp-markdown-details" internal code block (per TABLE_BLOCK_MARKDOWN_LANG, markdown_parser.rs:39) whose fence delimits the region; nested details survive as literal markdown inside the parent block and re-materialize on re-parse under MAX_DETAILS_DEPTH. Round-trip test added to the matrix.
  3. Surface enumeration — invariant 11 is now self-contained two-tier rendering (interactive widget in the agent conversation path via parse_markdown_into_text_and_code_sections, util.rs:35/189; deterministic static fallback everywhere else), and tech spec §5 enumerates every current parse_markdown call site per tier. The only remaining open question is the component choice (reuse block-folding vs dedicated disclosure), which is intentionally left to maintainers.

/oz-review

@oz-for-oss

oz-for-oss Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@seobew

I'm re-reviewing this spec-only pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds product and technical specs for rendering HTML5 <details>/<summary> blocks in Warp markdown surfaces. The product behavior covers malformed input, bounded nesting/widget counts, view-state semantics, serialization, static fallback rendering, and streaming. No approved implementation spec context was attached, so there is no implementation-vs-spec drift to evaluate.

Concerns

  • The parser design does not specify how the details tag scanner interacts with markdown code fences or other literal regions, which can cause tags inside body code blocks to rebalance or close the container incorrectly.
  • The proposed editor/internal serialization requires verbatim body markdown even though the proposed data model stores only parsed FormattedText, so the round-trip contract is not implementable as written.
  • The static fallback tier claims to enumerate all non-interactive parse_markdown consumers, but the current checkout has additional consumers that are not assigned handling requirements.

Verdict

Found: 0 critical, 3 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH10259/tech.md Outdated
Add `parse_details` to the `alt((...))` block chain in `crates/markdown_parser/src/markdown_parser.rs`, before `parse_paragraph`:

- Matches only at block start (spec invariant 8c): optional leading spaces, `<details` + optional attributes (`open` recognized, others ignored) + `>`.
- Scans forward for the matching `</details>` with a tag-balance counter; on success, extracts the first top-level `<summary>…</summary>` (invariant 4) and recursively calls `parse_markdown` on the body with a `depth` parameter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Define how the tag-balance scanner treats tags inside markdown literal regions such as fenced code blocks; otherwise a </details> inside the body code sample can prematurely close the container and violate the “body content parses as normal markdown” invariant.

Comment thread specs/GH10259/tech.md Outdated

The buffer round-trip follows the `FormattedTable` mechanism exactly. Tables survive the flat buffer model as dedicated internal code blocks (`TABLE_BLOCK_MARKDOWN_LANG = "warp-markdown-table"`, markdown_parser.rs:39, with `to_internal_format`/`from_internal_format`, lib.rs:361-430); details blocks get the analogous `DETAILS_BLOCK_MARKDOWN_LANG = "warp-markdown-details"` internal representation:

- **Internal format**: line 1 is a header carrying the `open` flag and the summary serialized as inline markdown (tab-separated, like the table format); lines 2..N are the body's markdown source verbatim — including any nested `<details>` markup as literal text. `FormattedDetails::to_internal_format`/`from_internal_format` in `crates/markdown_parser/src/lib.rs` own this, mirroring `FormattedTable`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This requires the body markdown source to be stored verbatim, but the proposed FormattedDetails model only keeps body: FormattedText, which loses the original markdown source. Add raw-source storage or specify canonical re-serialization so invariant 10 can be implemented.

Comment thread specs/GH10259/tech.md Outdated

**Interactive tier (initial implementation)** — the agent conversation block path: `app/src/ai/agent/util.rs:35` (`parse_markdown_into_text_and_code_sections`, via `parse_markdown_with_gfm_tables` at util.rs:189). This is where agent output renders and where the disclosure widget is implemented.

**Static-fallback tier** — all other current `parse_markdown` call sites render summary-then-body expanded with no widget. Enumerated at time of writing:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This is not a complete enumeration of current parse_markdown consumers; the checkout also has consumers such as notebooks, editor model conversion, CLI-agent rendering, agent API conversion, and skill parsing. Update this section so every new FormattedTextLine::Details consumer has an explicit widget or fallback path.

…source, structural fallback

- product.md: invariant 8 sub-items lettered (a)-(g); new (g): tags inside
  fenced code blocks are literal and never open/close a container;
  recognition only at block start outside fences.
- tech.md: closing-tag matching specified as a fence-aware line walk (no
  raw text scan); FormattedDetails gains body_source (verbatim slice) so
  the warp-markdown-details round-trip is implementable without
  reconstructing markdown from parsed FormattedText; rendering fallback
  anchored structurally at the two shared sinks (FormattedTextElement in
  warpui_core + editor buffer conversion) instead of per-surface
  enumeration, with the complete 18-file parse_markdown consumer list
  included as informative reference; test matrix extended (8g,
  byte-exact round-trip with fenced bodies).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@seobew

seobew commented Jul 3, 2026

Copy link
Copy Markdown
Author

Addressed all three points in a74d6ae:

  1. Tag scanning vs literal regions — new product invariant 8(g): tags inside fenced code blocks are code content and never open/close a container; recognition happens only at block start outside fences. Tech spec now specifies matching as a fence-aware line walk (reusing the fence recognition parse_code_block performs) instead of a raw text scan, with test row 8g locking the behavior.
  2. Round-trip contract vs data modelFormattedDetails now carries body_source: String (the verbatim input slice) alongside the parsed body. Serialization emits body_source directly, so the warp-markdown-details round-trip is byte-exact and implementable without reconstructing markdown from parsed FormattedText. Cost and equality semantics are stated.
  3. Consumer coverage — the fallback is no longer an enumerated per-surface obligation. It is anchored structurally at the two sinks all consumers render through: FormattedTextElement (warpui_core, the shared layout element — fallback there, widget as an opt-in builder used only by the agent path) and the editor buffer conversion (§4 mapping). The enum-variant addition is also a compile-time forcing function across the 57 non-test files referencing FormattedTextLine. The complete 18-file parse_markdown consumer list is retained as informative reference.

/oz-review

@oz-for-oss

oz-for-oss Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@seobew

I'm re-reviewing this spec-only pull request in response to a review request.

You can view the conversation on Warp.

I reviewed this pull request and requested human review from: @vorporeal.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss
oz-for-oss Bot dismissed their stale review July 3, 2026 04:55

Oz no longer requests changes for this pull request after the latest automated review.

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds product and technical specs for rendering <details>/<summary> markdown as collapsible sections, with explicit parser, editor round-trip, renderer fallback, streaming, accessibility, and validation expectations.

Concerns

  • No blocking completeness, clarity, feasibility, issue-alignment, internal-consistency, or high-level security concerns found in the changed specs. The specs define deterministic malformed-input behavior, bounded handling for untrusted markdown, generic static fallback behavior for non-interactive surfaces, round-trip behavior, and parser/editor/renderer test coverage.

Verdict

Found: 0 critical, 0 important, 0 suggestions

Approve

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss
oz-for-oss Bot requested a review from vorporeal July 3, 2026 04:55
@vorporeal
vorporeal requested review from bnavetta and removed request for vorporeal July 7, 2026 18:51
@vorporeal

Copy link
Copy Markdown
Contributor

hey @bnavetta - can you take a look at this? you have much more context on markdown rendering than i do.

Comment thread specs/GH10259/product.md Outdated
1. A block-level `<details>` element whose content is markdown renders as a collapsible section: a summary line with a disclosure indicator (▸/▾), followed by the body when expanded.
2. The section is collapsed by default. If the `<details>` tag carries the standard `open` attribute, it renders expanded initially.
3. Clicking the summary line (or pressing Enter/Space while it is focused) toggles the section. The toggle target is keyboard-focusable and exposed to accessibility APIs as an activatable disclosure control. Any accessibility IDs are renderer-generated; they are never derived from input attributes.
4. The first `<summary>` child provides the summary line, rendered with inline markdown styling. If no `<summary>` is present, the summary line is the localized literal "Details". Any additional `<summary>` siblings are treated as ordinary body content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "ordinary body content", do you mean that the text would be shown as-is, with the <summary> tags, or would the tags be omitted?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the spec did not actually say. Clarified in 2632ed6: the tags render as visible plain text and the inner content as normal markdown, consistent with 8(b)/8(e) and with the principle that markup the parser did not consume is never silently dropped.

Comment thread specs/GH10259/product.md Outdated
4. The first `<summary>` child provides the summary line, rendered with inline markdown styling. If no `<summary>` is present, the summary line is the localized literal "Details". Any additional `<summary>` siblings are treated as ordinary body content.
5. The body content between `<summary>` (or `<details>` when no summary exists) and `</details>` is parsed as normal markdown: paragraphs, code blocks, lists, tables, and nested `<details>` all render as they would at top level.
6. Nesting is supported up to a fixed depth of 8. This limit is deterministic: a `<details>` opening at depth 9 or greater is not rendered as a widget — its tags are rendered as plain text and its content rendered inline (current behavior), and this fallback applies consistently regardless of input size or timing.
7. A document renders at most 512 details widgets. From the 513th onward, the same deterministic plain-text fallback as (6) applies. Both limits exist to bound resource use against untrusted input; they are constants, not heuristics.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on this a bit? The cost of another level of <details> nesting or an additional <details> block should be fairly comparable to the cost of tracking the same content as plain text

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right that the content cost is comparable — the limits are not about content size. What each widget adds beyond its content is (a) a level of parser recursion (the depth limit) and (b) a keyboard-focusable element with an accessibility-tree node and event bookkeeping (the count limit). Invariant 7 now states this explicitly. Happy to adjust the constants if you would prefer different bounds.

Comment thread specs/GH10259/product.md Outdated
- (g) Details tags never take effect inside literal regions: a `<details>`, `</details>`, or `<summary>` appearing within a fenced code block is code content and neither opens nor closes a container. Outside fenced code, opening and closing tags are recognized only at the start of a block (consistent with (c)), so tags embedded mid-paragraph are plain text.
9. Collapsed state is view state, not content: copying the block, `raw_text()` extraction, and buffer conversion always include summary and full body regardless of the current collapsed state.
10. Round-trip: converting the rendered block to markdown (e.g. copying an agent message as markdown) emits `<details>`/`<summary>` markup, preserving the `open` attribute only if the block was expanded by default in the source.
11. Rendering is two-tier. The interactive disclosure widget (invariants 1-3) is implemented in the agent conversation block renderer — the surface the issue targets. Every other markdown surface uses a generic static fallback: the block renders expanded, in source order (summary line first, then body), with no disclosure indicator and no interaction. The fallback is the default for any renderer that has not opted into the widget, so no surface can render details in an undefined state. The concrete call sites for both tiers are enumerated in the tech spec.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth speccing out support in the notebook/plan renderer as well (RichTextEditorView). For plans in particular, support for collapsed content would be useful.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — and the buffer-native model from your §4 comment makes this nearly free: details regions now live directly in editor buffers with HiddenLinesModel folding, so RichTextEditorView becomes the second interactive-tier surface rather than a static-fallback consumer. Product invariant 11 and tech §5 are updated to commit to it (2632ed6).

Comment thread specs/GH10259/tech.md Outdated
// FormattedTextLine::Details(FormattedDetails)
```

`body_source` is the verbatim input slice between the end of the summary and the closing tag. It exists so the buffer/markdown round-trip (§4) serializes the body exactly, without reconstructing markdown from parsed `FormattedText`; `body` is derived from it (`parse_markdown(body_source)`) and used only for rendering. Cost: one extra copy of the body text per details block, bounded by input size. `PartialEq` derives over all fields; equal `body_source` implies equal `body`, so streaming delta comparisons stay consistent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, Warp's rich-text pipeline doesn't attempt to guarantee exact preservation of the source Markdown; I think it's fine to continue that here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Dropped body_source entirely; round-trip is now canonical re-serialization from parsed structure, and product invariant 10 is reworded to match the pipeline-wide guarantee (structure and content preserved, exact source bytes not). This also removes the duplicated body storage and its equality subtleties. (2632ed6)

Comment thread specs/GH10259/tech.md Outdated

`body_source` is the verbatim input slice between the end of the summary and the closing tag. It exists so the buffer/markdown round-trip (§4) serializes the body exactly, without reconstructing markdown from parsed `FormattedText`; `body` is derived from it (`parse_markdown(body_source)`) and used only for rendering. Cost: one extra copy of the body text per details block, bounded by input size. `PartialEq` derives over all fields; equal `body_source` implies equal `body`, so streaming delta comparisons stay consistent.

The whole `<details>…</details>` region becomes a single `FormattedTextLine`, like `Table`. Rationale over start/end marker lines: markers would let intervening edits produce unbalanced documents, and every renderer/serializer consumer would need to track container state; a single variant keeps the flat model's invariants intact.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach taken by Table is very much an intermediate implementation to keep our options open for eventually supporting table editing. We shouldn't try to replicate it here - details regions should be modeled directly in the buffer. Among other things, that approach makes text selection much more complicated (which is true of tables regardless).

Given that you're supporting nested details items, I'd look at lists for inspiration instead (specifically ordered lists). Details items are kind of like ordered list markers, where the block marker metadata is the summary text rather than the list number. That approach should also make it fairly doable to track nested details depth in the SumTree

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you — this reshaped tech §4. A details region is now modeled directly in the buffer on the ordered-list precedent:

  • A one-line DetailsSummary block whose marker carries { depth, default_open } and whose line text is the summary itself (editable/selectable, like ordered-list item text); the disclosure triangle is a render-time margin decoration like the list number, never in the char stream.
  • Body blocks follow as ordinary blocks, closed by a zero-content DetailsEnd sentinel.
  • Depth is a StyleSummary-style +1/−1 counter over the begin/end pair, so depth-at-offset is an O(log n) SumTree seek — the "track nested depth in the SumTree" you pointed at.
  • Both markers read as a single \n in the byte iterator, so selection/copy stay trivial (the property the table mechanism loses).
  • Unbalanced pairs after edits get deterministic rules mirroring product 8(a)/8(b); collapse reuses HiddenLinesModel (per-view, outside undo history), which makes invariant 9 hold by construction.

The table to_internal_format path is dropped. One judgment call worth flagging: I put the summary in the line's text content rather than in the marker metadata — reading your note strictly ("block marker metadata is the summary text") you may have meant the summary string lives on the marker itself. I chose line text because it keeps the summary editable and inline-selectable with zero new offset machinery, but it is an easy flip if you would rather keep it out of the char stream. (2632ed6)

…ound-trip

- Rewrite tech spec §4 on the ordered-list precedent instead of the
  FormattedTable internal-code-block mechanism: DetailsSummary block
  marker (depth, default_open) with summary as line text, DetailsEnd
  sentinel, SumTree counter dimension for depth, HiddenLinesModel for
  folding, deterministic 8a/8b rebalancing under edits.
- Drop body_source; round-trip is canonical re-serialization
  (product invariant 10 reworded).
- Add notebook/plan editor (RichTextEditorView) as the second
  interactive-tier surface (invariant 11, tech §5).
- Clarify duplicate <summary> rendering (tags visible as plain text)
  and the rationale for the depth/count limits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@seobew

seobew commented Jul 8, 2026

Copy link
Copy Markdown
Author

@bnavetta thanks for the review — all five points addressed in 2632ed6, replies inline on each thread. Summary:

  1. Duplicate <summary> (product): tags render as visible plain text, inner content as normal markdown — consistent with 8(b)/8(e).
  2. Depth/count limits (product): reworded — the bound is on what each widget adds (parser recursion; focusable element + a11y node + event bookkeeping), not content size.
  3. RichTextEditorView (product): promoted to the second interactive-tier surface; the buffer-native model makes native folding for plans nearly free.
  4. Byte-exact round-trip (tech): dropped body_source; round-trip is now canonical re-serialization, matching the rest of the pipeline.
  5. Buffer model (tech §4): rewritten off the table mechanism onto the ordered-list precedent — DetailsSummary/DetailsEnd block markers, summary as line text, a SumTree counter dimension for depth, HiddenLinesModel for collapse, and deterministic edit-rebalancing. Flagged one judgment call on that thread (summary as line text vs. marker metadata) — easy to flip if you prefer.

Ready for another look when you have a moment.

@seobew
seobew requested a review from bnavetta July 9, 2026 14:09
Comment thread specs/GH10259/tech.md Outdated

In the parser IR (the read-only rendering model), the whole `<details>…</details>` region is a single `FormattedTextLine`. Start/end marker lines are wrong *at this layer*: `FormattedText` consumers are stateless per-line renderers and `compute_formatted_text_delta` diffs lines independently, so nothing maintains marker balance here. The editor buffer is the opposite case — it has edit machinery that already maintains paired markers — and gets a paired representation instead (§4).

- `LineCount::num_lines` returns `1 + body line count` (content lines, independent of collapsed state — collapsed is view state, spec invariant 9).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, the 1 is for the summary line?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — the 1 is the summary/header line the container renders above its body, present regardless of collapse state. Reworded in ecd6ccd to say so explicitly.

Comment thread specs/GH10259/tech.md Outdated
Details regions are first-class buffer blocks, following the ordered-list precedent (per maintainer guidance: the `FormattedTable` internal-code-block mechanism is an intermediate implementation and is not replicated here — it makes the block multi-line and forces `TableCache`-style offset mapping onto selection).

- **Summary line**: a details region starts with a one-line block styled `BufferBlockStyle::DetailsSummary { depth: u8, default_open: bool }` (new variants alongside `OrderedList`/`TaskList`, text.rs:867). Exactly like an ordered-list item, the marker (`BufferText::BlockMarker`) carries the block metadata while the line's *text content is the summary itself* — editable, selectable inline text, the analogue of list-item text. The disclosure indicator is a render-time margin decoration like the list number (drawn by the render element, never present in the char stream). The list-number analogy for metadata is `depth` and `default_open`; the checkbox precedent (`TaskList { complete }`) shows a togglable bool on the marker already exists.
- **Region extent**: the body is the run of ordinary blocks following the summary line, terminated by a zero-content end sentinel block `BufferBlockStyle::DetailsEnd { depth: u8 }`. Begin/end markers form pairs the same way paired inline-style markers do (`StyleSummary`'s +1/−1 counters, text.rs:1551): a counter field added to the buffer summary increments on `DetailsSummary` and decrements on `DetailsEnd`, so nesting depth at any offset is an O(log n) `SumTree` summary seek, and the `depth` field on each marker is a cached copy of that structural depth (as `indent_level` is for lists).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this is zero-content makes me wonder if details start/end markers should be new top-level variants of BufferText, rather than nested in BufferBlockStyle. We assume that there's only one BufferBlockStyle active for a given character, so we'll need a way to express that something is a code block within a details section, for example - that seems easier if details are not themselves block styles.

When rendering a given block, we'd then be able to consult the SumTree to check both whether or not it's part of a details section and what its specific style is. We can then adjust styling (colors, fonts, etc.) on a per-block basis.

This seems sort of equivalent to how link start/end markers don't use quite the same modeling as data-less markers for other inline styles like bold and italic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right call — adopted in ecd6ccd, and it also settles the summary-placement question I flagged last round. §4 now models details as a top-level BufferText::Details(DetailsMarker::Start { summary, default_open } / End) zero-width span marker, directly on the Link precedent rather than as a BufferBlockStyle:

  • Because it is a span, not a style, it is orthogonal to per-character block style: a code block/list inside a details region keeps its own BufferBlockStyle, and a renderer consults the SumTree for both "inside details, at what depth" and "this block's own style" — exactly the per-block styling adjustment you described. (You are right that this parallels how link start/end markers differ from the data-less bold/italic markers.)
  • Depth is the balanced counter, not a stored field: I add details_counter/total_details_marker to StyleSummary and a DetailsDepth dimension mirroring link_counter/LinkCount (text.rs:1550/1743), so depth-at-offset is an O(log n) seek.
  • The marker is zero-width like Link (added to the zero-width arm of Item::summary(), text.rs:1817, and skipped by the Bytes iterator, text.rs:301), so it adds no \n and selection stays trivial.
  • Summary now lives on the Start payload (your "block marker metadata is the summary text"), so editing it is a marker-data affordance like editing a link URL, not inline line-text editing.

Comment thread specs/GH10259/tech.md Outdated
- **Selection/copy stay trivial**: both markers read as a single `"\n"` in the byte iterator (text.rs:~626), exactly like every other `BlockMarker`; the summary is ordinary line text and body blocks are ordinary blocks. No offset mapping, no selection special cases — the property the table mechanism loses.
- **Nesting**: a nested details region is simply a nested begin/end pair among the body blocks. `MAX_DETAILS_DEPTH` applies on conversion into the buffer; depth never multiplies block types.
- **Edit invariants (deterministic rebalancing, mirroring product 8a/8b)**: an unmatched `DetailsSummary` (its `DetailsEnd` was deleted) owns blocks to the end of the buffer — the 8(a) rule; an unmatched `DetailsEnd` is inert and skipped on serialization — the 8(b) rule. Both are properties of the pairing scan, not stored state, so no edit sequence can produce an undefined document. `line_break_behavior()` (text.rs:904): Enter on a summary line starts a new plain body block below it (the marker case, like lists); Enter in the body behaves normally. Converting a block to/from details styles goes through the existing `BufferEditAction::StyleBlock` path (`set_block_style`/`convert_block`, model.rs:1142/1175), which already shows how to preserve marker metadata across conversions.
- **Collapse = existing folding**: collapsed state reuses `HiddenLinesModel` (hidden_lines_model.rs:20) — the body range is an anchor-pair hidden region, per-editor and outside buffer content, initialized from `default_open`. This satisfies product invariant 9 by construction: hiding is view state; copy, `raw_text()`, and serialization walk the buffer, which always contains the full body. Toggling does not edit the buffer, so it cannot dirty deltas or undo history.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something to check during implementation, but copying might also respect HiddenLinesModel

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the copy path against the current tree: it does not respect HiddenLinesModel. selected_text_as_plain_textclipboard_text_in_range (buffer.rs:1039/2374) walks the selection's raw char-offset range, and the selection anchors resolve to real buffer offsets (selection_model.rs:128) with no HiddenLinesModel reference on that path — hidden ranges are only collapsed in the rendered tree (render/model/mod.rs:2691) and used for cursor navigation (selection.rs:414-459). So selecting across a collapsed details span copies the full hidden body, which is what makes invariant 9 hold today. I have written §4 to state this as an invariant the implementation must preserve (if copy is ever changed to respect folds, details would need to opt out), and added a regression test guarding it. Agree it is worth re-confirming during implementation.

@bnavetta

bnavetta commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for the second pass! I'd like the spec to clarify how details sections interact with other block-level styling, but this is generally headed in the right direction.

Second review pass (bnavetta):
- §4 rewritten off the Link precedent: a top-level BufferText::Details
  (DetailsMarker::Start{summary,default_open}/End) zero-width span
  marker, orthogonal to BufferBlockStyle, so body blocks keep their
  own styles and a renderer can query details-depth and block-style
  independently. Resolves the per-character block-style exclusivity
  problem raised in review.
- Depth is a SumTree counter dimension (LinkCount precedent), not a
  stored per-marker field.
- Confirmed via source that copy does not consult HiddenLinesModel
  (buffer.rs:1039/2374), so a collapsed span still copies its full
  body — invariant 9 holds; noted as an invariant to preserve.
- Clarified num_lines '1' is the summary/header line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@seobew

seobew commented Jul 13, 2026

Copy link
Copy Markdown
Author

@bnavetta thanks for the second pass — all three addressed in ecd6ccd, replies inline.

Headline: details are now a zero-width span marker, not a BufferBlockStyle. Your point about per-character block-style exclusivity was correct, so §4 follows the Link precedent — a top-level BufferText::Details(DetailsMarker::Start { summary, default_open } / End) that is orthogonal to block style. Body blocks keep their own styles; a renderer queries the SumTree for details-depth and block-style independently; depth is the balanced counter (LinkCount precedent), not a stored field; the marker is zero-width so selection stays trivial; and the summary lives on the Start payload (settling the summary-placement question from last round).

Also: I verified against the current tree that the copy path does not consult HiddenLinesModel (buffer.rs:1039/2374), so a collapsed span still copies its full body — invariant 9 holds, now written as an invariant to preserve with a regression test. And confirmed the 1 in num_lines is the summary/header line.

Ready for another look.

fbartho added a commit to fbartho/warp that referenced this pull request Jul 17, 2026
…w precedent (GH13735)

Model block alignment as start/end BufferText markers plus a SumTree
dimension (ordered-list precedent), not a FormattedTextLine::AlignedBlock
wrapper. The wrapper collapses into a BufferBlockStyle when lowered into
the editor content model, which violates bnavetta's warpdotdev#13345 constraint that
only one BufferBlockStyle is active per character (aligned + code block
can't compose). Demote the wrapper to a considered-and-rejected alternative.

Also: add TUI surface disposition (best-effort within terminal width,
left-aligned fallback) as an explicit scope decision; mark warpui_core::Align
as a GUI-only paint strategy rather than the content model; fix the feature-
gating path to crates/editor/src/content/buffer.rs:850-855; rewrite the Risks
blast radius honestly for the marker design (bounded BufferText match sites)
versus the rejected wrapper (20 files matching FormattedTextLine, incl. the
non-obvious ipynb_parser and warp_tui); cite bnavetta's canonical round-trip
ruling on invariant 6.

@bnavetta bnavetta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay! One last suggestion, but otherwise ready to implement

Comment thread specs/GH10259/tech.md

```rust
pub enum DetailsMarker {
Start { summary: String, default_open: bool }, // summary as inline-markdown source, like LinkMarker::Start(url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, thinking about this more, since the summary is also formatted text, we'll need a way to track that formatting. We probably want 3 markers, so the buffer structure is something like:

<details:start default_open=true>This is the summary<details:end-summary>This is the body<details:end>

I think we can do that without significantly changing the SumTree setup - we'd still count start/end markers to determine depth, and when extracting the summary, we can seek or linearly scan to the nearest EndSummary marker.

With the current structure, we'd have to parse the inline summary markdown again to display it

@bnavetta bnavetta added ready-to-implement The issue is ready for implementation work. area:editor-notebooks Editors, notebooks, markdown rendering, LSP, and code display. labels Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi @seobew — a reviewer requested changes on this PR and it hasn't had activity from you in 7 days. When you get a chance, please push updates or reply to the review so a reviewer can take another look. Without activity, this PR will be automatically closed after 14 days of inactivity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:editor-notebooks Editors, notebooks, markdown rendering, LSP, and code display. cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team. ready-to-implement The issue is ready for implementation work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support <details>/<summary> HTML tags in markdown rendering

3 participants