fix(tui): keep chat list sorted by updated.value across all mutations#449
Open
manuel-goepfi wants to merge 1 commit into
Open
fix(tui): keep chat list sorted by updated.value across all mutations#449manuel-goepfi wants to merge 1 commit into
manuel-goepfi wants to merge 1 commit into
Conversation
c3d05ca to
2d9b11b
Compare
The chat list panel held the newest-first invariant only at first-shot load. Three mutation paths could subsequently desync the visible order from chat.conversation.updated.value, leaving stale chats stuck at the top: * `add_chat` only sorted while ConversationsSnapshot.first_shot was True. After first-shot, a chat that *appeared* new to the widget map (because the panel had never seen it) jumped to position 0 regardless of timestamp. * `add_chat_at_top` blindly prepended every chat passed to it, even when the chat's updated.value placed it elsewhere in the order. * `update_chat` mutated title/summary in place but never re-positioned, so a chat whose updated.value just bumped (new message arrival) did not bubble to the top. This commit centralises the ordering rule: * Adds a module-level `_chat_sort_key` returning (rank, value) tuples so chats with a usable timestamp sort by datetime, while chats whose snapshot fails to load sink to the bottom under reverse=True without raising. `_chat_sort_key_tuple` is the (chat, title, subtitle) variant used by the items list. * `load_chats` now sorts the incoming list before mounting, so the panel opens with newest-first ordering even when copilot.chats() returns identifiers in arrival order. * `add_chat` always sorts, gated by `_sort_items_by_updated`. The existing `_reorder_pending` debounce moves into `_schedule_reorder` and continues to coalesce reorder passes during first-shot drain (avoids O(n^2) when hundreds of WS-streamed chats arrive in one refresh cycle). Post-first-shot, events are sparse — reorder eagerly. * `add_chat_at_top` becomes a thin shim over `add_chat`. A genuinely new chat will already land at the top via its newest timestamp; a chat that merely appeared new to the widget map falls into its correct slot instead of jumping forward. * `update_chat` resorts and reorders so a bumped updated.value bubbles the chat in the DOM to match the data invariant. * `base_list_panel._reorder_item_widgets` now narrows its bare-Exception handler to `NoMatches`, the only legitimately swallowable exception here (Textual mount is async and the just-mounted sibling may not yet be in container.children); other exceptions propagate. Sort failures are logged with chat id and exception type so a future regression that breaks .value loading is observable rather than silent. Verified locally against 74 chats including a stale entry that previously stuck at position 0 — it now lands at the correct rank under the new sort.
2d9b11b to
a9742ca
Compare
mark-at-pieces
approved these changes
Apr 29, 2026
Member
mark-at-pieces
left a comment
There was a problem hiding this comment.
great change looking solid, might ass some basic unit tests;
but thinking updated is the way to go here!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The chat list panel held the newest-first invariant only at first-shot load. Three mutation paths could subsequently desync the visible order from
chat.conversation.updated.value, leaving stale chats stuck at the top of the list.Root causes
add_chatonly sorted whileConversationsSnapshot.first_shotwas True. After first-shot, a chat that appeared new to the widget map (because the panel had never seen it) jumped to position 0 regardless of timestamp.add_chat_at_topblindly prepended every chat passed to it, even when the chat'supdated.valueplaced it elsewhere in the order.update_chatmutated title/summary in place but never re-positioned, so a chat whoseupdated.valuejust bumped (new message arrival) did not bubble to the top.Fix
Centralise the ordering rule via two helpers (
_chat_sort_keyand its tuple variant) returning(rank, value)so chats with usable timestamps sort by datetime, while chats whose snapshot fails to load sink to the bottom underreverse=Truewithout raising.load_chatssorts the incoming list before mounting (newest-first ordering even whencopilot.chats()returns identifiers in arrival order).add_chatalways sorts via_sort_items_by_updated. The existing_reorder_pendingdebounce moves into_schedule_reorderand continues to coalesce reorder passes during first-shot drain (avoids O(n²) when hundreds of WS-streamed chats arrive in one refresh cycle). Post-first-shot, events are sparse — reorder eagerly.add_chat_at_topbecomes a thin shim overadd_chat. A genuinely new chat lands at the top via its newest timestamp; a chat that merely appeared new to the widget map falls into its correct slot.update_chatresorts and reorders so a bumpedupdated.valuebubbles the chat in the DOM to match the data invariant.base_list_panel._reorder_item_widgetsnarrows its bare-Exception handler toNoMatches(the only legitimately swallowable exception here — Textual mount is async); other exceptions propagate.Sort failures are logged with chat id and exception type so a future regression that breaks
.valueloading is observable rather than silent.Test plan
_chat_sort_key_tuplereferenced before definition was the prior crash mode)._reorder_pendingflag preserved).