generated from exteraGramPlugins/pluginBase
Keep the chat on a far-back summary card after its anchor loads
Publish plugin / publish (push) Successful in 3s
Publish plugin / publish (push) Successful in 3s
With an offset the newest analyzed message is usually outside the loaded window, so placement asks the chat to load it first. That path could leave the view away from the card while offset 0 always worked: - Placement gave up after ~3 s (25 x 120 ms) and asked for the anchor only once. scrollToMessageId drops the request while waitingForGetDifference, and a load can be superseded. It now waits ~10 s and re-requests every ~2 s. - Loading a far anchor makes ChatActivity run its own postponed scroll, and a cache-then-server reload can re-scroll or replace the list. Either can land after the jump. Settle checks at 0.25/0.7/1.5/3 s re-jump if the card row is outside the visible range, and re-place the card if a reload dropped it. They stop as soon as the user drags the list. Also logs each run's count/offset, the fetched id range, pre-stream failures, load requests, placement and jumps to the diagnostics file, since bulletin-only errors previously left no trace. Verified on device with count 1000, offset 1000: anchor loaded on the first retry, card placed after it and the chat jumped there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
16bf8de84c
commit
af23d2e682
5 files changed
+219
-13
No files matched your search
+88
-2
@@ -1845,6 +1845,10 @@ def main(argv=None) -> int:
|
||||
def __init__(self):
|
||||
self.cells = []
|
||||
self.stopped = 0
|
||||
self.scroll_state = 0
|
||||
|
||||
def getScrollState(self):
|
||||
return self.scroll_state
|
||||
|
||||
def getMeasuredHeight(self):
|
||||
return 900
|
||||
@@ -1859,11 +1863,30 @@ def main(argv=None) -> int:
|
||||
return self.cells[index]
|
||||
|
||||
class _FakeLayoutManager:
|
||||
"""Records jumps and reports the visible row range they produce.
|
||||
|
||||
`hijack` simulates ChatActivity's own postponed scroll landing
|
||||
right after a jump and carrying the view elsewhere.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.jumps = []
|
||||
self.visible = (-1, -1)
|
||||
self.hijack = 0
|
||||
|
||||
def scrollToPositionWithOffset(self, position, offset, bottom):
|
||||
self.jumps.append((position, offset, bottom))
|
||||
if self.hijack:
|
||||
self.hijack -= 1
|
||||
self.visible = (40, 46)
|
||||
else:
|
||||
self.visible = (max(0, position - 3), position + 3)
|
||||
|
||||
def findFirstVisibleItemPosition(self):
|
||||
return self.visible[0]
|
||||
|
||||
def findLastVisibleItemPosition(self):
|
||||
return self.visible[1]
|
||||
|
||||
class _FakeChatActivity:
|
||||
def __init__(self, ids):
|
||||
@@ -2088,6 +2111,64 @@ def main(argv=None) -> int:
|
||||
f"ids={far_ids}",
|
||||
)
|
||||
|
||||
# Loading a far anchor makes ChatActivity run its own postponed scroll,
|
||||
# which can land after the jump and carry the view away from the card.
|
||||
# The settle checks must bring it back.
|
||||
hijacked = _FakeChatActivity([900, 800])
|
||||
hijacked.loadable[300] = [310, 300, 290]
|
||||
hijacked.chatLayoutManager.hijack = 1
|
||||
hijacked_card = injector.virtual_id_for(7, 300)
|
||||
injector.SummaryInjection(hijacked, 0, -4242, 0, 300, hijacked_card).insert(
|
||||
thinking="chunk", streaming=True
|
||||
)
|
||||
card_row = 2 + hijacked.ids().index(hijacked_card)
|
||||
first, last = hijacked.chatLayoutManager.visible
|
||||
check(
|
||||
"a jump overridden by the app's own scroll is redone until the card is on screen",
|
||||
len(hijacked.chatLayoutManager.jumps) == 2 and first <= card_row <= last,
|
||||
f"jumps={hijacked.chatLayoutManager.jumps} visible={(first, last)} row={card_row}",
|
||||
)
|
||||
|
||||
# Once the user takes hold of the list, the plugin stops steering it.
|
||||
dragged = _FakeChatActivity([600, 555, 500])
|
||||
dragged.chatLayoutManager.hijack = 1
|
||||
dragged.chatListView.scroll_state = 1
|
||||
injector.SummaryInjection(dragged, 0, -4242, 0, 555, injector.virtual_id_for(8, 555)).insert(
|
||||
thinking="chunk", streaming=True
|
||||
)
|
||||
check(
|
||||
"no re-jump while the user is dragging the list",
|
||||
len(dragged.chatLayoutManager.jumps) == 1,
|
||||
f"jumps={dragged.chatLayoutManager.jumps}",
|
||||
)
|
||||
|
||||
# A cache-then-server reload can replace the list under a fresh card.
|
||||
reloaded = _FakeChatActivity([600, 555, 500])
|
||||
reloaded_card = injector.virtual_id_for(9, 555)
|
||||
reloaded_inj = injector.SummaryInjection(reloaded, 0, -4242, 0, 555, reloaded_card)
|
||||
real_on_ui_for_reload = injector._on_ui
|
||||
dropped = []
|
||||
|
||||
def _reloading_on_ui(fn, delay_ms=0):
|
||||
if delay_ms and not dropped:
|
||||
# First settle check: the chat reloads its window first.
|
||||
reloaded.messages[:] = [m for m in reloaded.messages if m.messageOwner.id > 0]
|
||||
dropped.append(True)
|
||||
fn()
|
||||
|
||||
injector._on_ui = _reloading_on_ui
|
||||
try:
|
||||
reloaded_inj.insert(thinking="chunk", streaming=True)
|
||||
finally:
|
||||
injector._on_ui = real_on_ui_for_reload
|
||||
reloaded_ids = reloaded.ids()
|
||||
check(
|
||||
"a card dropped by a list reload right after placement is put back",
|
||||
dropped and reloaded_ids.count(reloaded_card) == 1
|
||||
and reloaded_ids.index(reloaded_card) == reloaded_ids.index(555) - 1,
|
||||
f"ids={reloaded_ids}",
|
||||
)
|
||||
|
||||
# An anchor that never loads: no card anywhere, rather than a card
|
||||
# attached to messages it did not analyze.
|
||||
never = _FakeChatActivity([900, 800])
|
||||
@@ -2097,8 +2178,13 @@ def main(argv=None) -> int:
|
||||
never_inj.update(answer_markdown="more", streaming=True)
|
||||
check(
|
||||
"an anchor that never loads places no card at a wrong position",
|
||||
never_card not in never.ids() and not never_inj.placed and never.scrolled_to == [300],
|
||||
f"ids={never.ids()} scrolled_to={never.scrolled_to}",
|
||||
never_card not in never.ids() and not never_inj.placed,
|
||||
f"ids={never.ids()}",
|
||||
)
|
||||
check(
|
||||
"a far anchor that does not arrive is requested again, not just once",
|
||||
len(never.scrolled_to) >= 2 and set(never.scrolled_to) == {300},
|
||||
f"scrolled_to={never.scrolled_to}",
|
||||
)
|
||||
# ...and the card shows up once the user scrolls the anchor in.
|
||||
for mid in (310, 300):
|
||||
|
||||
Reference in new issue
Block a user