Repository navigation
A ground-up correctness and architecture pass. No public API was removed
or renamed, so this is a zero-breaking-change release; see
doc/MIGRATION.md for upgrade notes.
Summary of the release (details in the sections below):
- Added: pure-Dart
RevealEnginecore with a catch-up pacer and a
smoothFadereveal mode; a built-in syntax-highlightedCodeBlockView
andCodeBlockThemeas the default code renderer (both exported from the
package barrel); additiveStreamingTextControlleraccessors
(isStreaming,markdown,copyToClipboard); accessibility support
(reduced motion, semantics, selectable text). - Changed: migrated to
gpt_markdown1.3; redesigned the markdown fade
as a cheap paint-only per-word fade; performance work (incrementalmend()
scan cache, fade repaint gating). - Fixed: pub score static analysis (
dart analyze --fatal-infosis
clean on the latest stable Flutter), plus every W-numbered bug from the
audit.
Fixed
Every W-numbered bug from the audit is fixed:
- W1 — word-by-word mode no longer rewrites whitespace; code
indentation inside a streamed markdown code block is preserved exactly,
andcodeBuilderreceives indented code verbatim. - W2 — appending text in word-by-word mode after completion no longer
drops spaces between words. - W3 — tapping the widget after it's already complete is now a no-op
(previously it could re-fireonComplete); a tap mid-animation reliably
reaches the completed state. - W4 — Arabic character mode no longer mutates the text (no more
tripled spaces) and the controller now reachescompleted. - W5/W6 — pause/resume during Arabic or LaTeX content never shrinks the
revealed text or drops characters; the end state always equals the
source. - W7 — auto-detected RTL direction is now honored in markdown mode
instead of being silently overridden to LTR. - W8 — a style or
Brightnesschange after completion now updates the
rendered output immediately (the stale per-text style cache is gone). - W9 — a config change (
typingSpeed/chunkSize/wordByWord/
markdownEnabled) mid-stream, or a plain rebuild with no change at all,
applies in place and never throwsStateError: Stream has already been listened to— including when the caller readsstream: controller.streamdirectly insidebuild()instead of caching the
Streamin a field. (StreamController.streamreturns a new,
==-equal-but-not-identicalwrapper object on every access, so the
stream-swap check now compares by value, not identity.) - W10 —
latexEnabled: trueno longer throws away markdown rendering:
headings, lists, and links keep rendering correctly alongside LaTeX. See
W11 for how$...$/$$...$$math is now handled. - W11 — shell-style
$VARSinside fenced or inline code are no longer
misdetected as LaTeX and reachcodeBuilderverbatim.latexEnabled
rewrites$...$/$$...$$togpt_markdown's native\(...\)/\[...\]
syntax itself, skipping fenced/inline code, instead of forwarding
useDollarSignsForLatextogpt_markdown(whose own rewrite runs before
it knows what is code). A$is only ever treated as LaTeX when it looks
like real math rather than currency:$5,Cost $10 - $20, and$ aloneare left as plain text, on both a closed source and mid-stream
(previously an open stream could freeze right before a bare$5while
waiting for a closing delimiter that would never come). - W15 — controller
pause/resume/stop/restartall work correctly
in stream mode (previouslypausewas ignored for streams, and
stop/restartwere ignored in every mode). - W16 — a stream error now sets the controller's
errorstate (via the
newmarkError) and renders through the newerrorBuilder, or a themed
default view, instead of a hard-coded red error message — the text
revealed so far always stays on screen. - W21 — markdown content inside an unbounded-width parent (e.g. a
Row) no longer throws; width is only forced when the incoming
constraint is actually bounded. - W22 — swapping the
controllerinstance now correctly unbinds the
old controller and binds the new one, instead of leaking a listener and
leaving both controllers dead. - W23 —
animationsEnabled: falseno longer firesonCompleteon
every text update. - W24 — a non-append text change (anything that isn't a pure suffix
addition) now keeps the common prefix and continues from there, instead
of restarting the whole reveal from zero. - W25 —
controller.onCompletedfires exactly once per
revealing→complete transition, no matter which ofupdateProgress(1.0),
markCompleted(), orskipToEnd()triggered it (previously it could
fire twice). - W26 —
.instant(stream:)andanimationsEnabled: falsewith a
stream now display chunks as they arrive and complete exactly once when
the stream closes, instead of rendering nothing. - Arabic text no longer forces
TextAlign.right; a user'stextAlignis
honored, including for Arabic content. _effectiveThemenow reacts towidget.themechanges instead of
ignoring them after the first build.- No more per-tick
RegExpcompilation for Arabic detection, and no more
O(n)StringBuffer.toString()buffer copies per stream tick.
Removed
- The hand-rolled LaTeX renderer and
LaTeXProcessor/TextSegmentregex
pipeline are deleted. Neither was ever exported, so this is non-breaking.
LaTeX rendering now goes throughgpt_markdown1.3 (its native
\(...\)/\[...\]syntax — see W11 for how$...$/$$...$$reach it)
plusflutter_math_forkfor the default renderer. - Internal dead code removed:
_cursorController,_markdownCache,
_completeMarkdownCache,_isAnimationActive,
_resumeWordByWordTypingFromOldText,_safeSetState, the unbounded
_rtlGroupCache, and the five-plus duplicatedTimer.periodicbodies
(replaced by oneRevealSchedulerwith exactly oneTimer.periodic).
Added
-
MarkdownRenderOptionsbundles everygpt_markdown1.3 pass-through
that doesn't have its own top-level parameter (style sheet, block/inline
builders, autolink config,useDollarSignsForLatex, and more) — the
single place newgpt_markdownforwards land going forward. -
errorBuilder(Widget Function(BuildContext, Object error)?) on
every constructor, called whenstreamemits an error (see W16 above). -
StreamingTextControllergains additive accessors for chat-style UIs:
isStreaming(truewhile astream:input is still open or a reveal
is in progress or paused,falseonce completed or idle),markdown
(the bound widget's full accumulated source text, e.g. behind a "copy"
button),copyToClipboard()(copiesmarkdownto the system
clipboard), and the publicupdateSource()method the bound widget
calls to publish its current source snapshot and open/closed state so
markdownandisStreamingstay truthful. -
Accessibility: reduced-motion support (reveals instantly with a
static caret and no fade), single-announcement semantics with mid-stream
text excluded from the tree,semanticsLabel, andselectable(wraps
output in aSelectionArea). -
showCursor(bool?,nullresolves tostream != null) and
cursorColor— an 8px pulsing dot caret shown while revealing, hidden on
completion. -
StreamingShimmerandMarkdownRenderOptionsare now exported from the
barrel file, along with a curated re-export of thegpt_markdowntypes
used inMarkdownRenderOptions's public signatures (MarkdownComponent,
GptMarkdownStyleSheet, the*Buildertypedefs,InlinePattern,
InlineDirective, ...), so consumers no longer need a direct
gpt_markdowndependency just to use it. -
doc/BENCHMARKS.md— published frame-budget numbers for streaming
markdown vs. bareGptMarkdown(see Performance below). -
doc/MIGRATION.mdand a weekly CI job (pub upgrade+ test,
pub downgrade+ analyze, andpana --exit-code-threshold 0). -
tool/check_coverage.dart— parsescoverage/lcov.infoand enforces a
minimum coverage threshold. -
RevealMode({smoothFade, wordFade, typewriter, instant}, exported)
and arevealModeparameter onStreamingTextand every
StreamingTextMarkdownconstructor.smoothFade(word-unit reveal, a
180ms opacity-only fade onCubic(0.2, 0, 0, 1)) is the new default on
StreamingText, the defaultStreamingTextMarkdownconstructor,
.chatGPT()and.claude();.typewriter()/.instant()default to
their own matching mode. PassrevealMode: nullexplicitly to opt out
entirely and keep the pre-2.0 behaviour driven by the legacy
wordByWord/fadeInEnabled/fadeInDuration/fadeInCurve/chunkSize/
typingSpeedparameters — seedoc/MIGRATION.md. -
StreamPacing(StreamPacing.catchUp(...)/StreamPacing.fixed(...))
and apacingparameter alongsiderevealMode.Stream<String>input
now defaults to catch-up pacing (a bursty-token-smoothing pacer: reveals
a backlog-proportional share every ~50ms, floors at 30 chars/s, and
drains fully within 400ms of the stream closing) instead of one fixed
unit pertypingSpeedtick; statictextinput keeps the fixed,
typingSpeed-driven pacer. An explicitpacing:always overrides the
default, on either input kind. -
StreamingRenderScope— an internalInheritedWidgetseam (carrying
isStreaming/isComplete/the caret builder) wrapped around the markdown
view, for a future block-level renderer to read instead of having those
threaded through by hand. Not part of the public API surface yet.
Deprecated
components/inlineComponentson all 6 constructors — use
markdownOptions.blockComponents/markdownOptions.inlinePatterns.
Passing either (even an empty list) dropsgpt_markdown's incremental
segment cache;markdownOptionsdoesn't have that cost.initialText— never displayed; has no effect.latexFadeInEnabled(widget-level andStreamingTextTheme-level) — a
no-op now that LaTeX is delegated togpt_markdown, which has no
per-run fade hook of its own. Use alatexBuilderinstead.StreamingTextTheme.blockLatexStyleand.latexScale— no-ops at the
theme level now; use alatexBuilderand the widget-levellatexScale.StreamingTextTheme.markdownStyle— usemarkdownStyleSheet.StreamProvider/DefaultStreamProvider(and the types around them) —
never wired to any widget. Pass aStream<String>directly to
StreamingTextMarkdown.streaminstead. Will be removed in 2.0.0.
All deprecations point to their replacement and are scheduled for removal
in 2.0.0; nothing is removed in this release.
Changed — SDK floor
sdk: '>=3.7.0 <4.0.0',flutter: '>=3.32.0'(raised from>=3.0.0/
>=3.10.0) — required by thegpt_markdown ^1.3.0upgrade.
Changed — behavior
These are visible behavior changes, listed explicitly since they can
affect existing consumers (notably flutter_gen_ai_chat_ui):
- The caret is on by default while revealing.
showCursordefaults to
null, which resolves tostream != null— a live stream now shows a
pulsing caret unless you explicitly passshowCursor: false. - The per-character fade is opacity-only. The old 10px translate
alongside the opacity fade is gone; only opacity animates now. - The per-character fade now applies to streams too, not just static
text: it is markdown mode (which gets its own paint-only per-word fade
viaMarkdownFadeMask, see thesmoothFadebullet below) and
Arabic/RTL where it is unavailable/suppressed, not stream vs. static
text. - Config changes apply in place. Changing
typingSpeed,chunkSize,
orwordByWordno longer restarts the reveal from the beginning — it
keeps the current position and applies the new setting going forward. onComplete/onCompletedfire exactly once per revealing→complete
transition, from whichever path reaches completion first (see W25
above). A rebuild with an unchanged source never re-fires them.- An open stream's last grapheme is held back until the next chunk
arrives or the stream closes — this is what allowssetSource/append
to never split a surrogate pair or grapheme cluster at the streaming
edge. - The default reveal is now
RevealMode.smoothFadeonStreamingText,
the defaultStreamingTextMarkdownconstructor,.chatGPT()and
.claude()(see Added above): word-unit reveal with a 180ms fade,
applying to plain text AND markdown streams, AND Arabic content (no
longer suppressed there, unlike the legacy per-character fade). Markdown
content fades too:MarkdownFadeMaskapplies a cheap paint-only
per-word fade insmoothFade/wordFademodes, without rebuilding the
GptMarkdownspan tree (disabled under reduced motion or when
animationsEnabledisfalse). PassrevealMode: nullto keep the
exact pre-2.0 behaviour. - Fenced code blocks now render through the built-in
CodeBlockView
(exported from the package barrel): a syntax-highlighted block themed by
CodeBlockTheme, with a language-label header and a copy affordance that
stays hidden but space-reserved while the block is still streaming.
Passing your owncodeBuilderopts out and keeps your own rendering,
unchanged. Stream<String>input reveals faster by default (catch-up pacing —
see Added above) instead of at a fixedtypingSpeed-per-unit rate. Pass
pacing: StreamPacing.fixed(typingSpeed)to keep the old rate.
Performance
-
A plain fade over 5k characters now uses at most 2 transient tickers
(previously oneAnimationControllerper character — 500+ concurrent
tickers at 5k chars in the old implementation). -
A 20k-character markdown stream now runs within ~1.0x-1.3x of bare
GptMarkdown1.3 (budget: 1.8x), down from roughly 2.1x against
gpt_markdown1.2.1 in the pre-rewrite implementation — see
doc/BENCHMARKS.mdfor the full methodology and numbers:Run ours median bare median ratio 1 3697us 3698us 1.000x 2 4438us 4337us 1.023x 3 4146us 4176us 0.993x -
No more per-tick
RegExpcompilation and no more O(n) buffer copies per
stream tick (both from the audit's_containsArabic/StringBuffer
findings). -
A
gpt_markdown"hybrid" reveal (lettingGptMarkdownfade-paint the
head our engine reveals) was prototyped and rejected: it measured ~1.4x
bare in isolation, but ~2.3x time and ~12.9x element-rebuilds once wired
into the real default (caret + engine + catch-up pacer together), over
both budgets. The shipped default instead fades markdown in
smoothFade/wordFadeviaMarkdownFadeMask: a paint-only, per-word
mask over the rendered paragraphs that repaints without rebuilding
GptMarkdown's span tree, keeping the real default at ~1.2x-1.5x bare
across both perf suites. Seedoc/BENCHMARKS.md's "B1-S5 correction".