Skip to content

Fix streamed Markdown cleanup at EOF - #910

Open
wkramme wants to merge 1 commit into
mpfaffenberger:mainfrom
wkramme:fix/streamed-markdown-eof-rendering
Open

Fix streamed Markdown cleanup at EOF#910
wkramme wants to merge 1 commit into
mpfaffenberger:mainfrom
wkramme:fix/streamed-markdown-eof-rendering

Conversation

@wkramme

@wkramme wkramme commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

This fixes streamed Markdown responses that can appear to stop unexpectedly and lose their final content when the provider closes the stream without emitting a PartEndEvent.

What changed

  • Centralized streamed text-part cleanup in _finish_text_part().
  • Flushes any remaining non-whitespace Markdown buffer.
  • Finalizes the Termflow Markdown parser on graceful stream EOF.
  • Awaits smooth-writer shutdown so buffered terminal output is not lost.
  • Reuses the same cleanup path for normal PartEndEvent handling.
  • Preserves the existing behavior for whitespace-only trailing buffers.
  • Added regression tests for:
    • Graceful EOF without PartEndEvent.
    • Unterminated fenced Markdown/code blocks.
    • Whitespace-only trailing content.
    • Exact parser call ordering and finalization.

Problem

The streaming handler incrementally parses complete lines and stores the final partial line in termflow_line_buffers.

Previously, that final buffer was only processed when a PartEndEvent arrived. However, providers can end an otherwise successful stream without sending that event. In that case:

  1. The final partial Markdown line remained buffered.
  2. Termflow never received finalize().
  3. Open Markdown constructs, such as fenced code blocks, were not closed.
  4. The smooth output writer could finish without receiving the missing content.
  5. The user saw an incomplete response that looked like Markdown rendering had randomly stopped.

Root cause

Text-part cleanup was tied exclusively to PartEndEvent.

Thinking streams already had post-stream cleanup for parts that remained open after the event iterator ended, but streamed text parts did not have an equivalent fallback. This left a gap specifically for graceful provider EOF without a matching PartEndEvent.

Implementation details

The new _finish_text_part() helper performs the complete text-part teardown:

  1. Removes the parser, renderer, and line buffer from the active state.
  2. Parses the remaining non-whitespace partial line, if any.
  3. Calls parser.finalize() to close open Markdown state.
  4. Awaits the smooth typewriter writer so all rendered output reaches the terminal.

The helper is called from:

  • The existing PartEndEvent path.
  • A new post-stream cleanup pass for text parts still active when the provider ends the stream.

The cleanup remains safe against duplicate calls because the relevant state is removed with dict.pop().

Testing

Validated with focused renderer and streaming tests:

text 140 passed

Also verified:

  • git diff --check
  • Ruff formatting on the changed files
  • Exact parser call ordering for complete and buffered lines
  • Whitespace-only buffers do not create extra Markdown content

The public checkout's local test environment currently has a pre-existing dependency mismatch during test configuration import:

text ModuleNotFoundError: No module named 'pydantic_ai.capabilities'

The focused tests passed in the development checkout before the change was ported to this clean upstream-based branch. The dependency mismatch is unrelated to this PR and should be resolved by synchronizing the local environment with the repository's pinned dependencies.

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.

1 participant