fix(web): summarize session titles, sort by activity, and paginate history (BUG-553)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 14:34:30 +08:00
co-authored by Cursor
parent d04990fc4a
commit a1956deb63
36 changed files with 1089 additions and 64 deletions
+19 -2
View File
@@ -9,6 +9,7 @@ import { useQueuedMessage } from "@/hooks/use-queued-message";
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
import { parseAgentReply, isGenericSessionTitle, resolveSessionTitle } from "@/lib/agent-reply";
import { isAutoDerivedSessionTitle } from "@/lib/session-title";
import type { ConsultationBirthTimeMode } from "@/lib/consultation-birth-time-mode";
import {
GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION,
@@ -273,6 +274,9 @@ export function useConsultationRun(params: ConsultationRunParams) {
.then((status) => {
if (status?.status !== "reserved") return;
const session = sessions.find((item) => item.id === status.sessionId);
if (typeof status.title === "string" && status.title && session && isAutoDerivedSessionTitle(session.title)) {
updateSession(session.id, (current) => ({ ...current, title: status.title! }));
}
if (session) restoreConsultationRecovery(session, status.requestId);
})
.catch(() => undefined);
@@ -381,6 +385,11 @@ export function useConsultationRun(params: ConsultationRunParams) {
setComposerNotice("回答已完成,正在恢复服务端完整内容。");
try {
const status = await fetchConsultationStatus(pending.sessionId, pending.requestId);
if (typeof status.title === "string" && status.title) {
updateSession(pending.sessionId, (current) => (
isAutoDerivedSessionTitle(current.title) ? { ...current, title: status.title! } : current
));
}
if (status.status === "completed") {
const detailed = await fetchSessionDetail(pending.sessionId, modelCatalog);
if (detailed) {
@@ -767,6 +776,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let answer = "";
let streamedTitle: string | undefined;
const updateActivity = (event: ConsultationAgentPublicEvent) => {
let activity: AgentActivityView | undefined;
if (event.type === "skill.started") {
@@ -823,6 +833,12 @@ export function useConsultationRun(params: ConsultationRunParams) {
parseAgentReply(answer).text,
);
}
if (event.type === "session.title") {
streamedTitle = event.title;
updateSession(sessionId, (current) => (
isAutoDerivedSessionTitle(current.title) ? { ...current, title: event.title } : current
));
}
if (event.type === "run.completed") {
runCompleted = true;
agentExecutionReceipt = event.receipt;
@@ -902,8 +918,9 @@ export function useConsultationRun(params: ConsultationRunParams) {
: new Error("Agent 没有返回可显示的回答,请重试。");
}
const completedTitle = reply.title && !isGenericSessionTitle(reply.title)
? resolveSessionTitle(question, reply.title, {
const modelTitle = streamedTitle ?? reply.title;
const completedTitle = modelTitle && !isGenericSessionTitle(modelTitle)
? resolveSessionTitle(question, modelTitle, {
entrypoint: consultEntrypoint,
theme,
existingTitles: sessions.filter((item) => item.id !== sessionId).map((item) => item.title),
+52 -3
View File
@@ -1,6 +1,7 @@
"use client";
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
import { useRef } from "react";
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
import { writeChatSession } from "@/lib/chat-session-write-contract";
@@ -18,11 +19,14 @@ import {
activeChartStorageKey,
createSession,
fetchSessionDetail,
fetchSessions,
LoginRedirectError,
mergeHydratedSession,
patchSessionModel,
readSessions,
} from "@/lib/home-cloud-sync";
import { chartSnapshotForSession } from "@/lib/home-profile";
import { beginSessionPageLoad, mergeSessionPage } from "@/lib/session-groups";
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
import {
persistSessionModelSelection,
@@ -30,7 +34,6 @@ import {
} from "@/lib/session-model-persistence";
import type { PublicLanguageModelCatalog } from "@/lib/public-models";
import {
timestamp,
type Account,
type ChartLibraryRecord,
type ChatSession,
@@ -73,6 +76,10 @@ export type SessionManagementParams = {
setSessionDetailLoadingId: Dispatch<SetStateAction<string | null>>;
setSessionFullPrompt: Dispatch<SetStateAction<{ question: string; theme: Theme } | null>>;
setSessions: Dispatch<SetStateAction<ChatSession[]>>;
sessionsCursor: string | null;
setSessionsCursor: Dispatch<SetStateAction<string | null>>;
showArchivedSessions: boolean;
setShowArchivedSessions: Dispatch<SetStateAction<boolean>>;
uiPreview: MutableRefObject<boolean>;
visibleSessions: ChatSession[];
openRectificationSession: (exactSessionId: string) => Promise<void> | void;
@@ -113,11 +120,16 @@ export function useSessionManagement(params: SessionManagementParams) {
setSessionDetailLoadingId,
setSessionFullPrompt,
setSessions,
sessionsCursor,
setSessionsCursor,
showArchivedSessions,
setShowArchivedSessions,
uiPreview,
visibleSessions,
openRectificationSession,
} = params;
sessionsRef.current = sessions;
const loadMoreInFlight = useRef(false);
function updateSession(sessionId: string, change: (session: ChatSession) => ChatSession) {
setSessions((current) => current.map((session) => (session.id === sessionId ? change(session) : session)));
@@ -185,7 +197,7 @@ export function useSessionManagement(params: SessionManagementParams) {
async function renameSession(session: ChatSession) {
const title = window.prompt("重命名聊天记录", session.title)?.trim();
if (!title || title === session.title) return;
const nextSession = { ...session, title, updatedAt: timestamp() };
const nextSession = { ...session, title };
updateSession(session.id, () => nextSession);
try {
await persistSession(nextSession);
@@ -348,6 +360,41 @@ export function useSessionManagement(params: SessionManagementParams) {
sessionSelectionSource.current = "user";
}
async function loadMoreSessions() {
if (!beginSessionPageLoad(loadMoreInFlight, sessionsCursor)) return;
try {
const page = await fetchSessions(undefined, {
before: sessionsCursor,
archived: showArchivedSessions,
});
const incoming = readSessions(page.sessions, modelCatalog).sessions;
setSessions((current) => mergeSessionPage(current, incoming));
setSessionsCursor(page.nextCursor);
} catch (caught) {
if (caught instanceof LoginRedirectError) return;
setComposerNotice(caught instanceof Error ? caught.message : "暂时无法读取聊天记录");
} finally {
loadMoreInFlight.current = false;
}
}
async function toggleArchivedView() {
const nextArchived = !showArchivedSessions;
setShowArchivedSessions(nextArchived);
try {
const page = await fetchSessions(undefined, { archived: nextArchived });
const parsed = readSessions(page.sessions, modelCatalog).sessions;
const active = sessionsRef.current.find((session) => session.id === activeSessionId);
setSessions(active && !parsed.some((session) => session.id === active.id)
? mergeHydratedSession(parsed, active)
: parsed);
setSessionsCursor(page.nextCursor);
} catch (caught) {
if (caught instanceof LoginRedirectError) return;
setComposerNotice(caught instanceof Error ? caught.message : "暂时无法读取聊天记录");
}
}
applySessionPopStateRef.current = (search: string) => {
if (uiPreview.current) return;
const listed = sessionsRef.current;
@@ -384,7 +431,7 @@ export function useSessionManagement(params: SessionManagementParams) {
const nextSession: ChatSession = retryingFailedSync
? activeSession
: { ...activeSession, modelId, updatedAt: timestamp() };
: { ...activeSession, modelId };
const selectionVersion = (modelSelectionVersions.current.get(nextSession.id) ?? 0) + 1;
modelSelectionVersions.current.set(nextSession.id, selectionVersion);
if (!retryingFailedSync) updateSession(activeSession.id, () => nextSession);
@@ -440,5 +487,7 @@ export function useSessionManagement(params: SessionManagementParams) {
startNewChat,
selectSession,
selectSessionModel,
loadMoreSessions,
toggleArchivedView,
};
}