优化会话加载逻辑
This commit is contained in:
@@ -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(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user