优化会话加载逻辑
This commit is contained in:
@@ -4,6 +4,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Plus, RefreshCw, X } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { toast } from "sonner"
|
||||
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||
import { useFolderContext } from "@/contexts/folder-context"
|
||||
import { useTabContext } from "@/contexts/tab-context"
|
||||
import { useConnectionLifecycle } from "@/hooks/use-connection-lifecycle"
|
||||
@@ -11,8 +12,11 @@ import { MessageListView } from "@/components/message/message-list-view"
|
||||
import { ConversationShell } from "@/components/chat/conversation-shell"
|
||||
import { WelcomeInputPanel } from "@/components/chat/welcome-input-panel"
|
||||
import { updateConversationStatus } from "@/lib/tauri"
|
||||
import { useDbMessageDetail } from "@/hooks/use-db-message-detail"
|
||||
import type { AgentType, PromptDraft } from "@/lib/types"
|
||||
import {
|
||||
useDbMessageDetail,
|
||||
warmupDetailCache,
|
||||
} from "@/hooks/use-db-message-detail"
|
||||
import type { AcpEvent, AgentType, PromptDraft } from "@/lib/types"
|
||||
import type { AdaptedMessage } from "@/lib/adapters/ai-elements-adapter"
|
||||
import {
|
||||
buildUserMessageTextPartsFromDraft,
|
||||
@@ -246,10 +250,129 @@ const ExistingConversationView = memo(function ExistingConversationView({
|
||||
|
||||
export function ConversationDetailPanel() {
|
||||
const t = useTranslations("Folder.conversation")
|
||||
const { folder, newConversation } = useFolderContext()
|
||||
const { folder, newConversation, conversations, refreshConversations } =
|
||||
useFolderContext()
|
||||
const { tabs, activeTabId, openNewConversationTab, closeTab } =
|
||||
useTabContext()
|
||||
const [reloadByTabId, setReloadByTabId] = useState<Record<string, number>>({})
|
||||
const tabsRef = useRef(tabs)
|
||||
const conversationsRef = useRef(conversations)
|
||||
const pendingClosedConversationIdsRef = useRef<Set<number>>(new Set())
|
||||
const pendingRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
tabsRef.current = tabs
|
||||
}, [tabs])
|
||||
|
||||
useEffect(() => {
|
||||
conversationsRef.current = conversations
|
||||
}, [conversations])
|
||||
|
||||
const flushClosedConversationRefresh = useCallback(() => {
|
||||
const conversationIds = Array.from(pendingClosedConversationIdsRef.current)
|
||||
if (conversationIds.length === 0) return
|
||||
pendingClosedConversationIdsRef.current.clear()
|
||||
|
||||
void (async () => {
|
||||
await Promise.all(
|
||||
conversationIds.map(async (conversationId) => {
|
||||
const summary =
|
||||
conversationsRef.current.find(
|
||||
(item) => item.id === conversationId
|
||||
) ?? null
|
||||
if (summary?.status === "in_progress") {
|
||||
try {
|
||||
await updateConversationStatus(conversationId, "pending_review")
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[ConversationDetailPanel] background update status failed:",
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await warmupDetailCache(conversationId)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[ConversationDetailPanel] background detail cache refresh failed:",
|
||||
error
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
refreshConversations()
|
||||
})()
|
||||
}, [refreshConversations])
|
||||
|
||||
const scheduleClosedConversationRefresh = useCallback(
|
||||
(conversationId: number) => {
|
||||
pendingClosedConversationIdsRef.current.add(conversationId)
|
||||
if (pendingRefreshTimerRef.current) return
|
||||
|
||||
// Delay briefly so local session file writes can settle.
|
||||
pendingRefreshTimerRef.current = setTimeout(() => {
|
||||
pendingRefreshTimerRef.current = null
|
||||
flushClosedConversationRefresh()
|
||||
}, 1200)
|
||||
},
|
||||
[flushClosedConversationRefresh]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let unlisten: (() => void | Promise<void>) | null = null
|
||||
const pendingClosedConversationIds = pendingClosedConversationIdsRef.current
|
||||
|
||||
void import("@tauri-apps/api/event")
|
||||
.then(({ listen }) =>
|
||||
listen<AcpEvent>("acp://event", (event) => {
|
||||
const payload = event.payload
|
||||
if (payload.type !== "turn_complete") return
|
||||
|
||||
const summary = conversationsRef.current.find(
|
||||
(item) => item.external_id === payload.session_id
|
||||
)
|
||||
if (!summary) return
|
||||
|
||||
const isOpenInTabs = tabsRef.current.some(
|
||||
(tab) => tab.conversationId === summary.id
|
||||
)
|
||||
if (isOpenInTabs) return
|
||||
|
||||
scheduleClosedConversationRefresh(summary.id)
|
||||
})
|
||||
)
|
||||
.then((dispose) => {
|
||||
if (cancelled) {
|
||||
disposeTauriListener(
|
||||
dispose,
|
||||
"ConversationDetailPanel.backgroundRefresh"
|
||||
)
|
||||
return
|
||||
}
|
||||
unlisten = dispose
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore when non-tauri runtime.
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (pendingRefreshTimerRef.current) {
|
||||
clearTimeout(pendingRefreshTimerRef.current)
|
||||
pendingRefreshTimerRef.current = null
|
||||
}
|
||||
pendingClosedConversationIds.clear()
|
||||
disposeTauriListener(
|
||||
unlisten,
|
||||
"ConversationDetailPanel.backgroundRefresh"
|
||||
)
|
||||
}
|
||||
}, [scheduleClosedConversationRefresh])
|
||||
|
||||
const conversationTabs = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { DbConversationDetail } from "@/lib/types"
|
||||
|
||||
// Module-level cache: survives component unmount/remount
|
||||
const detailCache = new Map<number, DbConversationDetail>()
|
||||
const detailInFlight = new Map<number, Promise<DbConversationDetail>>()
|
||||
const detailListeners = new Map<
|
||||
number,
|
||||
Set<(detail: DbConversationDetail) => void>
|
||||
@@ -50,6 +51,38 @@ export function invalidateDetailCache(conversationId: number) {
|
||||
detailCache.delete(conversationId)
|
||||
}
|
||||
|
||||
async function loadAndCacheDetail(
|
||||
conversationId: number
|
||||
): Promise<DbConversationDetail> {
|
||||
const existing = detailInFlight.get(conversationId)
|
||||
if (existing) return existing
|
||||
|
||||
const promise = getFolderConversation(conversationId)
|
||||
.then((detail) => {
|
||||
setCachedDetail(conversationId, detail)
|
||||
return detail
|
||||
})
|
||||
.finally(() => {
|
||||
detailInFlight.delete(conversationId)
|
||||
})
|
||||
|
||||
detailInFlight.set(conversationId, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
export async function warmupDetailCache(
|
||||
conversationId: number
|
||||
): Promise<DbConversationDetail> {
|
||||
return loadAndCacheDetail(conversationId)
|
||||
}
|
||||
|
||||
export async function refreshDetailCache(
|
||||
conversationId: number
|
||||
): Promise<DbConversationDetail> {
|
||||
detailCache.delete(conversationId)
|
||||
return loadAndCacheDetail(conversationId)
|
||||
}
|
||||
|
||||
interface State {
|
||||
key: number
|
||||
detail: DbConversationDetail | null
|
||||
@@ -109,9 +142,8 @@ export function useDbMessageDetail(conversationId: number) {
|
||||
if (detailCache.has(conversationId)) return
|
||||
|
||||
let cancelled = false
|
||||
getFolderConversation(conversationId)
|
||||
loadAndCacheDetail(conversationId)
|
||||
.then((d) => {
|
||||
setCachedDetail(conversationId, d)
|
||||
if (!cancelled) {
|
||||
setState((prev) =>
|
||||
prev.key === conversationId
|
||||
|
||||
@@ -440,7 +440,12 @@ export type AcpEvent =
|
||||
tool_call: unknown
|
||||
options: PermissionOptionInfo[]
|
||||
}
|
||||
| { type: "turn_complete"; connection_id: string; stop_reason: string }
|
||||
| {
|
||||
type: "turn_complete"
|
||||
connection_id: string
|
||||
session_id: string
|
||||
stop_reason: string
|
||||
}
|
||||
| {
|
||||
type: "session_started"
|
||||
connection_id: string
|
||||
|
||||
Reference in New Issue
Block a user