optimize: session list loading

This commit is contained in:
xintaofei
2026-04-01 13:41:22 +08:00
parent b98f50340f
commit 3a5d720cc9
3 changed files with 85 additions and 36 deletions

View File

@@ -144,7 +144,8 @@ const ConversationTabView = memo(function ConversationTabView({
const t = useTranslations("Folder.conversation") const t = useTranslations("Folder.conversation")
const tWelcome = useTranslations("Folder.chat.welcomeInputPanel") const tWelcome = useTranslations("Folder.chat.welcomeInputPanel")
const sharedT = useTranslations("Folder.chat.shared") const sharedT = useTranslations("Folder.chat.shared")
const { folder, folderId, refreshConversations } = useFolderContext() const { folder, folderId, refreshConversations, updateConversationLocal } =
useFolderContext()
const { tabs, bindConversationTab, setTabRuntimeConversationId, pinTab } = const { tabs, bindConversationTab, setTabRuntimeConversationId, pinTab } =
useTabContext() useTabContext()
const { setSessionStats } = useSessionStats() const { setSessionStats } = useSessionStats()
@@ -371,18 +372,17 @@ const ConversationTabView = memo(function ConversationTabView({
} }
if (targetStatus) { if (targetStatus) {
updateConversationStatus(persistedId, targetStatus) updateConversationLocal(persistedId, { status: targetStatus })
.then(() => refreshConversations()) updateConversationStatus(persistedId, targetStatus).catch((e: unknown) =>
.catch((e: unknown) => console.error("[ConversationTabView] update status:", e)
console.error("[ConversationTabView] update status:", e) )
)
} }
}, [ }, [
completeTurn, completeTurn,
connStatus, connStatus,
effectiveConversationId, effectiveConversationId,
refreshConversations,
syncTurnMetadata, syncTurnMetadata,
updateConversationLocal,
]) ])
// Auto-send queued messages when agent finishes responding. // Auto-send queued messages when agent finishes responding.
@@ -473,16 +473,18 @@ const ConversationTabView = memo(function ConversationTabView({
if (!persistedId) return if (!persistedId) return
if (connStatus === "disconnected") { if (connStatus === "disconnected") {
statusUpdatedRef.current = true statusUpdatedRef.current = true
updateConversationStatus(persistedId, "completed") updateConversationLocal(persistedId, { status: "completed" })
.then(() => refreshConversations()) updateConversationStatus(persistedId, "completed").catch((e) =>
.catch((e) => console.error("[ConversationTabView] update status:", e)) console.error("[ConversationTabView] update status:", e)
)
} else if (connStatus === "error") { } else if (connStatus === "error") {
statusUpdatedRef.current = true statusUpdatedRef.current = true
updateConversationStatus(persistedId, "cancelled") updateConversationLocal(persistedId, { status: "cancelled" })
.then(() => refreshConversations()) updateConversationStatus(persistedId, "cancelled").catch((e) =>
.catch((e) => console.error("[ConversationTabView] update status:", e)) console.error("[ConversationTabView] update status:", e)
)
} }
}, [connStatus, refreshConversations]) }, [connStatus, updateConversationLocal])
useEffect(() => { useEffect(() => {
if (dbConversationId == null) return if (dbConversationId == null) return
@@ -561,11 +563,11 @@ const ConversationTabView = memo(function ConversationTabView({
const persistedId = dbConvIdRef.current const persistedId = dbConvIdRef.current
if (persistedId) { if (persistedId) {
updateConversationStatus(persistedId, "in_progress") updateConversationLocal(persistedId, { status: "in_progress" })
.then(() => refreshConversations()) updateConversationStatus(persistedId, "in_progress").catch(
.catch((e: unknown) => (e: unknown) =>
console.error("[ConversationTabView] update status:", e) console.error("[ConversationTabView] update status:", e)
) )
statusUpdatedRef.current = false statusUpdatedRef.current = false
return return
} }
@@ -610,11 +612,14 @@ const ConversationTabView = memo(function ConversationTabView({
// of setting "in_progress" (which would never be updated). // of setting "in_progress" (which would never be updated).
const initialStatus = deferredStatusRef.current ?? "in_progress" const initialStatus = deferredStatusRef.current ?? "in_progress"
deferredStatusRef.current = null deferredStatusRef.current = null
updateConversationStatus(newConversationId, initialStatus) refreshConversations()
.then(() => refreshConversations()) updateConversationLocal(newConversationId, {
.catch((e: unknown) => status: initialStatus,
})
updateConversationStatus(newConversationId, initialStatus).catch(
(e: unknown) =>
console.error("[ConversationTabView] update status:", e) console.error("[ConversationTabView] update status:", e)
) )
}) })
.catch((e: unknown) => .catch((e: unknown) =>
console.error("[ConversationTabView] create conversation:", e) console.error("[ConversationTabView] create conversation:", e)
@@ -643,6 +648,7 @@ const ConversationTabView = memo(function ConversationTabView({
tWelcome, tWelcome,
tabId, tabId,
trySaveExternalId, trySaveExternalId,
updateConversationLocal,
] ]
) )
@@ -686,7 +692,7 @@ const ConversationTabView = memo(function ConversationTabView({
sessionIdRef.current = forkedSessionId sessionIdRef.current = forkedSessionId
setExternalId(effectiveConversationId, forkedSessionId) setExternalId(effectiveConversationId, forkedSessionId)
await refreshConversations() refreshConversations()
// Send the message on the forked session (S2) // Send the message on the forked session (S2)
handleSend(draft, selectedModeIdArg) handleSend(draft, selectedModeIdArg)
} catch (err) { } catch (err) {
@@ -1004,8 +1010,13 @@ export function ConversationDetailPanel() {
getSession, getSession,
removeConversation: runtimeRemoveConversation, removeConversation: runtimeRemoveConversation,
} = useConversationRuntime() } = useConversationRuntime()
const { folder, newConversation, conversations, refreshConversations } = const {
useFolderContext() folder,
newConversation,
conversations,
refreshConversations,
updateConversationLocal,
} = useFolderContext()
const { const {
tabs, tabs,
activeTabId, activeTabId,
@@ -1042,6 +1053,7 @@ export function ConversationDetailPanel() {
const runtimeCompleteTurnRef = useRef(runtimeCompleteTurn) const runtimeCompleteTurnRef = useRef(runtimeCompleteTurn)
const runtimeRemoveConversationRef = useRef(runtimeRemoveConversation) const runtimeRemoveConversationRef = useRef(runtimeRemoveConversation)
const refreshConversationsRef = useRef(refreshConversations) const refreshConversationsRef = useRef(refreshConversations)
const updateConversationLocalRef = useRef(updateConversationLocal)
useEffect(() => { useEffect(() => {
getConversationIdByExternalIdRef.current = getConversationIdByExternalId getConversationIdByExternalIdRef.current = getConversationIdByExternalId
}, [getConversationIdByExternalId]) }, [getConversationIdByExternalId])
@@ -1057,6 +1069,9 @@ export function ConversationDetailPanel() {
useEffect(() => { useEffect(() => {
refreshConversationsRef.current = refreshConversations refreshConversationsRef.current = refreshConversations
}, [refreshConversations]) }, [refreshConversations])
useEffect(() => {
updateConversationLocalRef.current = updateConversationLocal
}, [updateConversationLocal])
// Background turn_complete handler: for conversations not open in tabs. // Background turn_complete handler: for conversations not open in tabs.
// Registered once — uses refs to avoid re-creating the listener on every // Registered once — uses refs to avoid re-creating the listener on every
@@ -1108,14 +1123,16 @@ export function ConversationDetailPanel() {
summary?.id ?? summary?.id ??
(matchedConversationId > 0 ? matchedConversationId : null) (matchedConversationId > 0 ? matchedConversationId : null)
if (dbId && (!summary || summary.status === "in_progress")) { if (dbId && (!summary || summary.status === "in_progress")) {
updateConversationStatus(dbId, "pending_review") updateConversationLocalRef.current(dbId, {
.then(() => refreshConversationsRef.current()) status: "pending_review",
.catch((error: unknown) => })
updateConversationStatus(dbId, "pending_review").catch(
(error: unknown) =>
console.error( console.error(
"[ConversationDetailPanel] background update status:", "[ConversationDetailPanel] background update status:",
error error
) )
) )
} }
}) })
) )

View File

@@ -186,6 +186,7 @@ export function SidebarConversationList({
selectedConversation, selectedConversation,
folderId, folderId,
refreshConversations, refreshConversations,
updateConversationLocal,
} = useFolderContext() } = useFolderContext()
const { openTab, closeConversationTab, openNewConversationTab } = const { openTab, closeConversationTab, openNewConversationTab } =
@@ -347,10 +348,10 @@ export function SidebarConversationList({
const handleStatusChange = useCallback( const handleStatusChange = useCallback(
async (id: number, status: ConversationStatus) => { async (id: number, status: ConversationStatus) => {
updateConversationLocal(id, { status })
await updateConversationStatus(id, status) await updateConversationStatus(id, status)
refreshConversations()
}, },
[refreshConversations] [updateConversationLocal]
) )
const handleNewConversation = useCallback(() => { const handleNewConversation = useCallback(() => {
@@ -391,12 +392,15 @@ export function SidebarConversationList({
if (completingReview || reviewConversationCount === 0) return if (completingReview || reviewConversationCount === 0) return
setCompletingReview(true) setCompletingReview(true)
try { try {
// Optimistic: update all locally first
for (const conversation of reviewConversations) {
updateConversationLocal(conversation.id, { status: "completed" })
}
await Promise.all( await Promise.all(
reviewConversations.map((conversation) => reviewConversations.map((conversation) =>
updateConversationStatus(conversation.id, "completed") updateConversationStatus(conversation.id, "completed")
) )
) )
refreshConversations()
toast.success( toast.success(
t("toasts.reviewCompleted", { count: reviewConversationCount }) t("toasts.reviewCompleted", { count: reviewConversationCount })
) )
@@ -404,6 +408,8 @@ export function SidebarConversationList({
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e) const msg = e instanceof Error ? e.message : String(e)
toast.error(t("toasts.completeReviewFailed", { message: msg })) toast.error(t("toasts.completeReviewFailed", { message: msg }))
// Revert on error — refetch from server
refreshConversations()
} finally { } finally {
setCompletingReview(false) setCompletingReview(false)
} }
@@ -412,13 +418,14 @@ export function SidebarConversationList({
reviewConversationCount, reviewConversationCount,
reviewConversations, reviewConversations,
refreshConversations, refreshConversations,
updateConversationLocal,
t, t,
]) ])
return ( return (
<div className="flex flex-col flex-1 min-h-0"> <div className="relative flex flex-col flex-1 min-h-0">
{(loading || refreshing) && ( {(loading || refreshing) && (
<div className="flex items-center justify-center py-1"> <div className="absolute top-0 left-0 right-0 flex items-center justify-center py-1 z-10 pointer-events-none">
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" /> <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
</div> </div>
)} )}

View File

@@ -50,6 +50,11 @@ interface FolderContextValue {
stats: AgentStats | null stats: AgentStats | null
refreshConversations: () => void refreshConversations: () => void
/** Optimistically update a conversation's status in local state + cache. */
updateConversationLocal: (
id: number,
patch: Partial<Pick<DbConversationSummary, "status" | "title">>
) => void
} }
const FolderContext = createContext<FolderContextValue | null>(null) const FolderContext = createContext<FolderContextValue | null>(null)
@@ -222,9 +227,27 @@ export function FolderProvider({
}, []) }, [])
const refreshConversations = useCallback(() => { const refreshConversations = useCallback(() => {
cache.delete(cacheKey) // Keep cache intact so fetchConversations shows existing data (refreshing
// spinner) instead of falling through to the loading/skeleton path.
fetchConversations() fetchConversations()
}, [cacheKey, fetchConversations]) }, [fetchConversations])
const updateConversationLocal = useCallback(
(
id: number,
patch: Partial<Pick<DbConversationSummary, "status" | "title">>
) => {
const now = new Date().toISOString()
const apply = (list: DbConversationSummary[]) =>
list.map((c) => (c.id === id ? { ...c, ...patch, updated_at: now } : c))
setConversations((prev) => {
const next = apply(prev)
cache.set(cacheKey, next)
return next
})
},
[cacheKey]
)
const stats = useMemo( const stats = useMemo(
() => (conversations.length > 0 ? computeStats(conversations) : null), () => (conversations.length > 0 ? computeStats(conversations) : null),
@@ -248,6 +271,7 @@ export function FolderProvider({
cancelNewConversation, cancelNewConversation,
stats, stats,
refreshConversations, refreshConversations,
updateConversationLocal,
}), }),
[ [
folder, folder,
@@ -265,6 +289,7 @@ export function FolderProvider({
cancelNewConversation, cancelNewConversation,
stats, stats,
refreshConversations, refreshConversations,
updateConversationLocal,
] ]
) )