优化会话加载逻辑

This commit is contained in:
xintaofei
2026-03-10 13:10:46 +08:00
parent 51310da549
commit 5564fdd39f
5 changed files with 169 additions and 6 deletions

View File

@@ -1343,6 +1343,7 @@ async fn run_conversation_loop<'a>(
"acp://event", "acp://event",
AcpEvent::TurnComplete { AcpEvent::TurnComplete {
connection_id: conn_id.into(), connection_id: conn_id.into(),
session_id: sid.0.to_string(),
stop_reason: reason_str.into(), stop_reason: reason_str.into(),
}, },
); );
@@ -1372,6 +1373,7 @@ async fn run_conversation_loop<'a>(
"acp://event", "acp://event",
AcpEvent::TurnComplete { AcpEvent::TurnComplete {
connection_id: conn_id.into(), connection_id: conn_id.into(),
session_id: sid.0.to_string(),
stop_reason: reason_str.into(), stop_reason: reason_str.into(),
}, },
); );

View File

@@ -80,6 +80,7 @@ pub enum AcpEvent {
/// Turn completed /// Turn completed
TurnComplete { TurnComplete {
connection_id: String, connection_id: String,
session_id: String,
stop_reason: String, stop_reason: String,
}, },
/// Session established with agent-assigned session ID /// Session established with agent-assigned session ID

View File

@@ -4,6 +4,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Plus, RefreshCw, X } from "lucide-react" import { Plus, RefreshCw, X } from "lucide-react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { toast } from "sonner" import { toast } from "sonner"
import { disposeTauriListener } from "@/lib/tauri-listener"
import { useFolderContext } from "@/contexts/folder-context" import { useFolderContext } from "@/contexts/folder-context"
import { useTabContext } from "@/contexts/tab-context" import { useTabContext } from "@/contexts/tab-context"
import { useConnectionLifecycle } from "@/hooks/use-connection-lifecycle" 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 { ConversationShell } from "@/components/chat/conversation-shell"
import { WelcomeInputPanel } from "@/components/chat/welcome-input-panel" import { WelcomeInputPanel } from "@/components/chat/welcome-input-panel"
import { updateConversationStatus } from "@/lib/tauri" import { updateConversationStatus } from "@/lib/tauri"
import { useDbMessageDetail } from "@/hooks/use-db-message-detail" import {
import type { AgentType, PromptDraft } from "@/lib/types" 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 type { AdaptedMessage } from "@/lib/adapters/ai-elements-adapter"
import { import {
buildUserMessageTextPartsFromDraft, buildUserMessageTextPartsFromDraft,
@@ -246,10 +250,129 @@ const ExistingConversationView = memo(function ExistingConversationView({
export function ConversationDetailPanel() { export function ConversationDetailPanel() {
const t = useTranslations("Folder.conversation") const t = useTranslations("Folder.conversation")
const { folder, newConversation } = useFolderContext() const { folder, newConversation, conversations, refreshConversations } =
useFolderContext()
const { tabs, activeTabId, openNewConversationTab, closeTab } = const { tabs, activeTabId, openNewConversationTab, closeTab } =
useTabContext() useTabContext()
const [reloadByTabId, setReloadByTabId] = useState<Record<string, number>>({}) 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( const conversationTabs = useMemo(
() => () =>

View File

@@ -6,6 +6,7 @@ import type { DbConversationDetail } from "@/lib/types"
// Module-level cache: survives component unmount/remount // Module-level cache: survives component unmount/remount
const detailCache = new Map<number, DbConversationDetail>() const detailCache = new Map<number, DbConversationDetail>()
const detailInFlight = new Map<number, Promise<DbConversationDetail>>()
const detailListeners = new Map< const detailListeners = new Map<
number, number,
Set<(detail: DbConversationDetail) => void> Set<(detail: DbConversationDetail) => void>
@@ -50,6 +51,38 @@ export function invalidateDetailCache(conversationId: number) {
detailCache.delete(conversationId) 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 { interface State {
key: number key: number
detail: DbConversationDetail | null detail: DbConversationDetail | null
@@ -109,9 +142,8 @@ export function useDbMessageDetail(conversationId: number) {
if (detailCache.has(conversationId)) return if (detailCache.has(conversationId)) return
let cancelled = false let cancelled = false
getFolderConversation(conversationId) loadAndCacheDetail(conversationId)
.then((d) => { .then((d) => {
setCachedDetail(conversationId, d)
if (!cancelled) { if (!cancelled) {
setState((prev) => setState((prev) =>
prev.key === conversationId prev.key === conversationId

View File

@@ -440,7 +440,12 @@ export type AcpEvent =
tool_call: unknown tool_call: unknown
options: PermissionOptionInfo[] 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" type: "session_started"
connection_id: string connection_id: string