conduit/lib/shared/widgets
cogwheel fdf89a8149
Fix streaming body strobing between content and loading skeleton (#540) (#541)
* Fix streaming body strobing between content and loading skeleton (#540)

Photosensitivity/seizure hazard: during a response the assistant body flashed at
~13Hz between the rendered markdown and the MarkdownLoadingSkeleton.

Root cause: StreamingMarkdownWidget let the loading skeleton (and a second
SizedBox.shrink blank path) REPLACE already-rendered content. When the turn phase
latches to completed mid-response (responseDone gap / status+tool phases), the
body is treated as non-streaming while answer tokens keep arriving; each
non-streaming content growth left the compiled document stale-but-valid during
the async recompile, and the gate regressed to the skeleton instead of keeping
the rendered content.

Make the skeleton/blank strictly a first-paint state: once a compiled document
exists it is always rendered (a stale doc lags at most one frame and is
superseded by _applyCompiledDocumentState), so the skeleton can only appear when
there is no renderable content yet. Removed the now-dead
_preserveStaleCompiledDocumentUntilFreshFinal field.

Adds a deterministic regression test (delayed compile service) reproducing the
non-streaming content-growth stale window; fails before the fix (skeleton over
rendered content), passes after.

* Clear stale content on scope change so a version switch can't show the old body (#541 review)

Macroscope + Greptile flagged that the #540 fix ('always render a non-null doc')
lets a reused StreamingMarkdownWidget show the previous message/version's content
while the new body compiles asynchronously. Fix without re-introducing the #540
strobe: thread a clearStaleDocument signal so a stateScopeId change (a new
message/version) clears the stale document — the skeleton covers the gap — while
same-scope content growth keeps its document. Added a regression test (fails
before: the previous version's text lingers on a scope switch).

* Don't make a stale streaming document selectable (#541 review)

Macroscope: the #540 fix removed the blank path that previously prevented a
stale (non-streaming, not-yet-fresh) document from being wrapped in SelectionArea.
During the responseDone gap the body is non-streaming while content still grows,
so a stale doc was made selectable — re-enabling the concurrent-modification
crash in Flutter's selection system. Gate SelectionArea on hasFreshCompiledDocument
so a stale doc renders (no #540 flash) but isn't selectable until the compile
settles. Extended the regression test to assert it.

* Clear stale streaming document on scope change (#541 review)

Greptile: the scope-change clear only ran on the non-streaming path. A scope
change while streaming (e.g. switching from an old version back to the live,
still-streaming current) kept the previous scope's compiled document visible
until the deferred refresh landed, flashing a prior message under the new scope.

Extend the existing clearDocumentWhenAsync mechanism to resolveStreamingPrepared
(same async-only clear, so cached/sync compiles never flash), thread it through
_resolveCompiledDocument's streaming branch, and carry the scope change into the
deferred streaming refresh via _pendingClearStaleDocument so only the first
post-scope-change refresh clears (same-scope growth still keeps its doc, #540).
Adds a regression test for the deferred streaming scope-change path.

(cherry picked from commit 11d75779fcab4c0fb3e338f8d4d4b96c7d950448)

* Clear the stale streaming document immediately on scope change (#541 review)

CodeRabbit + Greptile: the deferred streaming scope-change path only armed a
pending clear (_pendingClearStaleDocument), consumed later by the scheduled
refresh. build() now keeps rendering any existing compiled document while
streaming (#540), so the previous scope painted for one frame under the new
stateScopeId before the deferred refresh landed — exactly the flash this guards.

Clear synchronously in didUpdateWidget instead: add
MarkdownDocumentController.clearDocument() (cancels in-flight resolves, nulls the
document, notifies) and call it on the deferred-path scope change; the scheduled
refresh then compiles the new content. Removes the _pendingClearStaleDocument
flag machinery entirely. The sync streaming and non-streaming paths already clear
synchronously via clearDocumentWhenAsync, so all scope-change clears now happen
within didUpdateWidget — no first-frame stale paint on any path.

clearDocument() always invalidates pending async resolves first (even when no
document is rendered yet) so a stale compile started under the old scope can't
land after the clear. Test asserts the FIRST post-switch frame is already clear,
that the new body stays absent until the compile is released (proving a genuine
async gap), and pins the harness key so the switch exercises didUpdateWidget
reuse rather than a remount. Verified: the first-frame assertion fails with the
deferred behaviour, passes with the immediate clear.

(cherry picked from commit 2798fe542d9477fe1fc3a4f518a2aac549282390)
2026-06-30 21:06:30 +05:30
..
markdown Fix streaming body strobing between content and loading skeleton (#540) (#541) 2026-06-30 21:06:30 +05:30
adaptive_route_shell.dart chore(dependencies): remove cupertino_context_menu_plus and update pubspec 2026-05-13 10:55:31 +05:30
adaptive_toolbar_components.dart fix(adaptive_ui): implement opaque glass fallback for adaptive buttons across chat and note editor components 2026-06-06 15:33:35 +05:30
assistant_detail_header.dart feat(chat): add buffering functionality for streaming messages 2026-05-01 11:02:45 +05:30
chat_action_button.dart feat(dependencies): add flutter_driver and update pubspec.lock 2026-05-13 10:55:31 +05:30
chrome_gradient_fade.dart refactor(dialogs): replace showDialog with ThemedDialogs for consistency 2026-05-13 10:55:31 +05:30
conduit_components.dart feat(toolbar): refactor adaptive toolbar components for improved consistency and functionality 2026-05-17 15:39:28 +05:30
conduit_loading.dart feat(refresh-indicator): switch to adaptive RefreshIndicator for improved platform consistency 2026-05-15 01:25:03 +05:30
drawer_shell_page.dart refactor(dialogs): replace showDialog with ThemedDialogs for consistency 2026-05-13 10:55:31 +05:30
drawer_slot.dart feat(navigation): add folder navigation and sidebar search enhancements 2026-05-13 10:55:31 +05:30
jovial_svg_image.dart feat(dependencies): update package dependencies and integrate jovial_svg 2026-05-24 22:54:01 +05:30
measure_size.dart refactor: fix lints 2025-09-16 18:15:44 +05:30
middle_ellipsis_text.dart refactor(settings): remove large text preference and related functionality 2026-06-04 23:25:04 +05:30
modal_safe_area.dart refactor: visual tweaks 2025-09-19 21:12:15 +05:30
model_avatar.dart feat(ios): add ios shortcuts support 2025-11-25 00:08:51 +05:30
model_list_tile.dart feat(tags): add support for model tags in platform APIs and UI components 2026-06-04 21:23:40 +05:30
native_dropdown_button.dart refactor(auth): remove prefetchConversations method and clean up unused code 2026-05-14 23:27:31 +05:30
optimized_list.dart chore(dependencies): update pubspec and Podfile for package management 2026-05-06 17:59:41 +05:30
responsive_drawer_layout.dart refactor(responsive_drawer): remove haptic feedback on drawer interactions 2026-05-28 00:12:21 +05:30
sheet_handle.dart refactor: formatting 2025-09-24 12:00:49 +05:30
sidebar_ios26_scaffold.dart refactor(dialogs): replace showDialog with ThemedDialogs for consistency 2026-05-13 10:55:31 +05:30
skeleton_loader.dart fix: regressions 2025-08-28 19:17:05 +05:30
themed_dialogs.dart refactor(dialogs): replace showDialog with ThemedDialogs for consistency 2026-05-13 10:55:31 +05:30
themed_sheets.dart fix(adaptive_ui): implement opaque glass fallback for adaptive buttons across chat and note editor components 2026-06-06 15:33:35 +05:30
user_avatar.dart refactor(dependencies): update cached_network_image to cached_network_image_ce 2026-05-20 13:51:40 +05:30
web_content_embed.dart Offline-first persistence with Drift (#508) 2026-06-19 15:08:16 +05:30
webview_content_height.dart feat(webview): migrate from webview_flutter_plus to flutter_inappwebview 2026-05-27 00:09:21 +05:30