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