From 3f1e55bf46554eaec98192a340ddb584fc182256 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 09:33:41 +0800 Subject: [PATCH 1/8] fix mobile short conversation alignment --- .../mobile/app/ui/chat/ConversationTimelineView.kt | 2 +- .../entry/src/main/ets/pages/components/ChatTimeline.ets | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/chat/ConversationTimelineView.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/chat/ConversationTimelineView.kt index 492decaf8d..67b915cbfb 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/chat/ConversationTimelineView.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/chat/ConversationTimelineView.kt @@ -178,7 +178,7 @@ internal fun ConversationTimelineView( top = topInset, bottom = if (bottomInset > 0.dp) bottomInset else 12.dp, ), - verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Bottom), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { if (hasMoreMessages) { item(key = "load-older-messages") { diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets index e39c3e0699..f6fde932f8 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets @@ -339,10 +339,9 @@ export struct ChatTimeline { // contentPadding and SwiftUI's safeAreaInset. .contentStartOffset(this.topInset) .contentEndOffset(this.bottomInset) - // A streaming reply grows in place rather than arriving as a new item, so - // the bottom of the list has to be the anchor — otherwise the text the - // agent is writing right now renders below the fold and never comes back. - .stackFromEnd(true) + // Keep short transcripts at the content start. Active turns and explicit + // follow-to-bottom calls still place streaming content at the tail. + .stackFromEnd(false) // Keeps the read position when older messages are prepended above. .maintainVisibleContentPosition(true) .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true }) From 1e6cd0e72759226b5b1f547f310b19b04668a1ce Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 09:41:30 +0800 Subject: [PATCH 2/8] preserve harmony history scroll anchor --- .../ets/pages/components/ChatTimeline.ets | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets index f6fde932f8..8eae7aa414 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets @@ -121,6 +121,9 @@ export struct ChatTimeline { private followedActiveTurnKey: string = ''; private userScrollActive: boolean = false; private historyTouchActive: boolean = false; + private firstVisibleIndex: number = 0; + private historyRestoreIndex: number = -1; + private historyRestoreCount: number = 0; // Whether new content should pull the viewport down with it. True until the // user scrolls up to read back through the transcript: at that point taking // the viewport away from them would be worse than letting the reply grow off @@ -179,6 +182,7 @@ export struct ChatTimeline { // revisions keep following only until the user deliberately scrolls away. @Monitor('timelineRevision') onTimelineChanged(): void { + this.restoreHistoryAnchorIfNeeded(); this.logTimelineProjection('revision'); const activeTurnKey = this.activeTurnKey(); if (activeTurnKey.length > 0 && activeTurnKey !== this.followedActiveTurnKey) { @@ -207,6 +211,7 @@ export struct ChatTimeline { if (!this.historyLoading) { this.historyArrival.cancelArrival(); this.stickToBottom = false; + this.captureHistoryAnchor(); this.onLoadOlder(); } }) @@ -360,6 +365,9 @@ export struct ChatTimeline { this.userScrollActive = true; } }) + .onScrollIndex((start: number, _end: number, _center: number) => { + this.firstVisibleIndex = start; + }) .onDidScroll((_scrollOffset: number, scrollState: ScrollState) => { if (this.listScroller.isAtEnd()) { this.stickToBottom = true; @@ -469,12 +477,39 @@ export struct ChatTimeline { } if (this.hasMoreMessages && !this.historyLoading && !this.historyFailed) { this.stickToBottom = false; + this.captureHistoryAnchor(); this.onLoadOlder(); return; } // A drag made while busy is consumed, not queued for after completion. } + private captureHistoryAnchor(): void { + if (this.historyRestoreIndex >= 0) { + return; + } + this.historyRestoreIndex = this.firstVisibleIndex; + this.historyRestoreCount = this.historyTimelineItems().length; + RemoteLogger.info( + `chat timeline history anchor capture index=${this.historyRestoreIndex} ` + + `count=${this.historyRestoreCount}` + ); + } + + private restoreHistoryAnchorIfNeeded(): void { + if (this.historyRestoreIndex < 0 || this.historyLoading) { + return; + } + const inserted = this.historyTimelineItems().length - this.historyRestoreCount; + const targetIndex = this.historyRestoreIndex + Math.max(inserted, 0); + this.historyRestoreIndex = -1; + this.historyRestoreCount = 0; + setTimeout(() => { + this.listScroller.scrollToIndex(targetIndex, false, ScrollAlign.START); + RemoteLogger.info(`chat timeline history anchor restore index=${targetIndex}`); + }, 0); + } + private requestFollowToBottom(reason: string): void { if (!this.stickToBottom || this.followTimerId !== 0) { return; From a659d000a43113b662a00b749d2573351b56ed45 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 09:45:26 +0800 Subject: [PATCH 3/8] keep visible history anchor during pagination --- .../entry/src/main/ets/pages/components/ChatTimeline.ets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets index 8eae7aa414..0707f4c9f5 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets @@ -505,7 +505,7 @@ export struct ChatTimeline { this.historyRestoreIndex = -1; this.historyRestoreCount = 0; setTimeout(() => { - this.listScroller.scrollToIndex(targetIndex, false, ScrollAlign.START); + this.listScroller.scrollToIndex(targetIndex, false, ScrollAlign.AUTO); RemoteLogger.info(`chat timeline history anchor restore index=${targetIndex}`); }, 0); } From a7f33e9cb35b4f21a1b71182da9093a19caf1fd5 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 09:52:47 +0800 Subject: [PATCH 4/8] align harmony conversation loading state --- .../src/main/ets/pages/components/ConversationLoadingState.ets | 3 ++- .../entry/src/main/ets/pages/components/ConversationView.ets | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationLoadingState.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationLoadingState.ets index b8c7593144..1d8fb95753 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationLoadingState.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationLoadingState.ets @@ -3,6 +3,7 @@ import { LINE, SOFT } from './Theme'; @ComponentV2 export struct ConversationLoadingState { @Param maxContentWidth: number = 0; + @Param topInset: number = 0; build() { Row() { @@ -13,7 +14,7 @@ export struct ConversationLoadingState { } .width('100%') .constraintSize({ maxWidth: this.maxContentWidth > 0 ? this.maxContentWidth : '100%' }) - .padding({ left: 22, right: 22, top: 28, bottom: 28 }) + .padding({ left: 22, right: 22, top: this.topInset + 28, bottom: 28 }) } .width('100%') .height('100%') diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationView.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationView.ets index 3307a1eebe..5ca9c1f71b 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationView.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ConversationView.ets @@ -243,7 +243,8 @@ export struct ConversationView { .height('100%') if (this.isLoadingTranscript) { ConversationLoadingState({ - maxContentWidth: this.composerPresentation === ComposerPresentation.Floating ? 760 : 0 + maxContentWidth: this.composerPresentation === ComposerPresentation.Floating ? 760 : 0, + topInset: this.hoverOperateMode ? 0 : this.topInset }) .width('100%') .height('100%') From d473831e289c6fd5cb6d422c4c14a3356a97efb7 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 10:12:55 +0800 Subject: [PATCH 5/8] anchor history pagination below load row --- .../entry/src/main/ets/pages/components/ChatTimeline.ets | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets index 0707f4c9f5..01a2b28276 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets @@ -488,7 +488,9 @@ export struct ChatTimeline { if (this.historyRestoreIndex >= 0) { return; } - this.historyRestoreIndex = this.firstVisibleIndex; + // The load-older row occupies index zero while history is available. + // Capture the first transcript row so it remains the viewport anchor. + this.historyRestoreIndex = this.firstVisibleIndex + (this.hasMoreMessages ? 1 : 0); this.historyRestoreCount = this.historyTimelineItems().length; RemoteLogger.info( `chat timeline history anchor capture index=${this.historyRestoreIndex} ` + @@ -505,7 +507,7 @@ export struct ChatTimeline { this.historyRestoreIndex = -1; this.historyRestoreCount = 0; setTimeout(() => { - this.listScroller.scrollToIndex(targetIndex, false, ScrollAlign.AUTO); + this.listScroller.scrollToIndex(targetIndex, false, ScrollAlign.START); RemoteLogger.info(`chat timeline history anchor restore index=${targetIndex}`); }, 0); } From 5288f515b5939ca668f86c15d8dd9d5b6f47ed88 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 10:46:06 +0800 Subject: [PATCH 6/8] restore harmony history by pixel anchor --- .../ets/pages/components/ChatTimeline.ets | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets index 01a2b28276..ba1a987dbe 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets @@ -122,8 +122,9 @@ export struct ChatTimeline { private userScrollActive: boolean = false; private historyTouchActive: boolean = false; private firstVisibleIndex: number = 0; - private historyRestoreIndex: number = -1; - private historyRestoreCount: number = 0; + private historyRestoreKey: string = ''; + private historyRestoreTop: number = 0; + private readonly itemTopByKey: Map = new Map(); // Whether new content should pull the viewport down with it. True until the // user scrolls up to read back through the transcript: at that point taking // the viewport away from them would be worse than letting the reply grow off @@ -286,6 +287,9 @@ export struct ChatTimeline { .onAppear(() => { this.logRenderedItem(repeatItem.item); }) + .onAreaChange((_oldArea: Area, newArea: Area) => { + this.itemTopByKey.set(ChatTimelineRevisionTracker.itemKey(repeatItem.item), this.areaPosition(newArea.position)); + }) } }) .key((item: ObservableChatTimelineItem, _index: number) => ChatTimelineRevisionTracker.itemKey(item)) @@ -485,30 +489,33 @@ export struct ChatTimeline { } private captureHistoryAnchor(): void { - if (this.historyRestoreIndex >= 0) { - return; - } - // The load-older row occupies index zero while history is available. - // Capture the first transcript row so it remains the viewport anchor. - this.historyRestoreIndex = this.firstVisibleIndex + (this.hasMoreMessages ? 1 : 0); - this.historyRestoreCount = this.historyTimelineItems().length; - RemoteLogger.info( - `chat timeline history anchor capture index=${this.historyRestoreIndex} ` + - `count=${this.historyRestoreCount}` - ); + if (this.historyRestoreKey.length > 0) return; + const history = this.historyTimelineItems(); + const index = Math.max(0, this.firstVisibleIndex - (this.hasMoreMessages ? 1 : 0)); + const item = history[index]; + if (!item) return; + const key = ChatTimelineRevisionTracker.itemKey(item); + const top = this.itemTopByKey.get(key); + if (top === undefined) return; + this.historyRestoreKey = key; + this.historyRestoreTop = top; + RemoteLogger.info(`chat timeline history anchor capture key=${key} top=${top}`); } private restoreHistoryAnchorIfNeeded(): void { - if (this.historyRestoreIndex < 0 || this.historyLoading) { - return; - } - const inserted = this.historyTimelineItems().length - this.historyRestoreCount; - const targetIndex = this.historyRestoreIndex + Math.max(inserted, 0); - this.historyRestoreIndex = -1; - this.historyRestoreCount = 0; + if (this.historyRestoreKey.length === 0 || this.historyLoading) return; + const key = this.historyRestoreKey; + const expectedTop = this.historyRestoreTop; setTimeout(() => { - this.listScroller.scrollToIndex(targetIndex, false, ScrollAlign.START); - RemoteLogger.info(`chat timeline history anchor restore index=${targetIndex}`); + const actualTop = this.itemTopByKey.get(key); + if (actualTop === undefined) return; + const delta = actualTop - expectedTop; + this.historyRestoreKey = ''; + this.historyRestoreTop = 0; + if (Math.abs(delta) > 0.5) { + this.listScroller.scrollBy(0, delta); + RemoteLogger.info(`chat timeline history anchor restore key=${key} delta=${delta}`); + } }, 0); } @@ -544,6 +551,11 @@ export struct ChatTimeline { source === ScrollSource.EDGE_EFFECT || source === ScrollSource.OTHER_USER_INPUT; } + private areaPosition(position: Object): number { + const value = (position as Record)?.y; + return this.areaLength(value === undefined ? 0 : value); + } + private areaLength(value: Object): number { const parsed = Number.parseFloat(`${value}`); return Number.isNaN(parsed) ? 0 : parsed; From 2bd8de8247640ab228e093d01521cb87a64722fb Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 11:04:33 +0800 Subject: [PATCH 7/8] preserve harmony viewport during history paging --- .../ets/pages/components/ChatTimeline.ets | 109 +++++++++++++----- .../main/ets/services/HostSessionStream.ets | 5 +- 2 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets index ba1a987dbe..0e5526c1bc 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets @@ -124,7 +124,10 @@ export struct ChatTimeline { private firstVisibleIndex: number = 0; private historyRestoreKey: string = ''; private historyRestoreTop: number = 0; - private readonly itemTopByKey: Map = new Map(); + private lastVisibleIndex: number = 0; + private historyRestoreTimer: number = 0; + private historyRestoreAttempts: number = 0; + private historyStableFrames: number = 0; // Whether new content should pull the viewport down with it. True until the // user scrolls up to read back through the transcript: at that point taking // the viewport away from them would be worse than letting the reply grow off @@ -176,12 +179,13 @@ export struct ChatTimeline { aboutToDisappear(): void { RemoteLogger.info(`chat timeline disposed items=${this.timelineItems.length}`); this.clearFollowTimer(); + this.cancelHistoryAnchor(); } // A new turn means the user has just sent a message and expects to follow its // answer even if opening the keyboard changed the List viewport. Subsequent // revisions keep following only until the user deliberately scrolls away. - @Monitor('timelineRevision') + @Monitor('timelineRevision', 'historyLoading') onTimelineChanged(): void { this.restoreHistoryAnchorIfNeeded(); this.logTimelineProjection('revision'); @@ -287,9 +291,6 @@ export struct ChatTimeline { .onAppear(() => { this.logRenderedItem(repeatItem.item); }) - .onAreaChange((_oldArea: Area, newArea: Area) => { - this.itemTopByKey.set(ChatTimelineRevisionTracker.itemKey(repeatItem.item), this.areaPosition(newArea.position)); - }) } }) .key((item: ObservableChatTimelineItem, _index: number) => ChatTimelineRevisionTracker.itemKey(item)) @@ -352,12 +353,13 @@ export struct ChatTimeline { // follow-to-bottom calls still place streaming content at the tail. .stackFromEnd(false) // Keeps the read position when older messages are prepended above. - .maintainVisibleContentPosition(true) + .maintainVisibleContentPosition(false) .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true }) .scrollBar(BarState.Off) .cachedCount(4) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { + if (!this.historyLoading) this.cancelHistoryAnchor(); this.historyTouchActive = true; this.historyArrival.beginGesture(); } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { @@ -369,8 +371,9 @@ export struct ChatTimeline { this.userScrollActive = true; } }) - .onScrollIndex((start: number, _end: number, _center: number) => { + .onScrollIndex((start: number, end: number, _center: number) => { this.firstVisibleIndex = start; + this.lastVisibleIndex = end; }) .onDidScroll((_scrollOffset: number, scrollState: ScrollState) => { if (this.listScroller.isAtEnd()) { @@ -378,6 +381,7 @@ export struct ChatTimeline { } else if (this.userScrollActive) { this.stickToBottom = false; } + if (this.historyLoading && this.userScrollActive) this.captureHistoryAnchor(); this.evaluateHistoryStart(); if (scrollState === ScrollState.Idle) { this.userScrollActive = false; @@ -488,39 +492,86 @@ export struct ChatTimeline { // A drag made while busy is consumed, not queued for after completion. } + private cancelHistoryAnchor(): void { + if (this.historyRestoreTimer !== 0) clearTimeout(this.historyRestoreTimer); + this.historyRestoreTimer = 0; + this.historyRestoreKey = ''; + this.historyRestoreAttempts = 0; + this.historyStableFrames = 0; + } + private captureHistoryAnchor(): void { - if (this.historyRestoreKey.length > 0) return; const history = this.historyTimelineItems(); - const index = Math.max(0, this.firstVisibleIndex - (this.hasMoreMessages ? 1 : 0)); + const header = this.hasMoreMessages ? 1 : 0; + // A page can extend the first assistant row upwards without adding rows. + // Preserve the lower visible row's trailing edge, not that row's changing head. + const index = Math.min(history.length - 1, Math.max(0, this.lastVisibleIndex - header)); const item = history[index]; if (!item) return; - const key = ChatTimelineRevisionTracker.itemKey(item); - const top = this.itemTopByKey.get(key); - if (top === undefined) return; - this.historyRestoreKey = key; - this.historyRestoreTop = top; - RemoteLogger.info(`chat timeline history anchor capture key=${key} top=${top}`); + try { + const rect = this.listScroller.getItemRect(index + header); + if (rect.height <= 0) return; + this.historyRestoreKey = ChatTimelineRevisionTracker.itemKey(item); + this.historyRestoreTop = rect.y + rect.height; + this.historyRestoreAttempts = 0; + this.historyStableFrames = 0; + this.clearFollowTimer(); + this.stickToBottom = false; + RemoteLogger.info( + `history viewport capture key=${this.historyRestoreKey} index=${index} bottom=${this.historyRestoreTop} rows=${history.length}` + ); + } catch (_) { + RemoteLogger.warn('History viewport capture unavailable'); + } } private restoreHistoryAnchorIfNeeded(): void { - if (this.historyRestoreKey.length === 0 || this.historyLoading) return; - const key = this.historyRestoreKey; - const expectedTop = this.historyRestoreTop; - setTimeout(() => { - const actualTop = this.itemTopByKey.get(key); - if (actualTop === undefined) return; - const delta = actualTop - expectedTop; - this.historyRestoreKey = ''; - this.historyRestoreTop = 0; - if (Math.abs(delta) > 0.5) { - this.listScroller.scrollBy(0, delta); - RemoteLogger.info(`chat timeline history anchor restore key=${key} delta=${delta}`); + if (!this.historyRestoreKey || this.historyLoading || this.historyRestoreTimer !== 0) return; + this.historyRestoreTimer = setTimeout(() => { + this.historyRestoreTimer = 0; + const key = this.historyRestoreKey; + const history = this.historyTimelineItems(); + const index = history.findIndex((item: ObservableChatTimelineItem) => + ChatTimelineRevisionTracker.itemKey(item) === key); + if (index < 0 || this.historyFailed) { + this.cancelHistoryAnchor(); + return; } - }, 0); + this.historyRestoreAttempts++; + try { + const listIndex = index + (this.hasMoreMessages ? 1 : 0); + const rect = this.listScroller.getItemRect(listIndex); + if (rect.height <= 0) { + // Mount the identity's new virtual position before measuring it. + this.listScroller.scrollToIndex(listIndex, false, ScrollAlign.END); + this.historyStableFrames = 0; + } else { + const actual = rect.y + rect.height; + const delta = actual - this.historyRestoreTop; + RemoteLogger.info( + `history viewport measure key=${key} rows=${history.length} actual=${actual} expected=${this.historyRestoreTop} delta=${delta} attempt=${this.historyRestoreAttempts}` + ); + if (Math.abs(delta) > 0.5) { + this.historyStableFrames = 0; + this.listScroller.scrollBy(0, delta); + } else { + this.historyStableFrames++; + } + } + } catch (_) { + this.listScroller.scrollToIndex(index + (this.hasMoreMessages ? 1 : 0), false, ScrollAlign.END); + } + if (this.historyStableFrames >= 3 || this.historyRestoreAttempts >= 30) { + RemoteLogger.info(`history viewport settle stable=${this.historyStableFrames} attempts=${this.historyRestoreAttempts}`); + this.cancelHistoryAnchor(); + } else { + this.restoreHistoryAnchorIfNeeded(); + } + }, 16); } private requestFollowToBottom(reason: string): void { - if (!this.stickToBottom || this.followTimerId !== 0) { + if (this.historyRestoreKey || this.historyLoading || !this.stickToBottom || this.followTimerId !== 0) { return; } this.followTimerId = setTimeout(() => { diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets index b3324531ca..4b443718e1 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets @@ -1,3 +1,4 @@ +import { RemoteLogger } from './RemoteLogger'; import { CommandStatusResponse } from '../model/RemoteModels'; /** Stream id of the host's workspace/session catalog. */ @@ -257,7 +258,9 @@ export class HostSessionStream { let pages = 0; try { while (this.hasOlder && pages < MAX_HISTORY_PAGES_PER_REQUEST && !this.stopped) { - const page = await this.read({ before: this.oldest, epoch: this.epoch }); + const before = this.oldest; + const page = await this.read({ before: before, epoch: this.epoch }); + RemoteLogger.info(`history page received stream=${this.streamId} before=${before} events=${page.events.length} first=${page.events[0]?.seq} hasMore=${page.has_more} epoch=${page.epoch}`); if (this.stopped) return; if (page.epoch !== this.epoch) { await this.gap(); From 48cc2f41797dcb851a8de07e82f940a62ecc4ae6 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Wed, 23 Sep 2026 11:58:07 +0800 Subject: [PATCH 8/8] fix(harmony): keep one history viewport anchor per page --- .../entry/src/main/ets/pages/components/ChatTimeline.ets | 5 ++++- .../entry/src/main/ets/services/HostSessionStream.ets | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets index 0e5526c1bc..9f56690f2a 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/ChatTimeline.ets @@ -362,6 +362,7 @@ export struct ChatTimeline { if (!this.historyLoading) this.cancelHistoryAnchor(); this.historyTouchActive = true; this.historyArrival.beginGesture(); + RemoteLogger.info(`history gesture begin loading=${this.historyLoading} offset=${this.listScroller.currentOffset().yOffset}`); } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { this.historyTouchActive = false; } @@ -381,7 +382,6 @@ export struct ChatTimeline { } else if (this.userScrollActive) { this.stickToBottom = false; } - if (this.historyLoading && this.userScrollActive) this.captureHistoryAnchor(); this.evaluateHistoryStart(); if (scrollState === ScrollState.Idle) { this.userScrollActive = false; @@ -480,6 +480,9 @@ export struct ChatTimeline { private evaluateHistoryStart(): void { const atStart: boolean = this.listScroller.currentOffset().yOffset <= 0; const readerOwnsPosition: boolean = this.historyTouchActive && this.userScrollActive; + if (atStart && this.historyTouchActive) { + RemoteLogger.info(`history start check owns=${readerOwnsPosition} loading=${this.historyLoading} hasMore=${this.hasMoreMessages} failed=${this.historyFailed}`); + } if (!this.historyArrival.arrived(atStart && readerOwnsPosition)) { return; } diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets index 4b443718e1..2447652f8f 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/services/HostSessionStream.ets @@ -259,6 +259,7 @@ export class HostSessionStream { try { while (this.hasOlder && pages < MAX_HISTORY_PAGES_PER_REQUEST && !this.stopped) { const before = this.oldest; + RemoteLogger.info(`history page request stream=${this.streamId} before=${before} epoch=${this.epoch}`); const page = await this.read({ before: before, epoch: this.epoch }); RemoteLogger.info(`history page received stream=${this.streamId} before=${before} events=${page.events.length} first=${page.events[0]?.seq} hasMore=${page.has_more} epoch=${page.epoch}`); if (this.stopped) return;