From a6b3bec29b371a4095491b5a352302714d124dd8 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Thu, 28 May 2026 09:34:30 +0800 Subject: [PATCH] Add virtualized chat pagination (#1080) --- packages/client/src/api/hermes/chat.ts | 4 + packages/client/src/api/hermes/group-chat.ts | 11 +- packages/client/src/api/hermes/sessions.ts | 29 ++ .../src/components/hermes/chat/ChatInput.vue | 79 +++-- .../hermes/chat/HistoryMessageList.vue | 71 ++--- .../components/hermes/chat/MessageList.vue | 125 ++++---- .../hermes/chat/VirtualMessageList.vue | 282 ++++++++++++++++++ .../hermes/group-chat/GroupMessageList.vue | 100 +++++-- packages/client/src/stores/hermes/chat.ts | 63 +++- .../client/src/stores/hermes/group-chat.ts | 55 ++++ .../server/src/db/hermes/session-store.ts | 2 +- .../server/src/routes/hermes/group-chat.ts | 7 +- .../src/services/hermes/group-chat/index.ts | 13 +- .../hermes/run-chat/handle-api-run.ts | 4 + .../src/services/hermes/run-chat/index.ts | 4 + .../src/services/hermes/run-chat/types.ts | 4 + 16 files changed, 692 insertions(+), 161 deletions(-) create mode 100644 packages/client/src/components/hermes/chat/VirtualMessageList.vue diff --git a/packages/client/src/api/hermes/chat.ts b/packages/client/src/api/hermes/chat.ts index b4f08b9..a5e0d1b 100644 --- a/packages/client/src/api/hermes/chat.ts +++ b/packages/client/src/api/hermes/chat.ts @@ -75,6 +75,10 @@ export interface RunEvent { export interface ResumeSessionPayload { session_id: string messages: any[] + messageTotal?: number + messageLoadedCount?: number + messagePageLimit?: number + hasMoreBefore?: boolean isWorking: boolean isAborting?: boolean events: Array<{ event: string; data: RunEvent }> diff --git a/packages/client/src/api/hermes/group-chat.ts b/packages/client/src/api/hermes/group-chat.ts index 7b427c3..74ecaea 100644 --- a/packages/client/src/api/hermes/group-chat.ts +++ b/packages/client/src/api/hermes/group-chat.ts @@ -162,8 +162,15 @@ export async function listRooms(): Promise<{ rooms: RoomInfo[] }> { return request('/api/hermes/group-chat/rooms') } -export async function getRoomDetail(roomId: string): Promise<{ room: RoomInfo; messages: ChatMessage[]; agents: RoomAgent[]; members: MemberInfo[] }> { - return request(`/api/hermes/group-chat/rooms/${roomId}`) +export async function getRoomDetail( + roomId: string, + options: { offset?: number; limit?: number } = {}, +): Promise<{ room: RoomInfo; messages: ChatMessage[]; agents: RoomAgent[]; members: MemberInfo[]; total?: number; offset?: number; limit?: number; hasMore?: boolean }> { + const params = new URLSearchParams() + if (options.offset != null) params.set('offset', String(options.offset)) + if (options.limit != null) params.set('limit', String(options.limit)) + const query = params.toString() + return request(`/api/hermes/group-chat/rooms/${roomId}${query ? `?${query}` : ''}`) } export async function joinRoomByCode(code: string): Promise<{ room: RoomInfo }> { diff --git a/packages/client/src/api/hermes/sessions.ts b/packages/client/src/api/hermes/sessions.ts index 88374fc..66841a0 100644 --- a/packages/client/src/api/hermes/sessions.ts +++ b/packages/client/src/api/hermes/sessions.ts @@ -30,6 +30,15 @@ export interface SessionDetail extends SessionSummary { messages: HermesMessage[] } +export interface PaginatedSessionMessages { + session: SessionSummary + messages: HermesMessage[] + total: number + offset: number + limit: number + hasMore: boolean +} + export interface SessionSearchResult extends SessionSummary { matched_message_id: number | null snippet: string @@ -96,6 +105,26 @@ export async function fetchSession(id: string, profile?: string | null): Promise } } +export async function fetchSessionMessagesPage( + id: string, + offset: number, + limit = 300, + profile?: string | null, +): Promise { + try { + const params = new URLSearchParams() + params.set('offset', String(offset)) + params.set('limit', String(limit)) + if (profile) params.set('profile', profile) + const res = await request( + `/api/hermes/sessions/conversations/${encodeURIComponent(id)}/messages/paginated?${params}`, + ) + return res + } catch { + return null + } +} + /** * Fetch Hermes session detail only (exclude api_server source) */ diff --git a/packages/client/src/components/hermes/chat/ChatInput.vue b/packages/client/src/components/hermes/chat/ChatInput.vue index ee6a266..c863860 100644 --- a/packages/client/src/components/hermes/chat/ChatInput.vue +++ b/packages/client/src/components/hermes/chat/ChatInput.vue @@ -11,6 +11,8 @@ import { useI18n } from 'vue-i18n' import { useToolTraceVisibility } from '@/composables/useToolTraceVisibility' const chatStore = useChatStore() +const appStore = useAppStore() +const profilesStore = useProfilesStore() const { t } = useI18n() const message = useMessage() const { toolTraceVisible, toggleToolTraceVisible } = useToolTraceVisibility() @@ -150,6 +152,9 @@ function selectBridgeCommand(command: { name: string; args: string; insertText?: const contextLength = ref(256000) const FALLBACK_CONTEXT = 256000 +let contextLengthLoadedKey = '' +let contextLengthRequestKey = '' +let contextLengthRequest: Promise | null = null // Context length editing const showContextEditModal = ref(false) @@ -169,8 +174,8 @@ async function saveContextLimit() { isSavingContextLimit.value = true try { - const provider = chatStore.activeSession?.provider || useAppStore().selectedProvider || '' - const model = chatStore.activeSession?.model || useAppStore().selectedModel || '' + const provider = chatStore.activeSession?.provider || appStore.selectedProvider || '' + const model = chatStore.activeSession?.model || appStore.selectedModel || '' if (!provider || !model) { message.error(t('chat.contextEditFailed')) @@ -179,6 +184,7 @@ async function saveContextLimit() { await setModelContext(provider, model, editingContextLimit.value) contextLength.value = editingContextLimit.value + contextLengthLoadedKey = currentContextLengthKey() showContextEditModal.value = false message.success(t('chat.contextEditSuccess')) } catch (err: any) { @@ -188,28 +194,61 @@ async function saveContextLimit() { } } -async function loadContextLength() { - try { - const activeSession = chatStore.activeSession - const profile = activeSession?.profile || useProfilesStore().activeProfileName || undefined - contextLength.value = await fetchContextLength( - profile, - activeSession?.provider || undefined, - activeSession?.model || undefined, - ) - } catch { - contextLength.value = FALLBACK_CONTEXT +function currentContextLengthParams() { + const activeSession = chatStore.activeSession + return { + profile: activeSession?.profile || profilesStore.activeProfileName || undefined, + provider: activeSession?.provider || undefined, + model: activeSession?.model || undefined, } } +function currentContextLengthKey() { + const params = currentContextLengthParams() + return `${params.profile || ''}|${params.provider || ''}|${params.model || ''}` +} + +async function loadContextLength() { + const key = currentContextLengthKey() + if (key === contextLengthLoadedKey) return + if (key === contextLengthRequestKey && contextLengthRequest) return contextLengthRequest + + contextLengthRequestKey = key + contextLengthRequest = (async () => { + const params = currentContextLengthParams() + try { + const value = await fetchContextLength(params.profile, params.provider, params.model) + if (currentContextLengthKey() !== key) return + contextLength.value = value + contextLengthLoadedKey = key + } catch { + if (currentContextLengthKey() !== key) return + contextLength.value = FALLBACK_CONTEXT + contextLengthLoadedKey = key + } finally { + if (contextLengthRequestKey === key) { + contextLengthRequest = null + contextLengthRequestKey = '' + } + } + })() + return contextLengthRequest +} + onMounted(loadContextLength) -watch(() => useProfilesStore().activeProfileName, loadContextLength) -watch(() => useAppStore().selectedProvider, loadContextLength) -watch(() => useAppStore().selectedModel, loadContextLength) -watch(() => chatStore.activeSession?.id, loadContextLength) -watch(() => chatStore.activeSession?.profile, loadContextLength) -watch(() => chatStore.activeSession?.provider, loadContextLength) -watch(() => chatStore.activeSession?.model, loadContextLength) +watch( + () => [ + profilesStore.activeProfileName, + appStore.selectedProvider, + appStore.selectedModel, + chatStore.activeSession?.id, + chatStore.activeSession?.profile, + chatStore.activeSession?.provider, + chatStore.activeSession?.model, + ], + loadContextLength, + { flush: 'post' }, +) const totalTokens = computed(() => { const context = chatStore.activeSession?.contextTokens diff --git a/packages/client/src/components/hermes/chat/HistoryMessageList.vue b/packages/client/src/components/hermes/chat/HistoryMessageList.vue index e7f9ca9..bfc18d7 100644 --- a/packages/client/src/components/hermes/chat/HistoryMessageList.vue +++ b/packages/client/src/components/hermes/chat/HistoryMessageList.vue @@ -1,6 +1,7 @@ diff --git a/packages/client/src/components/hermes/group-chat/GroupMessageList.vue b/packages/client/src/components/hermes/group-chat/GroupMessageList.vue index d8c04b1..36f88ac 100644 --- a/packages/client/src/components/hermes/group-chat/GroupMessageList.vue +++ b/packages/client/src/components/hermes/group-chat/GroupMessageList.vue @@ -4,27 +4,30 @@ import { useI18n } from 'vue-i18n' import { useGroupChatStore } from '@/stores/hermes/group-chat' import { useToolTraceVisibility } from '@/composables/useToolTraceVisibility' import GroupMessageItem from './GroupMessageItem.vue' +import VirtualMessageList from '../chat/VirtualMessageList.vue' const store = useGroupChatStore() const { t } = useI18n() const { toolTraceVisible } = useToolTraceVisibility() -const listRef = ref() +const listRef = ref | null>(null) const isNearBottom = ref(true) const displayMessages = computed(() => store.sortedMessages.filter(msg => msg.role !== 'tool' || toolTraceVisible.value || msg.toolStatus === 'running')) function checkNearBottom(): void { - if (!listRef.value) return - const { scrollTop, scrollHeight, clientHeight } = listRef.value - isNearBottom.value = scrollHeight - scrollTop - clientHeight < 200 + isNearBottom.value = listRef.value?.isNearBottom(200) ?? true } function scrollToBottom(): void { - if (!listRef.value) return - listRef.value.scrollTop = listRef.value.scrollHeight + listRef.value?.scrollToBottom() } -function handleScroll(): void { - checkNearBottom() +async function handleTopReach(): Promise { + if (!store.hasMoreBefore || store.isLoadingOlderMessages) return + const snapshot = listRef.value?.captureScrollPosition() ?? null + const loaded = await store.loadOlderMessages() + if (!loaded) return + await nextTick() + listRef.value?.restoreScrollPosition(snapshot) } watch(() => store.messages.length, async () => { @@ -38,39 +41,42 @@ defineExpose({ scrollToBottom }) diff --git a/packages/client/src/stores/hermes/chat.ts b/packages/client/src/stores/hermes/chat.ts index 36c3c14..8cce8a3 100644 --- a/packages/client/src/stores/hermes/chat.ts +++ b/packages/client/src/stores/hermes/chat.ts @@ -1,5 +1,5 @@ import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, respondToolApproval, onPeerUserMessage, onSessionCommand, respondClarify, type RunEvent, type ResumeSessionPayload, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat' -import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, setSessionModel, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions' +import { deleteSession as deleteSessionApi, fetchSessionMessagesPage, fetchSessions, setSessionModel, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions' import { getActiveProfileName } from '@/api/client' import { getDownloadUrl } from '@/api/hermes/download' import { defineStore } from 'pinia' @@ -77,6 +77,10 @@ export interface Session { model?: string provider?: string messageCount?: number + messageTotal?: number + loadedMessageCount?: number + hasMoreBefore?: boolean + isLoadingOlderMessages?: boolean inputTokens?: number outputTokens?: number contextTokens?: number @@ -281,6 +285,9 @@ function mapHermesSession(s: SessionSummary): Session { model: s.model, provider: s.provider || (s as any).billing_provider || '', messageCount: s.message_count, + messageTotal: s.message_count, + loadedMessageCount: 0, + hasMoreBefore: false, inputTokens: s.input_tokens, outputTokens: s.output_tokens, endedAt: s.ended_at != null ? Math.round(s.ended_at * 1000) : null, @@ -511,13 +518,18 @@ export const useChatStore = defineStore('chat', () => { const sid = activeSessionId.value if (!sid) return false try { - const detail = await fetchSession(sid, activeSession.value?.profile) - if (!detail) return false const target = sessions.value.find(s => s.id === sid) if (!target) return false + const limit = Math.max(target.loadedMessageCount || 300, 300) + const detail = await fetchSessionMessagesPage(sid, 0, limit, activeSession.value?.profile) + if (!detail) return false const mapped = mapHermesMessages(detail.messages || []) target.messages = mapped - if (detail.title) target.title = detail.title + target.loadedMessageCount = detail.messages.length + target.messageTotal = detail.total + target.messageCount = detail.total + target.hasMoreBefore = detail.hasMore + if (detail.session.title) target.title = detail.session.title return true } catch (err) { console.error('Failed to refresh active session:', err) @@ -620,6 +632,10 @@ export const useChatStore = defineStore('chat', () => { if ((data as any).contextTokens != null) target.contextTokens = (data as any).contextTokens if (data.messages?.length) { target.messages = mapHermesMessages(data.messages as any[]) + target.loadedMessageCount = data.messageLoadedCount ?? data.messages.length + target.messageTotal = data.messageTotal ?? target.messageCount ?? target.loadedMessageCount + target.messageCount = target.messageTotal + target.hasMoreBefore = data.hasMoreBefore ?? target.loadedMessageCount < target.messageTotal } if (!target.title) { const firstUser = target.messages.find(m => m.role === 'user') @@ -728,6 +744,36 @@ export const useChatStore = defineStore('chat', () => { } } + async function loadOlderMessages(sessionId = activeSessionId.value): Promise { + if (!sessionId) return false + const target = sessions.value.find(s => s.id === sessionId) + if (!target || target.isLoadingOlderMessages || !target.hasMoreBefore) return false + const offset = target.loadedMessageCount || 0 + const limit = 300 + target.isLoadingOlderMessages = true + try { + const page = await fetchSessionMessagesPage(sessionId, offset, limit, target.profile) + if (!page || page.messages.length === 0) { + target.hasMoreBefore = false + return false + } + + const existingIds = new Set(target.messages.map(message => message.id)) + const olderMessages = mapHermesMessages(page.messages).filter(message => !existingIds.has(message.id)) + target.messages = [...olderMessages, ...target.messages] + target.loadedMessageCount = offset + page.messages.length + target.messageTotal = page.total + target.messageCount = page.total + target.hasMoreBefore = page.hasMore + return olderMessages.length > 0 + } catch (err) { + console.error('Failed to load older session messages:', err) + return false + } finally { + target.isLoadingOlderMessages = false + } + } + function newChat(options: { profile?: string; model?: string; provider?: string } = {}): Session { const appStore = useAppStore() const session = createSession({ @@ -1438,6 +1484,10 @@ export const useChatStore = defineStore('chat', () => { if (Array.isArray(data.messages)) { target.messages = mapHermesMessages(data.messages as any[]) + target.loadedMessageCount = data.messageLoadedCount ?? data.messages.length + target.messageTotal = data.messageTotal ?? target.messageCount ?? target.loadedMessageCount + target.messageCount = target.messageTotal + target.hasMoreBefore = data.hasMoreBefore ?? target.loadedMessageCount < target.messageTotal const lastAssistant = [...target.messages].reverse().find(m => m.role === 'assistant') if (data.isWorking && lastAssistant) { lastAssistant.isStreaming = true @@ -2518,6 +2568,10 @@ export const useChatStore = defineStore('chat', () => { if (!data.isWorking) setCompressionState(sid, null) if (data.messages?.length && activeSession.value) { activeSession.value.messages = mapHermesMessages(data.messages as any[]) + activeSession.value.loadedMessageCount = data.messageLoadedCount ?? data.messages.length + activeSession.value.messageTotal = data.messageTotal ?? activeSession.value.messageCount ?? activeSession.value.loadedMessageCount + activeSession.value.messageCount = activeSession.value.messageTotal + activeSession.value.hasMoreBefore = data.hasMoreBefore ?? activeSession.value.loadedMessageCount < activeSession.value.messageTotal } resumeServerWorkingRun(sid) }, activeSession.value?.profile) @@ -2618,6 +2672,7 @@ export const useChatStore = defineStore('chat', () => { newChat, newCliSession, switchSession, + loadOlderMessages, switchSessionModel, addOrUpdateSession, clearProviderFromSessions, diff --git a/packages/client/src/stores/hermes/group-chat.ts b/packages/client/src/stores/hermes/group-chat.ts index e43c9b7..e005b98 100644 --- a/packages/client/src/stores/hermes/group-chat.ts +++ b/packages/client/src/stores/hermes/group-chat.ts @@ -125,6 +125,23 @@ export const useGroupChatStore = defineStore('groupChat', () => { const contextStatuses = ref>(new Map()) const autoPlaySpeechEnabled = ref(false) const pendingApprovals = ref>(new Map()) + const totalMessages = ref(0) + const loadedMessageCount = ref(0) + const hasMoreBefore = ref(false) + const isLoadingOlderMessages = ref(false) + + function resetMessagePaging() { + totalMessages.value = 0 + loadedMessageCount.value = 0 + hasMoreBefore.value = false + isLoadingOlderMessages.value = false + } + + function applyMessagePaging(res: { messages: ChatMessage[]; total?: number; hasMore?: boolean }) { + loadedMessageCount.value = res.messages.length + totalMessages.value = res.total ?? res.messages.length + hasMoreBefore.value = res.hasMore ?? loadedMessageCount.value < totalMessages.value + } function setAutoPlaySpeech(enabled: boolean) { autoPlaySpeechEnabled.value = enabled @@ -232,6 +249,8 @@ export const useGroupChatStore = defineStore('groupChat', () => { messages.value = [...messages.value] } else { messages.value.push(resolvedMsg) + loadedMessageCount.value += 1 + totalMessages.value = Math.max(totalMessages.value + 1, loadedMessageCount.value) } if (autoPlaySpeechEnabled.value && resolvedMsg.role === 'assistant' && resolvedMsg.content?.trim()) { setTimeout(() => playMessageSpeech(resolvedMsg.id, resolvedMsg.content), 300) @@ -266,6 +285,8 @@ export const useGroupChatStore = defineStore('groupChat', () => { messages.value = [...messages.value] } else { messages.value.push(msg) + loadedMessageCount.value += 1 + totalMessages.value = Math.max(totalMessages.value + 1, loadedMessageCount.value) } }) @@ -400,6 +421,7 @@ export const useGroupChatStore = defineStore('groupChat', () => { if (room) room.totalTokens = data.totalTokens if (data.roomId === currentRoomId.value) { messages.value = [] + resetMessagePaging() typingUsers.value.clear() contextStatuses.value.clear() pendingApprovals.value.clear() @@ -412,6 +434,7 @@ export const useGroupChatStore = defineStore('groupChat', () => { connected.value = false currentRoomId.value = null messages.value = [] + resetMessagePaging() members.value = [] agents.value = [] roomName.value = '' @@ -436,6 +459,7 @@ export const useGroupChatStore = defineStore('groupChat', () => { currentRoomId.value = res.room.id roomName.value = res.room.name messages.value = res.messages + applyMessagePaging(res) agents.value = res.agents members.value = res.members || [] } catch (err: any) { @@ -481,6 +505,28 @@ export const useGroupChatStore = defineStore('groupChat', () => { } } + async function loadOlderMessages(): Promise { + const roomId = currentRoomId.value + if (!roomId || isLoadingOlderMessages.value || !hasMoreBefore.value) return false + const offset = loadedMessageCount.value + isLoadingOlderMessages.value = true + try { + const res = await getRoomDetail(roomId, { offset, limit: 300 }) + const existingIds = new Set(messages.value.map(message => message.id)) + const olderMessages = res.messages.filter(message => !existingIds.has(message.id)) + messages.value = [...olderMessages, ...messages.value] + loadedMessageCount.value = offset + res.messages.length + totalMessages.value = res.total ?? totalMessages.value + hasMoreBefore.value = res.hasMore ?? loadedMessageCount.value < totalMessages.value + return olderMessages.length > 0 + } catch (err: any) { + error.value = err.message + return false + } finally { + isLoadingOlderMessages.value = false + } + } + async function sendMessage(content: string, attachments?: Attachment[]) { const socket = getSocket() if (!socket || !currentRoomId.value) return @@ -503,6 +549,8 @@ export const useGroupChatStore = defineStore('groupChat', () => { role: 'user', attachments: attachments.map(att => ({ ...att, url: urlMap.get(att.name) || att.url, file: undefined })), }) + loadedMessageCount.value += 1 + totalMessages.value = Math.max(totalMessages.value + 1, loadedMessageCount.value) } return new Promise((resolve, reject) => { @@ -560,6 +608,7 @@ export const useGroupChatStore = defineStore('groupChat', () => { if (currentRoomId.value === roomId) { currentRoomId.value = null messages.value = [] + resetMessagePaging() members.value = [] agents.value = [] roomName.value = '' @@ -586,6 +635,7 @@ export const useGroupChatStore = defineStore('groupChat', () => { try { const res = await clearRoomContext(currentRoomId.value) messages.value = [] + resetMessagePaging() typingUsers.value.clear() contextStatuses.value.clear() const idx = rooms.value.findIndex(r => r.id === currentRoomId.value) @@ -690,6 +740,10 @@ export const useGroupChatStore = defineStore('groupChat', () => { pendingApprovals, activePendingApproval, autoPlaySpeechEnabled, + totalMessages, + loadedMessageCount, + hasMoreBefore, + isLoadingOlderMessages, userId, userName, // Computed @@ -703,6 +757,7 @@ export const useGroupChatStore = defineStore('groupChat', () => { setUserInfo, setAutoPlaySpeech, joinRoom, + loadOlderMessages, sendMessage, loadRooms, emitTyping, diff --git a/packages/server/src/db/hermes/session-store.ts b/packages/server/src/db/hermes/session-store.ts index 613b212..26c35a8 100644 --- a/packages/server/src/db/hermes/session-store.ts +++ b/packages/server/src/db/hermes/session-store.ts @@ -452,7 +452,7 @@ export function updateSessionStats(id: string): void { export function getSessionDetailPaginated( id: string, offset = 0, - limit = 500, + limit = 300, ): PaginatedSessionDetailResult | null { if (!isSqliteAvailable()) { return null diff --git a/packages/server/src/routes/hermes/group-chat.ts b/packages/server/src/routes/hermes/group-chat.ts index 83e4893..7d0aa45 100644 --- a/packages/server/src/routes/hermes/group-chat.ts +++ b/packages/server/src/routes/hermes/group-chat.ts @@ -182,10 +182,13 @@ groupChatRoutes.get('/api/hermes/group-chat/rooms/:roomId', async (ctx) => { return } - const messages = chatServer.getStorage().getMessages(ctx.params.roomId) + const offset = ctx.query.offset ? Math.max(0, parseInt(ctx.query.offset as string, 10) || 0) : 0 + const limit = ctx.query.limit ? Math.max(1, parseInt(ctx.query.limit as string, 10) || 300) : 300 + const messages = chatServer.getStorage().getMessages(ctx.params.roomId, limit, offset) + const total = chatServer.getStorage().getMessageCount(ctx.params.roomId) const agents = chatServer.getStorage().getRoomAgents(ctx.params.roomId) const members = chatServer.getStorage().getRoomMembers(ctx.params.roomId) - ctx.body = { room, messages, agents, members } + ctx.body = { room, messages, agents, members, total, offset, limit, hasMore: offset + messages.length < total } }) // List rooms diff --git a/packages/server/src/services/hermes/group-chat/index.ts b/packages/server/src/services/hermes/group-chat/index.ts index bd646d4..4b9108b 100644 --- a/packages/server/src/services/hermes/group-chat/index.ts +++ b/packages/server/src/services/hermes/group-chat/index.ts @@ -390,16 +390,23 @@ class ChatStorage { // ─── Messages ───────────────────────────────────────────── - getMessages(roomId: string, limit = 500): ChatMessage[] { + getMessages(roomId: string, limit = 300, offset = 0): ChatMessage[] { const rows = (this.db()?.prepare( - 'SELECT id, roomId, senderId, senderName, content, timestamp, role, tool_call_id, tool_calls, tool_name, finish_reason, reasoning, reasoning_details, reasoning_content FROM gc_messages WHERE roomId = ? ORDER BY timestamp DESC LIMIT ?' - ).all(roomId, limit) || []) as any[] + 'SELECT id, roomId, senderId, senderName, content, timestamp, role, tool_call_id, tool_calls, tool_name, finish_reason, reasoning, reasoning_details, reasoning_content FROM gc_messages WHERE roomId = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?' + ).all(roomId, limit, offset) || []) as any[] return sortGroupMessages(rows.map(row => ({ ...row, tool_calls: parseJsonArray(row.tool_calls), }))) } + getMessageCount(roomId: string): number { + const row = this.db()?.prepare( + 'SELECT COUNT(*) as total FROM gc_messages WHERE roomId = ?' + ).get(roomId) as { total: number } | undefined + return row?.total || 0 + } + getMessage(messageId: string): ChatMessage | null { const row = this.db()?.prepare( 'SELECT id, roomId, senderId, senderName, content, timestamp, role, tool_call_id, tool_calls, tool_name, finish_reason, reasoning, reasoning_details, reasoning_content FROM gc_messages WHERE id = ?' diff --git a/packages/server/src/services/hermes/run-chat/handle-api-run.ts b/packages/server/src/services/hermes/run-chat/handle-api-run.ts index b04154d..5573001 100644 --- a/packages/server/src/services/hermes/run-chat/handle-api-run.ts +++ b/packages/server/src/services/hermes/run-chat/handle-api-run.ts @@ -68,6 +68,10 @@ export async function loadSessionStateFromDb(sid: string, _sessionMap: Map abortController?: AbortController