optimize: session list loading
This commit is contained in:
@@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user