feat(chat): add message history pagination with scroll-up loading
Return latest messages by default instead of oldest. Support paginated loading of older messages when scrolling up via IntersectionObserver, with scrollHeight compensation to preserve scroll position. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c2b5ada2ef
commit
65c2fea1b6
12 changed files with 232 additions and 28 deletions
2
apps/desktop/electron/electron-env.d.ts
vendored
2
apps/desktop/electron/electron-env.d.ts
vendored
|
|
@ -193,7 +193,7 @@ interface ElectronAPI {
|
|||
localChat: {
|
||||
subscribe: (agentId: string) => Promise<{ ok?: boolean; error?: string; alreadySubscribed?: boolean }>
|
||||
unsubscribe: (agentId: string) => Promise<{ ok: boolean }>
|
||||
getHistory: (agentId: string) => Promise<{ messages: unknown[] }>
|
||||
getHistory: (agentId: string, options?: { offset?: number; limit?: number }) => Promise<{ messages: unknown[]; total: number; offset: number; limit: number }>
|
||||
send: (agentId: string, content: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
resolveExecApproval: (approvalId: string, decision: string) => Promise<{ ok: boolean }>
|
||||
onEvent: (callback: (event: LocalChatEvent) => void) => void
|
||||
|
|
|
|||
|
|
@ -340,23 +340,28 @@ export function registerHubIpcHandlers(): void {
|
|||
})
|
||||
|
||||
/**
|
||||
* Get message history for local chat.
|
||||
* Get message history for local chat with pagination.
|
||||
* Returns raw AgentMessageItem[] so the renderer can render content blocks,
|
||||
* tool results, thinking blocks, etc. — same format as the Gateway RPC.
|
||||
*/
|
||||
ipcMain.handle('localChat:getHistory', async (_event, agentId: string) => {
|
||||
ipcMain.handle('localChat:getHistory', async (_event, agentId: string, options?: { offset?: number; limit?: number }) => {
|
||||
const h = getHub()
|
||||
const agent = h.getAgent(agentId)
|
||||
if (!agent) {
|
||||
return { messages: [] }
|
||||
return { messages: [], total: 0, offset: 0, limit: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
await agent.ensureInitialized()
|
||||
const messages = agent.getMessages()
|
||||
return { messages }
|
||||
const allMessages = agent.getMessages()
|
||||
const total = allMessages.length
|
||||
// Must match DEFAULT_MESSAGES_LIMIT from @multica/sdk/actions/rpc
|
||||
const limit = options?.limit ?? 10
|
||||
const offset = options?.offset ?? Math.max(0, total - limit)
|
||||
const sliced = allMessages.slice(offset, offset + limit)
|
||||
return { messages: sliced, total, offset, limit }
|
||||
} catch {
|
||||
return { messages: [] }
|
||||
return { messages: [], total: 0, offset: 0, limit: 0 }
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -207,8 +207,9 @@ const electronAPI = {
|
|||
subscribe: (agentId: string) => ipcRenderer.invoke('localChat:subscribe', agentId),
|
||||
/** Unsubscribe from agent events */
|
||||
unsubscribe: (agentId: string) => ipcRenderer.invoke('localChat:unsubscribe', agentId),
|
||||
/** Get message history for local chat (returns raw AgentMessageItem[]) */
|
||||
getHistory: (agentId: string) => ipcRenderer.invoke('localChat:getHistory', agentId),
|
||||
/** Get message history for local chat with pagination (returns raw AgentMessageItem[]) */
|
||||
getHistory: (agentId: string, options?: { offset?: number; limit?: number }) =>
|
||||
ipcRenderer.invoke('localChat:getHistory', agentId, options),
|
||||
/** Send message to agent via direct IPC (no Gateway) */
|
||||
send: (agentId: string, content: string) => ipcRenderer.invoke('localChat:send', agentId, content),
|
||||
/** Resolve an exec approval request */
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ export function LocalChat() {
|
|||
streamingIds,
|
||||
isLoading,
|
||||
isLoadingHistory,
|
||||
isLoadingMore,
|
||||
hasMore,
|
||||
error,
|
||||
pendingApprovals,
|
||||
sendMessage,
|
||||
loadMore,
|
||||
resolveApproval,
|
||||
} = useLocalChat()
|
||||
|
||||
|
|
@ -39,9 +42,12 @@ export function LocalChat() {
|
|||
streamingIds={streamingIds}
|
||||
isLoading={isLoading}
|
||||
isLoadingHistory={isLoadingHistory}
|
||||
isLoadingMore={isLoadingMore}
|
||||
hasMore={hasMore}
|
||||
error={error}
|
||||
pendingApprovals={pendingApprovals}
|
||||
sendMessage={sendMessage}
|
||||
loadMore={loadMore}
|
||||
resolveApproval={resolveApproval}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import type {
|
|||
StreamPayload,
|
||||
ExecApprovalRequestPayload,
|
||||
ApprovalDecision,
|
||||
AgentMessageItem,
|
||||
} from '@multica/sdk'
|
||||
import { DEFAULT_MESSAGES_LIMIT } from '@multica/sdk'
|
||||
|
||||
export function useLocalChat() {
|
||||
const chat = useChat()
|
||||
|
|
@ -13,8 +15,11 @@ export function useLocalChat() {
|
|||
const [agentId, setAgentId] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(true)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
const isLoadingMoreRef = useRef(false)
|
||||
const [initError, setInitError] = useState<string | null>(null)
|
||||
const initRef = useRef(false)
|
||||
const offsetRef = useRef<number | null>(null)
|
||||
|
||||
// Initialize hub and get default agent ID
|
||||
useEffect(() => {
|
||||
|
|
@ -61,12 +66,16 @@ export function useLocalChat() {
|
|||
chatRef.current.addApproval(approval as ExecApprovalRequestPayload)
|
||||
})
|
||||
|
||||
// Fetch history
|
||||
window.electronAPI.localChat.getHistory(agentId)
|
||||
// Fetch history with pagination
|
||||
window.electronAPI.localChat.getHistory(agentId, { limit: DEFAULT_MESSAGES_LIMIT })
|
||||
.then((result) => {
|
||||
console.log('[LocalChat] getHistory result:', result.messages?.length, 'messages, sample:', result.messages?.[0])
|
||||
console.log('[LocalChat] getHistory result:', result.messages?.length, 'messages, total:', result.total)
|
||||
if (result.messages?.length) {
|
||||
chatRef.current.setHistory(result.messages as never[], agentId)
|
||||
chatRef.current.setHistory(result.messages as AgentMessageItem[], agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
})
|
||||
offsetRef.current = result.offset
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
|
|
@ -91,6 +100,31 @@ export function useLocalChat() {
|
|||
[agentId],
|
||||
)
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
const currentOffset = offsetRef.current
|
||||
if (!agentId || currentOffset == null || currentOffset <= 0 || isLoadingMoreRef.current) return
|
||||
|
||||
isLoadingMoreRef.current = true
|
||||
setIsLoadingMore(true)
|
||||
try {
|
||||
const newOffset = Math.max(0, currentOffset - DEFAULT_MESSAGES_LIMIT)
|
||||
const limit = currentOffset - newOffset
|
||||
const result = await window.electronAPI.localChat.getHistory(agentId, { offset: newOffset, limit })
|
||||
if (result.messages?.length) {
|
||||
chatRef.current.prependHistory(result.messages as AgentMessageItem[], agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
})
|
||||
offsetRef.current = result.offset
|
||||
}
|
||||
} catch {
|
||||
// Best-effort — pagination failure does not block chat
|
||||
} finally {
|
||||
isLoadingMoreRef.current = false
|
||||
setIsLoadingMore(false)
|
||||
}
|
||||
}, [agentId])
|
||||
|
||||
const resolveApproval = useCallback(
|
||||
(approvalId: string, decision: ApprovalDecision) => {
|
||||
chatRef.current.removeApproval(approvalId)
|
||||
|
|
@ -106,9 +140,12 @@ export function useLocalChat() {
|
|||
streamingIds: chat.streamingIds,
|
||||
isLoading,
|
||||
isLoadingHistory,
|
||||
isLoadingMore,
|
||||
hasMore: chat.hasMore,
|
||||
error: chat.error,
|
||||
pendingApprovals: chat.pendingApprovals,
|
||||
sendMessage,
|
||||
loadMore,
|
||||
resolveApproval,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,12 @@ export function useChat() {
|
|||
const [streamingIds, setStreamingIds] = useState<Set<string>>(new Set());
|
||||
const [pendingApprovals, setPendingApprovals] = useState<PendingApproval[]>([]);
|
||||
const [error, setError] = useState<ChatError | null>(null);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
|
||||
const isStreaming = streamingIds.size > 0;
|
||||
|
||||
/** Load history: convert raw AgentMessageItem[] → Message[] */
|
||||
const setHistory = useCallback((raw: AgentMessageItem[], agentId: string) => {
|
||||
/** Convert raw AgentMessageItem[] → Message[] */
|
||||
const convertMessages = useCallback((raw: AgentMessageItem[], agentId: string): Message[] => {
|
||||
const toolCallArgsMap = new Map<string, { name: string; args: Record<string, unknown> }>();
|
||||
for (const m of raw) {
|
||||
if (m.role === "assistant") {
|
||||
|
|
@ -101,10 +102,25 @@ export function useChat() {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
setMessages(loaded);
|
||||
return loaded;
|
||||
}, []);
|
||||
|
||||
/** Load initial history (replaces all messages) */
|
||||
const setHistory = useCallback((raw: AgentMessageItem[], agentId: string, meta?: { total: number; offset: number }) => {
|
||||
const loaded = convertMessages(raw, agentId);
|
||||
setMessages(loaded);
|
||||
if (meta) {
|
||||
setHasMore(meta.offset > 0);
|
||||
}
|
||||
}, [convertMessages]);
|
||||
|
||||
/** Prepend older messages (for "load more" pagination) */
|
||||
const prependHistory = useCallback((raw: AgentMessageItem[], agentId: string, meta: { total: number; offset: number }) => {
|
||||
const older = convertMessages(raw, agentId);
|
||||
setMessages((prev) => [...older, ...prev]);
|
||||
setHasMore(meta.offset > 0);
|
||||
}, [convertMessages]);
|
||||
|
||||
/** Add a user message */
|
||||
const addUserMessage = useCallback((text: string, agentId: string) => {
|
||||
setMessages((prev) => [
|
||||
|
|
@ -217,11 +233,13 @@ export function useChat() {
|
|||
messages,
|
||||
streamingIds,
|
||||
isStreaming,
|
||||
hasMore,
|
||||
pendingApprovals,
|
||||
error,
|
||||
// State control (for transport layer to call)
|
||||
setError,
|
||||
setHistory,
|
||||
prependHistory,
|
||||
addUserMessage,
|
||||
handleStream,
|
||||
addApproval,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import {
|
||||
type GatewayClient,
|
||||
type StreamPayload,
|
||||
type GetAgentMessagesResult,
|
||||
type ExecApprovalRequestPayload,
|
||||
type ApprovalDecision,
|
||||
DEFAULT_MESSAGES_LIMIT,
|
||||
StreamAction,
|
||||
ExecApprovalRequestAction,
|
||||
} from "@multica/sdk";
|
||||
|
|
@ -22,12 +23,24 @@ export function useGatewayChat({ client, hubId, agentId }: UseGatewayChatOptions
|
|||
const chat = useChat();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(true);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const isLoadingMoreRef = useRef(false);
|
||||
const offsetRef = useRef<number | null>(null);
|
||||
|
||||
// Fetch history
|
||||
// Fetch latest messages on mount
|
||||
useEffect(() => {
|
||||
client
|
||||
.request<GetAgentMessagesResult>(hubId, "getAgentMessages", { agentId, limit: 200 })
|
||||
.then((result) => chat.setHistory(result.messages, agentId))
|
||||
.request<GetAgentMessagesResult>(hubId, "getAgentMessages", {
|
||||
agentId,
|
||||
limit: DEFAULT_MESSAGES_LIMIT,
|
||||
})
|
||||
.then((result) => {
|
||||
chat.setHistory(result.messages, agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
});
|
||||
offsetRef.current = result.offset;
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsLoadingHistory(false));
|
||||
}, [client, hubId, agentId]);
|
||||
|
|
@ -66,6 +79,31 @@ export function useGatewayChat({ client, hubId, agentId }: UseGatewayChatOptions
|
|||
[client, hubId, agentId],
|
||||
);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
const currentOffset = offsetRef.current;
|
||||
if (currentOffset == null || currentOffset <= 0 || isLoadingMoreRef.current) return;
|
||||
|
||||
isLoadingMoreRef.current = true;
|
||||
setIsLoadingMore(true);
|
||||
try {
|
||||
const newOffset = Math.max(0, currentOffset - DEFAULT_MESSAGES_LIMIT);
|
||||
const limit = currentOffset - newOffset;
|
||||
const result = await client.request<GetAgentMessagesResult>(
|
||||
hubId, "getAgentMessages", { agentId, offset: newOffset, limit },
|
||||
);
|
||||
chat.prependHistory(result.messages, agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
});
|
||||
offsetRef.current = result.offset;
|
||||
} catch {
|
||||
// Best-effort — pagination failure does not block chat
|
||||
} finally {
|
||||
isLoadingMoreRef.current = false;
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
}, [client, hubId, agentId]);
|
||||
|
||||
const resolveApproval = useCallback(
|
||||
(approvalId: string, decision: ApprovalDecision) => {
|
||||
chat.removeApproval(approvalId);
|
||||
|
|
@ -79,9 +117,12 @@ export function useGatewayChat({ client, hubId, agentId }: UseGatewayChatOptions
|
|||
streamingIds: chat.streamingIds,
|
||||
isLoading,
|
||||
isLoadingHistory,
|
||||
isLoadingMore,
|
||||
hasMore: chat.hasMore,
|
||||
error: chat.error,
|
||||
pendingApprovals: chat.pendingApprovals,
|
||||
sendMessage,
|
||||
loadMore,
|
||||
resolveApproval,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export {
|
|||
isResponseSuccess,
|
||||
isResponseError,
|
||||
type AgentMessageItem,
|
||||
DEFAULT_MESSAGES_LIMIT,
|
||||
type GetAgentMessagesParams,
|
||||
type GetAgentMessagesResult,
|
||||
type GetHubInfoResult,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ export function isResponseError(
|
|||
|
||||
// ============ RPC Method Types ============
|
||||
|
||||
/** Default number of messages returned per page */
|
||||
export const DEFAULT_MESSAGES_LIMIT = 10;
|
||||
|
||||
/** getAgentMessages - request params */
|
||||
export interface GetAgentMessagesParams {
|
||||
agentId: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useRef, useEffect, useCallback } from "react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { Skeleton } from "@multica/ui/components/ui/skeleton";
|
||||
import { ChatInput } from "@multica/ui/components/chat-input";
|
||||
|
|
@ -30,9 +30,12 @@ export interface ChatViewProps {
|
|||
streamingIds: Set<string>;
|
||||
isLoading: boolean;
|
||||
isLoadingHistory: boolean;
|
||||
isLoadingMore?: boolean;
|
||||
hasMore?: boolean;
|
||||
error: ChatViewError | null;
|
||||
pendingApprovals: ChatViewApproval[];
|
||||
sendMessage: (text: string) => void;
|
||||
loadMore?: () => void;
|
||||
resolveApproval: (approvalId: string, decision: "allow-once" | "allow-always" | "deny") => void;
|
||||
onDisconnect?: () => void;
|
||||
}
|
||||
|
|
@ -42,15 +45,76 @@ export function ChatView({
|
|||
streamingIds,
|
||||
isLoading,
|
||||
isLoadingHistory,
|
||||
isLoadingMore = false,
|
||||
hasMore = false,
|
||||
error,
|
||||
pendingApprovals,
|
||||
sendMessage,
|
||||
loadMore,
|
||||
resolveApproval,
|
||||
onDisconnect,
|
||||
}: ChatViewProps) {
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
const fadeStyle = useScrollFade(mainRef);
|
||||
useAutoScroll(mainRef);
|
||||
const { suppressAutoScroll } = useAutoScroll(mainRef);
|
||||
|
||||
// scrollHeight compensation for prepended messages
|
||||
const prevScrollHeightRef = useRef(0);
|
||||
const isPrependingRef = useRef(false);
|
||||
const unlockRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// Snapshot scrollHeight before prepend render
|
||||
const onLoadMore = useCallback(() => {
|
||||
if (!loadMore || !mainRef.current) return;
|
||||
const el = mainRef.current;
|
||||
prevScrollHeightRef.current = el.scrollHeight;
|
||||
isPrependingRef.current = true;
|
||||
// Lock auto-scroll during prepend
|
||||
unlockRef.current = suppressAutoScroll();
|
||||
loadMore();
|
||||
}, [loadMore, suppressAutoScroll]);
|
||||
|
||||
// After messages change, compensate scroll position if we just prepended
|
||||
useEffect(() => {
|
||||
const el = mainRef.current;
|
||||
if (!el || !isPrependingRef.current) return;
|
||||
|
||||
isPrependingRef.current = false;
|
||||
|
||||
// Double-rAF ensures DOM layout is complete before compensating
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const newScrollHeight = el.scrollHeight;
|
||||
const heightDiff = newScrollHeight - prevScrollHeightRef.current;
|
||||
if (heightDiff > 0) {
|
||||
el.scrollTop = el.scrollTop + heightDiff;
|
||||
}
|
||||
// Release auto-scroll lock after position is restored
|
||||
unlockRef.current?.();
|
||||
unlockRef.current = null;
|
||||
});
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
// IntersectionObserver to trigger loadMore when sentinel is visible
|
||||
// Skip during initial history load to avoid premature triggering
|
||||
useEffect(() => {
|
||||
const sentinel = sentinelRef.current;
|
||||
if (!sentinel || isLoadingHistory) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && hasMore && !isLoadingMore) {
|
||||
onLoadMore();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "100px" },
|
||||
);
|
||||
|
||||
observer.observe(sentinel);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, isLoadingMore, isLoadingHistory, onLoadMore]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
|
|
@ -122,6 +186,15 @@ export function ChatView({
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Sentinel element for IntersectionObserver load-more trigger */}
|
||||
<div ref={sentinelRef} className="h-px shrink-0" />
|
||||
{isLoadingMore && (
|
||||
<div className="flex justify-center py-3">
|
||||
<div className="text-xs text-muted-foreground animate-pulse">
|
||||
Loading older messages...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<MessageList messages={messages} streamingIds={streamingIds} />
|
||||
{pendingApprovals.length > 0 && (
|
||||
<div className="relative px-4 max-w-4xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { type RefObject, useEffect, useRef } from "react"
|
||||
import { type RefObject, useEffect, useRef, useCallback } from "react"
|
||||
|
||||
/**
|
||||
* Auto-scrolls a scroll container to the bottom when its inner content grows,
|
||||
* as long as the user hasn't scrolled up to read older content.
|
||||
*
|
||||
* Observes child element size changes via ResizeObserver on all children,
|
||||
* plus MutationObserver for added/removed nodes. Works for new messages,
|
||||
* history loads, streaming updates, and image loads.
|
||||
* Returns a `lockRef` that can be set to `true` to temporarily suppress
|
||||
* auto-scroll (e.g. during history prepend operations).
|
||||
*/
|
||||
export function useAutoScroll(ref: RefObject<HTMLElement | null>) {
|
||||
const stickRef = useRef(true)
|
||||
const lockRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
|
|
@ -25,6 +25,7 @@ export function useAutoScroll(ref: RefObject<HTMLElement | null>) {
|
|||
}
|
||||
|
||||
const onContentChange = () => {
|
||||
if (lockRef.current) return
|
||||
if (stickRef.current) {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
|
@ -61,4 +62,12 @@ export function useAutoScroll(ref: RefObject<HTMLElement | null>) {
|
|||
mo.disconnect()
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
/** Temporarily suppress auto-scroll during prepend operations */
|
||||
const suppressAutoScroll = useCallback(() => {
|
||||
lockRef.current = true
|
||||
return () => { lockRef.current = false }
|
||||
}, [])
|
||||
|
||||
return { suppressAutoScroll }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { SessionManager } from "../../../agent/session/session-manager.js";
|
|||
import { resolveSessionPath } from "../../../agent/session/storage.js";
|
||||
import { RpcError, type RpcHandler } from "../dispatcher.js";
|
||||
|
||||
// Must match DEFAULT_MESSAGES_LIMIT from @multica/sdk/actions/rpc
|
||||
const DEFAULT_LIMIT = 10;
|
||||
|
||||
interface GetAgentMessagesParams {
|
||||
agentId: string;
|
||||
offset?: number;
|
||||
|
|
@ -14,7 +17,8 @@ export function createGetAgentMessagesHandler(): RpcHandler {
|
|||
if (!params || typeof params !== "object") {
|
||||
throw new RpcError("INVALID_PARAMS", "params must be an object");
|
||||
}
|
||||
const { agentId, offset = 0, limit = 50 } = params as GetAgentMessagesParams;
|
||||
const { agentId, limit = DEFAULT_LIMIT } = params as GetAgentMessagesParams;
|
||||
let { offset } = params as GetAgentMessagesParams;
|
||||
if (!agentId) {
|
||||
throw new RpcError("INVALID_PARAMS", "Missing required param: agentId");
|
||||
}
|
||||
|
|
@ -27,6 +31,12 @@ export function createGetAgentMessagesHandler(): RpcHandler {
|
|||
const session = new SessionManager({ sessionId: agentId });
|
||||
const allMessages = session.loadMessages();
|
||||
const total = allMessages.length;
|
||||
|
||||
// When offset is not provided, return the latest messages
|
||||
if (offset == null) {
|
||||
offset = Math.max(0, total - limit);
|
||||
}
|
||||
|
||||
const sliced = allMessages.slice(offset, offset + limit);
|
||||
|
||||
return { messages: sliced, total, offset, limit };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue