fix(ios): use plain Text during streaming to eliminate markdown flicker [AI-assisted]#40985
fix(ios): use plain Text during streaming to eliminate markdown flicker [AI-assisted]#40985eulicesl wants to merge 1 commit into
Conversation
Greptile SummaryThis PR replaces Key findings:
Confidence Score: 3/5
|
| let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace) | ||
| return segments | ||
| .filter { $0.kind != .thinking } | ||
| .map(\.text) | ||
| .joined(separator: "\n\n") |
There was a problem hiding this comment.
showsAssistantTrace parameter is always negated by the downstream filter
AssistantTextParser.segments(from:includeThinking:) is called with includeThinking: self.showsAssistantTrace. When showsAssistantTrace is true the function returns all segments — including .thinking ones. But the immediately-following .filter { $0.kind != .thinking } then unconditionally strips every .thinking segment regardless of that flag.
The net effect is that thinking-block content is always hidden during streaming, even when showsAssistantTrace is true. The includeThinking argument is therefore dead code and the PR description's claim — "correctly excluded from streaming view when showsAssistantTrace is false" — is misleading, because exclusion happens unconditionally.
If the intent is to always suppress thinking during streaming, simplify to:
| let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace) | |
| return segments | |
| .filter { $0.kind != .thinking } | |
| .map(\.text) | |
| .joined(separator: "\n\n") | |
| let segments = AssistantTextParser.visibleSegments(from: self.text) | |
| return segments | |
| .map(\.text) | |
| .joined(separator: "\n\n") |
If the intent is to honour showsAssistantTrace (show thinking when the flag is true), remove the redundant filter instead:
let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace)
return segments
.map(\.text)
.joined(separator: "\n\n")Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift
Line: 539-543
Comment:
**`showsAssistantTrace` parameter is always negated by the downstream filter**
`AssistantTextParser.segments(from:includeThinking:)` is called with `includeThinking: self.showsAssistantTrace`. When `showsAssistantTrace` is `true` the function returns **all** segments — including `.thinking` ones. But the immediately-following `.filter { $0.kind != .thinking }` then unconditionally strips every `.thinking` segment regardless of that flag.
The net effect is that thinking-block content is **always** hidden during streaming, even when `showsAssistantTrace` is `true`. The `includeThinking` argument is therefore dead code and the PR description's claim — "correctly excluded from streaming view when `showsAssistantTrace` is **false**" — is misleading, because exclusion happens unconditionally.
If the intent is to **always** suppress thinking during streaming, simplify to:
```suggestion
let segments = AssistantTextParser.visibleSegments(from: self.text)
return segments
.map(\.text)
.joined(separator: "\n\n")
```
If the intent is to **honour** `showsAssistantTrace` (show thinking when the flag is `true`), remove the redundant filter instead:
```swift
let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace)
return segments
.map(\.text)
.joined(separator: "\n\n")
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f58168c3dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Show only the visible (non-thinking) portion during streaming. | ||
| let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace) | ||
| return segments | ||
| .filter { $0.kind != .thinking } |
There was a problem hiding this comment.
Preserve thinking segments when trace mode is enabled
When showsAssistantTrace is true, this new filtering drops every .thinking segment from the streaming bubble, even though the caller still decides to render the bubble based on AssistantTextParser.hasVisibleContent(..., includeThinking: true) in ChatView. In trace mode, a response that is currently only inside <think> blocks will now render an empty bubble (and never show streaming trace text), which is a regression from the previous ChatAssistantTextBody(includesThinking:) behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Updates the iOS chat streaming UI to avoid markdown re-parsing/layout thrash during token streaming by rendering streaming assistant text as plain Text, while keeping full markdown rendering for the final committed message.
Changes:
- Replaces
ChatAssistantTextBodywith plain SwiftUITextinChatStreamingAssistantBubbleto prevent streaming flicker. - Adds
visibleTextcomputed property to derive what should be shown during streaming (currently filtering out thinking segments).
You can also share your feedback on Copilot code review. Take the survey.
| // Show only the visible (non-thinking) portion during streaming. | ||
| let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace) | ||
| return segments | ||
| .filter { $0.kind != .thinking } |
There was a problem hiding this comment.
visibleText always filters out .thinking segments. That changes behavior vs the previous ChatAssistantTextBody(... includesThinking: showsAssistantTrace) path: when showsAssistantTrace is true, thinking content will never appear during streaming, and if the stream is thinking-only the bubble can render as an empty Text("") even though ChatView will still show the streaming bubble (because hasVisibleContent(... includeThinking: true) is true). Adjust the logic so thinking segments are included when showsAssistantTrace is true (and use response-only segments when it’s false).
| // Show only the visible (non-thinking) portion during streaming. | |
| let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace) | |
| return segments | |
| .filter { $0.kind != .thinking } | |
| let segments = AssistantTextParser.segments(from: self.text, includeThinking: self.showsAssistantTrace) | |
| let visibleSegments: [AssistantTextParser.Segment] = { | |
| // When showsAssistantTrace is true, include thinking segments so the trace | |
| // is visible during streaming. Otherwise, show only response segments. | |
| if self.showsAssistantTrace { | |
| return segments | |
| } else { | |
| return segments.filter { $0.kind != .thinking } | |
| } | |
| }() | |
| return visibleSegments |
| } | ||
| .padding(12) | ||
| .assistantBubbleContainerStyle() | ||
| } | ||
|
|
||
| /// Strip markdown symbols during streaming so the plain Text view looks reasonable. |
There was a problem hiding this comment.
The doc comment for visibleText says it "Strip[s] markdown symbols during streaming", but this property only removes thinking segments (and the PR description notes markdown symbols remain visible during streaming). Please update the comment to reflect the actual behavior to avoid misleading future changes.
| /// Strip markdown symbols during streaming so the plain Text view looks reasonable. | |
| /// Show only non-thinking segments during streaming so the plain Text view reflects visible content. |
f58168c to
177727b
Compare
|
@greptile-apps Both issues addressed in amended commit 177727b:
|
|
Perfect! Both fixes confirmed:
The logic is cleaner and matches the documented behavior. Ready for final review. |
177727b to
bef8bad
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bef8badb25
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// Thinking blocks are never shown during streaming regardless of showsAssistantTrace — | ||
| /// they appear only in the final committed message via ChatAssistantTextBody. | ||
| private var visibleText: String { | ||
| AssistantTextParser.segments(from: self.text, includeThinking: false) |
There was a problem hiding this comment.
Align streaming visibility with trace-filtered content
This unconditionally removes thinking segments during streaming, but OpenClawChatView still decides whether to render the streaming bubble using AssistantTextParser.hasVisibleContent(..., includeThinking: self.showsAssistantTrace) in ChatView.swift (line 218). In trace mode, a thinking-only stream (e.g. partial <think>...</think> before final text arrives) now passes the visibility check but renders Text(""), producing an empty assistant bubble; this is a regression introduced by this change unless the visibility predicate is updated to match the filtered content.
Useful? React with 👍 / 👎.
bef8bad to
5c363ca
Compare
ChatStreamingAssistantBubble was calling ChatAssistantTextBody, which runs StructuredText (Textual) on every token. Textual re-parses the full markdown AST and re-lays out the entire bubble on each update, causing visible layout thrash and flicker during streaming. Replace with a plain SwiftUI Text view while streaming is in progress. Markdown symbols are visible but the bubble is stable. Once streaming completes the message is committed and re-rendered via ChatAssistantTextBody with full markdown formatting, matching the established ChatGPT / Claude.ai mobile pattern. No new dependencies. The Textual import in ChatMessageViews is still required by ChatAssistantTextBody for completed messages. Fixes: streaming flicker on iOS (and macOS via OpenClawKit)
5c363ca to
cb0d97c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb0d97cdf1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// Thinking blocks are never shown during streaming regardless of showsAssistantTrace — | ||
| /// they appear only in the final committed message via ChatAssistantTextBody. | ||
| private var visibleText: String { | ||
| AssistantTextParser.segments(from: self.text, includeThinking: false) |
There was a problem hiding this comment.
Gate streaming bubble on non-thinking content
When trace mode is enabled, this new visibleText path always strips thinking segments, but OpenClawChatView still decides to render the streaming bubble using hasVisibleContent(..., includeThinking: self.showsAssistantTrace). Fresh evidence in this commit is that includeThinking is now hard-coded to false here, so a thinking-only in-flight response will pass the ChatView visibility check and render an empty assistant bubble until non-thinking text arrives.
Useful? React with 👍 / 👎.
|
Closing this as superseded by #50483. Why:
Canonical replacement: #50483 |
Problem
ChatStreamingAssistantBubblewas callingChatAssistantTextBodyon every token update.ChatAssistantTextBodyuses Textual'sStructuredText, which re-parses the full markdown AST and re-lays out the entire bubble each time. On fast streams this causes visible layout thrash and flicker.Fix
Replace the streaming view with a plain SwiftUI
Textview. Stable during streaming. Once complete, the message commits andChatMessageBodyre-renders viaChatAssistantTextBodywith full markdown — same as before.Matches the pattern used by ChatGPT and Claude.ai mobile.
Before / After
Changes
ChatStreamingAssistantBubble: replaceChatAssistantTextBodywith plainText+visibleTextcomputed propertyvisibleTextusesincludeThinking: falseunconditionally — thinking blocks are never shown mid-stream;showsAssistantTraceapplies only to the final committed render viaChatAssistantTextBodymarkdownVariantproperty fromChatStreamingAssistantBubbleand its call site inChatViewTesting
swift buildon OpenClawKit: clean (0 errors, 0 warnings)upstream/mainBot Review
Greptile flagged two issues — both addressed in this commit:
showsAssistantTracelogic contradiction — fixed:includeThinking: falseis now unconditional. Thinking blocks are never displayed during streaming regardless of the flag.markdownVariantproperty — removed from struct and call site.AI Disclosure 🤖
Built with Claude Sonnet 4.6 via OpenClaw. Approach reviewed and understood by human author. Bot feedback addressed before requesting review.