Skip to content

text: Commit background parses that a newer streamed chunk overtook - #3344

Merged
huacnlee merged 2 commits into
longbridge:mainfrom
FenjuFu:text-commit-overtaken-parses
Oct 2, 2026
Merged

huacnlee merged 2 commits into
longbridge:mainfrom
FenjuFu:text-commit-overtaken-parses

Conversation

@FenjuFu

@FenjuFu FenjuFu commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Closes #2634

Description

This fixes the condition @huacnlee pointed out in #2634 (comment): the result receiver in TextViewState drops any background parse whose revision is not the latest one.

When a stream appends faster than the background parser finishes, the next push_str / set_text arrives while a parse is still running. By the time that parse's result reaches the receiver, state.revision has already moved on, so the result is dropped. The next parse has the same problem. During a steady stream every result is discarded, and the view only updates after the stream goes quiet. This also undoes MAX_COALESCED_UPDATES_PER_PARSE. That bound exists so sustained streams still render intermediate updates (#2371), but the intermediate results it produces always carry an older revision, so they were dropped too.

A result parsed after the last full replacement is a prefix of the current text, and committing it is safe. The background parser builds every append on top of the document it has accumulated, so later results still contain all of the text. This PR moves the receiver body into TextViewState::commit_parsed_update and changes only the discard rule:

  • commit a result whose revision is at least full_update_revision and newer than committed_revision;
  • discard a result from before the last replacement (its text is no longer a prefix of the current text) or one not newer than what is already committed (for example the baseline ack of a synchronous replacement).

Everything else in the commit path is unchanged.

Real-stream cadence

To check that a real LLM stream hits this, I streamed one answer from Spark-X2.5 (the open-weight XHToken/Spark-X2.5 reasoning model, served through iFlytek Astron's OpenAI-compatible API) and timed the deltas:

deltas chars gap p50 gap p90 median delta
reasoning_content 1,768 18,221 70.8 ms 98.6 ms 10 chars
content 226 2,705 71.1 ms 95.3 ms 11 chars

That is the same ~55–100 ms cadence reported in the issue, sustained for over two minutes while the model reasons. Any parse that takes longer than one gap gets overtaken.

How to Test

New tests in crates/base/src/text/state.rs:

  • stream_commits_a_parse_that_a_newer_chunk_overtook: pushes a chunk, then pushes the next chunk before the first one's parse lands, and checks that the overtaken parse is committed. The final parse then brings in the rest.
  • a_parse_from_before_a_replacement_is_discarded: a parse that lands after a (background) full replacement is still discarded.
cargo test -p gpui-base --lib
cargo clippy -p gpui-base --all-targets -- --deny warnings

I ran both on Linux in my fork's CI, along with a check that restores the old revision != state.revision rule and confirms stream_commits_a_parse_that_a_newer_chunk_overtook fails without the fix: https://github.com/FenjuFu/gpui-kit/actions/runs/36841533259 (1282 passed; the old rule fails the new test).

I could not run the Story app on my machine (Windows without the MSVC linker), so this is verified by the tests above, not by watching example-stream-markdown.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

TextViewState discarded every background parse result whose revision was
not the latest. A stream that appends faster than a parse completes has
always pushed another chunk by the time a result lands, so every result
was discarded and the view showed nothing new until the stream stopped.

Commit a result parsed since the text was last replaced: it is a prefix
of the current text. Results from before a replacement, or older than the
committed revision, are still discarded.

Signed-off-by: FenjuFu <[email protected]>
trancong12102 pushed a commit to aislopware/gpui-kit that referenced this pull request Oct 1, 2026
Taken from longbridge#3344 (FenjuFu), not yet merged there.

A background parse result used to be dropped whenever its revision was not
the latest. A stream that appends faster than a parse completes had always
moved on, so nothing landed until the stream stopped. A result parsed since
the text was last replaced is a prefix of the current text, and is now
committed. One from before that replacement, or older than what is already
committed, is discarded.

Reconciled with this fork's own changes:
- The receiving task reads the state first, so that a result it discards
  is not an update. It now uses the same rule (`takes_parse_of`).
- `commit_parsed_update` hands the result to the fork's `commit_parse`.
- The fork parses a small append on the UI thread, so the two tests build
  on a paragraph long enough to send the append to the background parser.

Co-Authored-By: Claude Code
trancong12102 added a commit to aislopware/slopty that referenced this pull request Oct 1, 2026
gpui-fast moves to 85f8347, which imports zed 1399a804. The only GPUI
change is zed#64935: the which-key popup hides bindings that are
disabled, and key dispatch is unchanged. longbridge/gpui-fast main
92a9b0f (#33, #34) was already in the fork.

gpui-kit moves to 5b79c23a. It takes longbridge/gpui-kit#3344 ahead of
its merge upstream. A background parse that a newer streamed chunk
overtook used to be dropped, so a long streamed answer showed nothing
until the stream stopped. Now it is committed. The receiving task still
reads before it updates, so a parse it drops changes nothing.

Co-Authored-By: Claude Code
trancong12102 pushed a commit to aislopware/gpui-kit that referenced this pull request Oct 2, 2026
Taken from longbridge#3344 (FenjuFu), not yet merged there.

A background parse result used to be dropped whenever its revision was not
the latest. A stream that appends faster than a parse completes had always
moved on, so nothing landed until the stream stopped. A result parsed since
the text was last replaced is a prefix of the current text, and is now
committed. One from before that replacement, or older than what is already
committed, is discarded.

Reconciled with this fork's own changes:
- The receiving task reads the state first, so that a result it discards
  is not an update. It now uses the same rule (`takes_parse_of`).
- `commit_parsed_update` hands the result to the fork's `commit_parse`.
- The fork parses a small append on the UI thread, so the two tests build
  on a paragraph long enough to send the append to the background parser.

Co-Authored-By: Claude Code
@huacnlee
huacnlee enabled auto-merge (squash) October 2, 2026 13:47
@huacnlee
huacnlee merged commit a4ba683 into longbridge:main Oct 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TextView markdown: trailing-only debounce starves reparse under steady streaming updates

2 participants