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

View File

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

View File

@@ -50,6 +50,11 @@ interface FolderContextValue {
stats: AgentStats | null
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)
@@ -222,9 +227,27 @@ export function FolderProvider({
}, [])
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()
}, [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(
() => (conversations.length > 0 ? computeStats(conversations) : null),
@@ -248,6 +271,7 @@ export function FolderProvider({
cancelNewConversation,
stats,
refreshConversations,
updateConversationLocal,
}),
[
folder,
@@ -265,6 +289,7 @@ export function FolderProvider({
cancelNewConversation,
stats,
refreshConversations,
updateConversationLocal,
]
)