diff --git a/src/components/chat/AgentHistoryPanel.test.tsx b/src/components/chat/AgentHistoryPanel.test.tsx index 8dc3d6d..8dc4227 100644 --- a/src/components/chat/AgentHistoryPanel.test.tsx +++ b/src/components/chat/AgentHistoryPanel.test.tsx @@ -33,8 +33,37 @@ describe("AgentHistoryPanel", () => { fireEvent.change(screen.getByPlaceholderText("请输入会话标题"), { target: { value: "新的会话标题" }, }); - fireEvent.click(screen.getByLabelText("确认修改历史会话标题")); + fireEvent.click(screen.getByLabelText("确认")); expect(onRenameSession).toHaveBeenCalledWith("session-1", "新的会话标题"); }); + + it("orders history by the first message time instead of the latest update time", () => { + renderWithTheme( + , + ); + + const sessionTitles = screen.getAllByText(/较新的/).map((element) => element.textContent); + + expect(sessionTitles).toEqual(["较新的首条消息", "较新的更新"]); + }); }); diff --git a/src/components/chat/AgentHistoryPanel.tsx b/src/components/chat/AgentHistoryPanel.tsx index ca606c4..496b816 100644 --- a/src/components/chat/AgentHistoryPanel.tsx +++ b/src/components/chat/AgentHistoryPanel.tsx @@ -73,7 +73,6 @@ const getSessionGroupLabel = (timestamp: number) => { }; export const AgentHistoryPanel = ({ - sessions, activeSessionId, isHydrating = false, @@ -95,11 +94,25 @@ export const AgentHistoryPanel = ({ return sessions.filter((session) => session.title.toLowerCase().includes(normalizedKeyword)); }, [keyword, sessions]); + const sortedFilteredSessions = React.useMemo( + () => + [...filteredSessions].sort((left, right) => { + const createdAtDiff = right.createdAt - left.createdAt; + if (createdAtDiff !== 0) return createdAtDiff; + + const updatedAtDiff = right.updatedAt - left.updatedAt; + if (updatedAtDiff !== 0) return updatedAtDiff; + + return right.id.localeCompare(left.id); + }), + [filteredSessions], + ); + const groupedSessions = React.useMemo(() => { const groups = new Map(); - filteredSessions.forEach((session) => { - const label = getSessionGroupLabel(session.updatedAt); + sortedFilteredSessions.forEach((session) => { + const label = getSessionGroupLabel(session.createdAt); const existing = groups.get(label); if (existing) { existing.push(session); @@ -109,7 +122,7 @@ export const AgentHistoryPanel = ({ }); return Array.from(groups.entries()); - }, [filteredSessions]); + }, [sortedFilteredSessions]); const pendingDeleteSession = filteredSessions.find( (session) => session.id === pendingDeleteSessionId, @@ -390,7 +403,7 @@ export const AgentHistoryPanel = ({ {session.title} - {formatRelativeDate(session.updatedAt)} + {formatRelativeDate(session.createdAt)} )} diff --git a/src/components/chat/chatStorage.ts b/src/components/chat/chatStorage.ts index 3c2d794..51d8a34 100644 --- a/src/components/chat/chatStorage.ts +++ b/src/components/chat/chatStorage.ts @@ -51,6 +51,28 @@ const sanitizeMessages = (messages: Message[] | undefined) => const sanitizeBranchGroups = (branchGroups: BranchGroup[] | undefined) => Array.isArray(branchGroups) ? cloneBranchGroups(branchGroups) : []; +const hasChatContent = (state: { + messages: Message[]; + branchGroups: BranchGroup[]; + sessionId?: string; +}) => + state.messages.length > 0 || + state.branchGroups.length > 0 || + Boolean(state.sessionId); + +const compareSessionsByAnchorTime = ( + left: Pick, + right: Pick, +) => { + const createdAtDiff = right.createdAt - left.createdAt; + if (createdAtDiff !== 0) return createdAtDiff; + + const updatedAtDiff = right.updatedAt - left.updatedAt; + if (updatedAtDiff !== 0) return updatedAtDiff; + + return right.id.localeCompare(left.id); +}; + const toLoadedChatState = (session: ChatSessionRecord | undefined): LoadedChatState => { if (!session) return emptyLoadedChatState(); return { @@ -131,7 +153,7 @@ const getLatestSession = async () => { const db = await getDb(); const sessions = await db.getAll(SESSION_STORE); if (sessions.length === 0) return undefined; - return sessions.sort((left, right) => right.updatedAt - left.updatedAt)[0]; + return sessions.sort(compareSessionsByAnchorTime)[0]; }; const migrateLegacyLocalStorage = async () => { @@ -215,10 +237,7 @@ export const saveActiveChatState = async ( ): Promise => { if (typeof window === "undefined") return state.storageSessionId; - const hasContent = - state.messages.length > 0 || - state.branchGroups.length > 0 || - Boolean(state.sessionId); + const hasContent = hasChatContent(state); const db = await getDb(); const existingSession = state.storageSessionId @@ -241,11 +260,15 @@ export const saveActiveChatState = async ( const storageSessionId = state.storageSessionId ?? createId(); const preferredTitle = state.title?.trim(); const finalTitle = preferredTitle || existingSession?.title || "新对话"; + const shouldAnchorCreatedAtToFirstMessage = + Boolean(existingSession) && !hasChatContent(existingSession) && hasContent; const nextRecord: ChatSessionRecord = { id: storageSessionId, title: finalTitle, isTitleManuallyEdited: state.isTitleManuallyEdited ?? existingSession?.isTitleManuallyEdited ?? false, - createdAt: existingSession?.createdAt ?? now, + createdAt: shouldAnchorCreatedAtToFirstMessage + ? now + : existingSession?.createdAt ?? now, updatedAt: now, sessionId: state.sessionId, messages: sanitizeMessages(state.messages), @@ -268,9 +291,7 @@ export const listChatSessions = async (): Promise => { const db = await getDb(); const sessions = await db.getAll(SESSION_STORE); - return sessions - .sort((left, right) => right.updatedAt - left.updatedAt) - .map(toSessionSummary); + return sessions.sort(compareSessionsByAnchorTime).map(toSessionSummary); }; export const updateChatSessionTitle = async ( @@ -353,9 +374,7 @@ export const deleteChatSession = async (sessionId: string): Promise right.updatedAt - left.updatedAt, - )[0]; + const nextActiveSession = remainingSessions.sort(compareSessionsByAnchorTime)[0]; const meta = await getMeta(); await setMeta({