Skip to content

ckl - #7358

Closed
vesper-collab wants to merge 7 commits into
agentscope-ai:mainfrom
vesper-collab:cursor/chat-history-pagination-b439
Closed

ckl#7358
vesper-collab wants to merge 7 commits into
agentscope-ai:mainfrom
vesper-collab:cursor/chat-history-pagination-b439

Conversation

@vesper-collab

@vesper-collab vesper-collab commented Aug 27, 2026

Copy link
Copy Markdown

Description

#7049 added optional limit / before pagination on GET /api/chats/{chat_id} so the console does not have to download an entire conversation at once. @zhijianma closed it with Close-and-review-later:

仅添加后端接口,不解决问题, 需要配合等前端支持后再考虑 merge

Backend-only does not fix the freeze / timeout on long chats (#3915, #6635). This PR is the chat-history half of that work: window the stored history, convert only that window, and keep the Chat transcript from mounting every loaded bubble.

Backend (same public contract as #7049, still backward compatible):

  • limit — max most-recent source messages (AgentScope Msg objects), default 0 = full history, ge=0 le=10000
  • before — cursor is the persistent AgentScope Msg.id, exposed after conversion as metadata.original_id. Message.id is a fresh uuid4 every request and is not used as a cursor
  • Response still has total (unwindowed source-message count) and has_more
  • Stale / unknown cursor falls back to a plain limit window
  • Windowing runs on the Msg list before agentscope_msg_to_message. Opening a 540k-token / thousands-of-msgs chat no longer converts the full history into runtime Message objects just to throw most of them away. One Msg can still expand into several Message objects that share original_id; cutting at the Msg layer keeps those groups intact

Console:

  1. Opening or switching a chat requests GET /api/chats/{chat_id}?limit=N (include_app_owned=false). N is a Console-local preference (localStorage qwenpaw_chat_history_page_size), default 50, integer 1–10000 to match the backend limit contract. It is not stored on channels.console display settings (those are outbound IM rendering).
  2. Settings → Console → Chat has a number field labeled Messages loaded when opening a chat. The same N is also a compact input next to Load earlier messages. Empty or non-numeric input is ignored and the last valid value is restored; out-of-range numbers are clamped. Changing N refetches the latest window (no before) and replaces the transcript.
  3. Load earlier messages sends before set to the oldest loaded metadata.original_id, prepends when has_more, and keeps scroll position. Groups sharing original_id are not split. Short chats hide the Load earlier button (has_more=false) but still show the page-size input. Scrolling the transcript to the oldest edge also triggers load-earlier; the button stays as a fallback.
  4. The main transcript is virtualized. @agentscope-ai/chat has no virtual list (its PAGE_SIZE=10 helper still accumulates into a full DOM). The Chat page swaps the vendor MessageList for a host list that keeps the reverse scroller (column-reverse, scrollTop === 0 at the newest edge) and only mounts viewport + overscan rows. Row heights are measured and cached (code blocks, cards, images); there is no fixed row height. New / streaming messages still stick to the bottom; prepending older history does not jump the viewport. Session switch abort, ownership epochs, pending user-message patching, the converted-session LRU, and reverse-list wheel handling are unchanged.
  5. Chat search still fetches without limit / before. A window with has_more is cached for cheap switch-back but is never treated as the full canonical history.

Skills-list gzip from #6635 is not part of this PR.

Related Issue: Relates to #7049, #3915, #6635

Security Considerations: limit and before only slice a history the caller can already read through this endpoint; they do not change ownership or include_app_owned. limit is clamped (ge=0, le=10000). Conversion runs only on the windowed Msg list. The Console page size is a browser-local integer in that same range. before is an opaque Msg.id / metadata.original_id string, not a path or query language; an unknown cursor degrades to a plain limit window instead of erroring. total / has_more are counts of source messages, derived from the same memory list the handler already loaded.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactoring

Component(s) Affected

  • Core / Backend (app, agents, config, providers, utils, local_models)
  • Console (frontend web UI)
  • Channels (DingTalk, Lark, QQ, Discord, iMessage, etc.)
  • Skills
  • CLI
  • Documentation (website)
  • Tests
  • CI/CD
  • Scripts / Deploy

Checklist

  • I ran pre-commit run --all-files locally and it passes
  • If pre-commit auto-fixed files, I committed those changes and reran checks
  • I ran tests locally (pytest or as relevant) and they pass
  • Documentation updated (if needed)
  • Ready for review

For Channel Changes (DingTalk, Lark, QQ, Console, etc.)

  • I ran ./scripts/check-channels.sh (or ./scripts/check-channels.sh --changed) and it passes
  • Contract test exists in tests/contract/channels/test_<channel>_contract.py (REQUIRED)
  • Contract test implements create_instance() with proper channel initialization
  • All 19 contract verification points pass (see tests/contract/channels/__init__.py)
  • Optional: Unit tests in tests/unit/channels/test_<channel>.py for complex internal logic

Testing

Manual:

  1. Open a long chat (more than 50 messages).
  2. The first history request should be GET /api/chats/{chat_id}?limit=50 (plus include_app_owned=false from the Chat surface). The handler should convert only that window, not the full Msg list.
  3. Change the compact number next to Load earlier messages to 200 (or set it under Settings → Console → Chat). Confirm the value persists across reload.
  4. Changing N while the chat is open should refetch ?limit=200 (no before) and replace the transcript with the newest 200 source messages.
  5. Scroll toward the oldest messages (or click Load earlier messages). The next request should send before equal to the oldest loaded source message original_id and limit=200.
  6. Older messages prepend without jumping the viewport. New / streaming messages still stick to the bottom.
  7. Only bubbles near the viewport should be in the DOM (React DevTools / inspector); scrolling recycles rows of uneven height (code, cards, images).
  8. Short chats should not show Load earlier messages (has_more=false) but can still show the page-size input.
  9. Empty / garbage input should restore the last valid N and not send a request.
  10. Chat search should still call getChat without limit / before so matches outside the latest window are found.
  11. Switching sessions should still abort in-flight loads; pending user messages and the LRU cache should behave as before.

Evidence

$ pre-commit run --all-files
check python ast.........................................................Passed
sort simple yaml files...............................(no files to check)Skipped
check yaml...............................................................Passed
check xml................................................................Passed
check toml...............................................................Passed
check docstring is first.................................................Passed
check json...............................................................Passed
fix python encoding pragma...............................................Passed
detect private key.......................................................Passed
trim trailing whitespace.................................................Passed
Add trailing commas......................................................Passed
mypy.....................................................................Passed
black....................................................................Passed
flake8...................................................................Passed
pylint...................................................................Passed
prettier.................................................................Passed
Lint GitHub Actions workflow files.......................................Passed

$ pytest tests/unit/app/chats/test_history_window.py \
    tests/unit/app/chats/test_api_history_pagination.py \
    tests/unit/app/chats/test_api.py \
    tests/unit/app/chats/test_models.py -q
.............................................                            [100%]
45 passed in 0.70s

$ npx vitest run src/api/modules/chat.test.ts \
    src/pages/Chat/sessionApi/historyWindow.test.ts \
    src/pages/Chat/sessionApi/historyPageSize.test.ts \
    src/pages/Chat/tests/historyPagination.test.ts \
    src/pages/Chat/tests/agentSessionOwnership.test.ts \
    src/pages/Chat/components/LoadEarlierMessages/LoadEarlierMessages.test.tsx \
    src/pages/Chat/components/HistoryPageSizeInput/index.test.tsx \
    src/pages/Settings/Console/index.test.tsx \
    src/pages/Chat/tests/pendingUserMessage.test.ts \
    src/pages/Chat/tests/sessionCacheStaleness.test.ts \
    src/pages/Chat/tests/testLargeSession.test.ts \
    src/pages/Chat/ChatPage.coverage.test.tsx \
    src/pages/Chat/virtualMessageList/range.test.ts \
    src/pages/Chat/virtualMessageList/VirtualizedBubbleList.test.tsx \
    src/pages/Chat/messageScroll.test.ts
 Test Files  15 passed (15)
      Tests  210 passed (210)

Additional Notes

Backend design and tests started from #7049 (@zsrmoyanzsr). Current main also has include_app_owned on this endpoint; the pagination params sit next to that. This is meant to answer @zhijianma's review on #7049 rather than re-open that branch as-is.

No website docs change: website/public/docs only lists /api/chats/*. Channel contract checks above are N/A (this is the web Console UI + chats API, not an IM channel). I did not have a 540k-token chat fixture in this environment, so the long-open path is covered by the Msg-layer conversion spy, API pagination tests, and the virtual-list mount/prepend/scroll-to-top tests rather than a live browser pass against a huge session.

@github-project-automation github-project-automation Bot moved this to Todo in QwenPaw Aug 27, 2026
@github-actions github-actions Bot added the first-time-contributor PR created by a first time contributor label Aug 27, 2026
@github-actions

Copy link
Copy Markdown

Welcome to QwenPaw! 🐾

Hi @vesper-collab, thank you for your first Pull Request! 🎉

🙌 Join Developer Community

Thanks so much for your contribution! We'd love to invite you to join the official QwenPaw developer group! You can find the Discord and DingTalk group links under the "Developer Community" section on our docs page:
https://qwenpaw.agentscope.io/docs/community

We truly appreciate your enthusiasm—and look forward to your future contributions! 😊

We'll review your PR soon.


Tip

⭐ If you find QwenPaw useful, please give us a Star!

Star QwenPaw

Staying ahead

Star QwenPaw on GitHub and be instantly notified of new releases.

Your star helps more developers discover this project! 🐾

Port GET /api/chats/{chat_id} limit/before pagination from agentscope-ai#7049 and wire
the console to open chats with the latest 50 messages, plus a load-earlier
control that pages on metadata.original_id.

This is the frontend support @zhijianma asked for before merging agentscope-ai#7049.
@vesper-collab vesper-collab changed the title feat(chats): paginate chat history with console load-earlier feat(chats): paginate chat history and add console load-earlier Aug 27, 2026
@cursor
cursor Bot force-pushed the cursor/chat-history-pagination-b439 branch from cdc7ecf to 72088e4 Compare August 27, 2026 12:46
@cursor
cursor Bot requested a deployment to ai-review-approved August 27, 2026 12:46 Waiting
@cursor
cursor Bot requested a deployment to ai-review-approved August 27, 2026 12:50 Waiting
useSyncExternalStore re-renders forever if getHistoryPage returns a new
object on every read. Point the ChatPage mocks at one reused empty page
and document the same requirement on the real getter.
@cursor
cursor Bot requested a deployment to ai-review-approved August 27, 2026 12:56 Waiting
@cursor
cursor Bot requested a deployment to ai-review-approved August 27, 2026 12:58 Waiting
@vesper-collab
vesper-collab marked this pull request as ready for review August 27, 2026 13:04
Persist N in localStorage (default 50, clamp 1-10000), expose it on the
Console settings Chat section and next to Load earlier, and use it for
open, load-earlier, and live refetch of the latest window.
@cursor
cursor Bot requested a deployment to ai-review-approved August 27, 2026 13:30 Waiting
@vesper-collab vesper-collab changed the title feat(chats): paginate chat history and add console load-earlier feat(chats): paginate chat history with a user-set console page size Aug 27, 2026
…transcript

Slice the AgentScope Msg list (cursor = Msg.id / metadata.original_id) before
agentscope_msg_to_message so opening a long chat converts only the requested
window. Replace the vendor message list with a reverse virtual list so the
Chat page mounts viewport plus overscan, keeps load-earlier scroll, and still
sticks to new/streaming messages.
@cursor
cursor Bot requested a deployment to ai-review-approved August 27, 2026 14:29 Waiting
@vesper-collab vesper-collab changed the title feat(chats): paginate chat history with a user-set console page size feat(chats): paginate long chat history and virtualize the transcript Aug 27, 2026
Stick to newest at scrollTop 0, restore the visible row after prepend or
live-row growth, and put flex gap on the mounted rows so spacers do not
steal spacing. isLast stays on the live edge. Tests cover viewport mount,
prepend, stick-to-newest, onStartReached, and session listener cleanup.
@cursor
cursor Bot requested a deployment to ai-review-approved August 27, 2026 15:10 Waiting
@github-project-automation github-project-automation Bot moved this from Todo to Done in QwenPaw Aug 27, 2026
@vesper-collab vesper-collab changed the title feat(chats): paginate long chat history and virtualize the transcript ckl Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

first-time-contributor PR created by a first time contributor

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant