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:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user