Skip to content

[APP-5344] Defer layout for AI plan document editors - #15579

Open
C0W0 wants to merge 4 commits into
masterfrom
terry/lazy-plan-document-layout
Open

[APP-5344] Defer layout for AI plan document editors#15579
C0W0 wants to merge 4 commits into
masterfrom
terry/lazy-plan-document-layout

Conversation

@C0W0

@C0W0 C0W0 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

Restoring a conversation replays every CreateDocuments / EditDocuments action to rehydrate plan documents (restore_ai_documents_from_exchanges). Each revision builds a NotebooksEditorModel, and that type hardcoded RenderState's lazy_layout to false, so every revision font-shaped its entire markdown document synchronously during startup — including archived revisions that are only reachable through version history and are almost never opened.

Two things made that expensive:

  • The deltas are whole-document. reset_with_markdown goes through Buffer::replace, whose EditDelta.new_lines is styled_blocks_in_range(1..max_charoffset) — the full buffer, not an incremental edit.
  • Each restored revision costs two of them. create_new_document_version snapshots the current content into version history by serializing it back to markdown and re-parsing it into a fresh editor, and then the caller resets the live editor with the next revision. In the captured trace, 94 of 174 adjacent layout passes are byte-identical, back-to-back duplicates for exactly this reason.

Cost therefore scales as revisions × document size, and plan documents grow over the course of a conversation, so long planning sessions degrade sharply.

Measurements

Restoring the same plan-heavy conversation, with RenderState instrumented locally:

before after
eager layout passes 175 75
blocks font-shaped 41,513 219
avg blocks / delta 237 3
max blocks / delta 646 11

The 75 remaining passes are ~3-block comment markdown editors, not plan documents.

Approach

NotebooksEditorModel::new_internal takes a lazy_layout flag, exposed as new_unbound_lazy. Layout then happens on first element layout via RichTextElement::layouttry_layout_pending_edits, which is the same deferral CodeEditorView already uses for code diffs.

A LayoutTiming enum picks per call site, so only bulk rehydration defers:

  • Lazyrestore_document, apply_persisted_content, and create_new_document_version (the archived-revision snapshot).
  • Eagercreate_document (agent CreateDocuments), get_or_create_streaming_document_for_create_documents (streaming plans, which auto-open their pane), and create_document_from_notebook (opening a saved plan from Warp Drive).

Every editor converted in the trace came from the restore paths, so this keeps the full win while leaving documents that are about to be displayed byte-identical to master. Bound editors — notebooks and comment editors — are untouched and still use new / new_unbound.

Not included here: the archived-revision snapshot still serializes and re-parses the document even though it no longer font-shapes it. Storing the markdown and materializing that editor on demand would remove the remaining duplicated work; worth doing as a follow-up if restore is still hot.

Linked Issue

Tracked in Linear as APP-5344. No corresponding GitHub issue.

  • The linked issue is labeled ready-to-spec or ready-to-implement.
  • Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes).

Testing

  • I have manually tested my changes locally with ./script/run

Verified by instrumenting RenderState::add_pending_edit and try_layout_pending_edits, then restoring the same conversation on a locally-run build before and after; the numbers above come from those two runs. LAZY-FLUSH was 0 in both, confirming the deferred layouts are genuinely never performed for documents that are never opened — deferral is elimination here, not postponement.

New tests

test_eager_layout_populates_child_models_after_binding and test_deferred_layout_populates_child_models_after_binding share a body and cover the case review flagged: an editor that is unbound when its content is set, which is exactly what restore does.

handle_render_model_event drops render events while rte_window_id is None, so the LayoutUpdated emitted when content is first set never reaches child_models. That holds with and without deferred layout — both tests assert zero child models at that point. Recovery comes from the first layout reporting a viewport width change (ViewportState::viewport_size flags needs_layout whenever the width differs, and an unbound editor starts at width 0), which drives a debounced rebuild_layout whose buffer edit emits a second LayoutUpdated. Both tests then assert the code block has its NotebookCommand model.

The pair exists so the eager path acts as a live control: if deferred layout ever diverges, one test passes and the other fails. This is what guards restored plans keeping their code block, embedded item, and Mermaid controls.

ChildModels::update reads content.outline_blocks() — the buffer, not the laid-out render tree — so it does not depend on layout having been applied.

Other coverage

  • ai_document — 15/15 pass, including version isolation, revert, and streaming-vs-reset equivalence.
  • notebooks::editor — 86/86 pass.
  • Existing integration test test_copy_ai_document_as_markdown_from_overflow_menu creates a document through create_document, opens the pane, and blocks on a layout-dependent assertion (the overflow button's position must land in the position cache) before copying as markdown.
  • ./script/format and all three cargo clippy ... -D warnings invocations from script/presubmit pass.

Reviewer note: NotebooksEditorModel has not previously run under deferred layout, so a spot-check of opening a restored plan pane — one with a code block and a Mermaid diagram — and switching between versions is still worthwhile.

Screenshots / Videos

N/A — no visual change intended.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-IMPROVEMENT: Faster conversation restore for conversations containing plan documents.

C0W0 and others added 2 commits August 24, 2026 18:25
Restoring a conversation replays every CreateDocuments/EditDocuments action,
rebuilding a NotebooksEditorModel per document revision. NotebooksEditorModel
hardcoded RenderState's lazy_layout to false, so every one of those editors
font-shaped its entire markdown document synchronously during restore, even
though archived revisions are only reachable via version history and are
almost never opened.

Measured while restoring a plan-heavy conversation: 175 eager layout passes
covering 41,513 blocks (avg 237, max 646 per delta) inside ~2s, 164 of them
owned by NotebooksEditorModel.

Thread a lazy_layout flag through new_internal, add new_unbound_lazy, and use
it for AIDocumentModel's editors so layout happens on first element layout --
the same mechanism code diff views already rely on. handle_render_model_event
already early-returns while rte_window_id is None, so unbound documents were
not acting on layout events during restore anyway.

Co-Authored-By: Warp <agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Aug 26, 2026
@warp-for-oss

warp-for-oss Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

@C0W0

I'm starting a first review of this pull request.

You can view the conversation on Warp.

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

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

Powered by Oz

@warp-for-oss warp-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 introduces lazy layout for AI plan document editors by threading a lazy_layout flag through NotebooksEditorModel and switching AI documents to the new lazy unbound constructor.

Concerns

  • Lazy-restored notebook editors can miss the child-model rebuild needed for code blocks, embedded items, and rendered Mermaid controls when they are first displayed.
  • One added public doc comment documents a specific caller path rather than keeping the constructor docs focused on behavior.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

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

Powered by Oz

Comment thread app/src/notebooks/editor/model.rs
Comment thread app/src/notebooks/editor/model.rs Outdated
C0W0 and others added 2 commits August 26, 2026 15:28
Only conversation restore and version archiving defer layout now. Documents that
are about to be shown -- agent CreateDocuments, streaming plans, and opening a
saved plan from Warp Drive -- lay out on creation as before. This keeps the full
measured win, since every editor converted in the restore trace came from the
restore paths, while leaving live document behavior byte-identical.

Also add paired tests for child model population on an editor that is unbound
when its content is set, which is what restore does. handle_render_model_event
drops render events while rte_window_id is None, so the LayoutUpdated emitted
when content is first set never reaches child_models. Recovery comes from the
first layout reporting a viewport width change, which drives a debounced
rebuild_layout whose buffer edit emits a second LayoutUpdated. The tests assert
the eager and deferred paths behave identically, so restored plans keep their
NotebookCommand/NotebookEmbed models for code blocks, embedded items, and
Mermaid controls.

Co-Authored-By: Warp <agent@warp.dev>
EditorLayoutTiming::OnCreate read as though layout happened exactly once at
creation, when it just means layout is not deferred. LayoutTiming::{Eager, Lazy}
matches the vocabulary already used by RenderState's lazy_layout flag.

Also trim the comments added by this branch to the repo guidelines: drop the
call-site enumeration from the doc comments, keep the inline comments to
non-obvious rationale for choosing eager over lazy, and stop narrating steps in
the test helper's doc comment.

Co-Authored-By: Warp <agent@warp.dev>
@C0W0

C0W0 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/warp-agent-review

@warp-for-oss

warp-for-oss Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

@C0W0

I'm re-reviewing this 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 /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@C0W0
C0W0 requested review from kevinyang372 and vorporeal August 26, 2026 19:50

@warp-for-oss warp-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 a lazy layout path for unbound AI plan document editors during conversation/document restoration, while keeping eagerly displayed creation/opening paths on the existing eager layout behavior. It also adds focused notebook editor tests covering child model recovery after an unbound editor is later bound and laid out.

Concerns

  • No blocking correctness, security, spec-drift, comment-guideline, or test-guideline concerns found in the annotated diff.

Verdict

Found: 0 critical, 0 important, 0 suggestions

Approve

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

Powered by Oz

@vorporeal
vorporeal removed their request for review August 26, 2026 23:47
@vorporeal

Copy link
Copy Markdown
Contributor

@kevinyang372 is the better choice for reviewer here; will leave it to him.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants