Skip to content

Commit f951d4f

Browse files
committed
fix(ui): prevent content vanishing on streaming completion
Three changes to fix scroll position loss when streaming ends: 1. Eliminate double completeStreamingMessage - ChatWidget now checks if message is still streaming before calling completeStreamingMessage again; if already completed by orchestrator, uses updateMessage to add performance metrics without triggering full finalization 2. Streaming completion scroll uses scroll-bottom-anchor instead of message ID with .bottom anchor, and delay increased from 50ms to 150ms to allow layout to settle after ProgressView removal and metrics addition 3. Remove content trimming in completeStreamingMessage to prevent cache invalidation cascade (content hash change -> getCachedClean miss -> displayedContent update -> MarkdownContentView re-parse)
1 parent 17fa7cb commit f951d4f

2 files changed

Lines changed: 30 additions & 17 deletions

File tree

Sources/ConversationEngine/ConversationMessageBus.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public class ConversationMessageBus: ObservableObject {
260260
let updated = EnhancedMessage(
261261
id: current.id,
262262
type: current.type,
263-
content: current.content.trimmingCharacters(in: .whitespacesAndNewlines), // Trim on completion
263+
content: current.content, // Preserve streamed content as-is to avoid cache invalidation
264264
contentParts: current.contentParts,
265265
isFromUser: current.isFromUser,
266266
timestamp: current.timestamp,
@@ -305,7 +305,9 @@ public class ConversationMessageBus: ObservableObject {
305305
contentParts: [MessageContentPart]? = nil,
306306
toolCalls: [SimpleToolCall]? = nil,
307307
status: ToolStatus? = nil,
308-
duration: TimeInterval? = nil
308+
duration: TimeInterval? = nil,
309+
performanceMetrics: MessagePerformanceMetrics? = nil,
310+
processingTime: TimeInterval? = nil
309311
) {
310312
guard let index = messageCache[id] else {
311313
logger.error("UPDATE_MESSAGE: Message not found id=\(id.uuidString.prefix(8))")
@@ -331,10 +333,10 @@ public class ConversationMessageBus: ObservableObject {
331333
toolMetadata: current.toolMetadata,
332334
toolCalls: toolCalls ?? current.toolCalls,
333335
toolCallId: current.toolCallId,
334-
processingTime: current.processingTime,
336+
processingTime: processingTime ?? current.processingTime,
335337
reasoningContent: current.reasoningContent,
336338
showReasoning: current.showReasoning,
337-
performanceMetrics: current.performanceMetrics,
339+
performanceMetrics: performanceMetrics ?? current.performanceMetrics,
338340
isStreaming: current.isStreaming,
339341
isToolMessage: current.isToolMessage,
340342
githubCopilotResponseId: current.githubCopilotResponseId,

Sources/UserInterface/Chat/ChatWidget.swift

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,14 +1279,15 @@ public struct ChatWidget: View {
12791279
performThrottledScroll(proxy: proxy, to: lastMessage)
12801280
}
12811281
.onChange(of: messages.last?.isStreaming) { oldValue, newValue in
1282-
/// Streaming completion correction: when streaming ends, the layout
1283-
/// recomputes (fixedSize kicks in for the full message height).
1284-
/// Fire a delayed scroll to keep content visible after relayout.
1282+
/// Streaming completion scroll: when streaming ends, layout recomputes
1283+
/// (ProgressView removed, metrics added, content re-parsed).
1284+
/// Scroll to bottom anchor after layout settles to keep content visible.
12851285
if oldValue == true && newValue == false {
1286-
guard scrollLockEnabled, let lastMessage = messages.last else { return }
1286+
guard scrollLockEnabled else { return }
12871287
Task { @MainActor in
1288-
try? await Task.sleep(for: .milliseconds(50))
1289-
proxy.scrollTo(lastMessage.id.uuidString, anchor: .bottom)
1288+
/// 150ms allows layout to settle after streaming->complete transition
1289+
try? await Task.sleep(for: .milliseconds(150))
1290+
proxy.scrollTo("scroll-bottom-anchor", anchor: .bottom)
12901291
}
12911292
}
12921293
}
@@ -4037,13 +4038,23 @@ public struct ChatWidget: View {
40374038
messageMetrics = nil
40384039
}
40394040

4040-
/// FIXED: Use MessageBus to finalize streaming message
4041-
/// completeStreamingMessage marks isStreaming=false, trims content, adds metrics
4042-
activeConversation?.messageBus?.completeStreamingMessage(
4043-
id: msgId,
4044-
performanceMetrics: messageMetrics,
4045-
processingTime: processingTime
4046-
)
4041+
/// Only finalize if still streaming (orchestrator may have already completed it)
4042+
/// Calling completeStreamingMessage twice causes double layout disruption
4043+
let currentMessage = messages[index]
4044+
if currentMessage.isStreaming {
4045+
activeConversation?.messageBus?.completeStreamingMessage(
4046+
id: msgId,
4047+
performanceMetrics: messageMetrics,
4048+
processingTime: processingTime
4049+
)
4050+
} else if messageMetrics != nil {
4051+
/// Message already completed by orchestrator - just add metrics
4052+
activeConversation?.messageBus?.updateMessage(
4053+
id: msgId,
4054+
performanceMetrics: messageMetrics,
4055+
processingTime: processingTime
4056+
)
4057+
}
40474058

40484059
let existing = messages[index] // Read for voice/workflow checks
40494060
logger.debug("MESSAGE_LIFECYCLE: Finalized message \(assistantMessageId?.uuidString.prefix(8) ?? "nil") with \(existing.content.count) chars (count still: \(messages.count))")

0 commit comments

Comments
 (0)