From 59635ac5ac343efcff59d84684218ee33297a7bf Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Sat, 1 Aug 2026 16:46:30 -0700
Subject: [PATCH] fix(ai-vault): key first-prompt-card by session.id
Remounting the card on session switches prevents late responses from
a previous load from writing stale data into the component's refs.
Also improves conversation-turn key stability.
---
.../right-sidebar/AiVaultSessionDetails.tsx | 10 +++-
.../ai-vault-first-prompt-card.tsx | 57 ++++++++++---------
2 files changed, 37 insertions(+), 30 deletions(-)
diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
index b631dc0958e..30e44d375ac 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
@@ -165,7 +165,11 @@ export function SessionInlineDetails({
'First prompt'
)}
>
-
+
}
@@ -176,9 +180,9 @@ export function SessionInlineDetails({
>
{detailTurns.length > 0 ? (
- {detailTurns.map((turn, index) => (
+ {detailTurns.map((turn) => (
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.tsx b/src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.tsx
index 78e4f04c136..819d29031d7 100644
--- a/src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.tsx
+++ b/src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.tsx
@@ -7,6 +7,16 @@ import { translate } from '@/i18n/i18n'
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
+function canLoadFullFirstPrompt(
+ session: Pick
+): boolean {
+ return (
+ session.executionHostId === LOCAL_EXECUTION_HOST_ID &&
+ Boolean(session.filePath.trim()) &&
+ typeof window.api.aiVault.getFirstUserPrompt === 'function'
+ )
+}
+
export function FirstPromptCard({
session,
previewText
@@ -15,16 +25,19 @@ export function FirstPromptCard({
/** Short preview from list scan; replaced by the full on-demand body when available. */
previewText: string
}): React.JSX.Element {
+ // Loading starts true when an on-demand re-parse is possible so the mount effect
+ // does not need a sync setState (react-doctor: no-adjust-state-on-prop-change).
const [fullText, setFullText] = useState(null)
- const [loading, setLoading] = useState(false)
+ const [loading, setLoading] = useState(() => canLoadFullFirstPrompt(session))
const [copied, setCopied] = useState(false)
const [copying, setCopying] = useState(false)
const fullTextRef = useRef(null)
const loadPromiseRef = useRef | null>(null)
- // Bumped on every session switch so a late response from the previous session
- // cannot write its body into the refs the Copy button reads.
+ // Bumped on unmount/cleanup so a late response cannot write into a dead card.
const generationRef = useRef(0)
+ const { agent, codexHome, executionHostId, filePath, sessionId } = session
+
const loadFullPrompt = useCallback((): Promise => {
if (fullTextRef.current != null) {
return Promise.resolve(fullTextRef.current)
@@ -33,22 +46,19 @@ export function FirstPromptCard({
return loadPromiseRef.current
}
- const canLoadFull =
- session.executionHostId === LOCAL_EXECUTION_HOST_ID && Boolean(session.filePath.trim())
- const getFirstUserPrompt = window.api.aiVault.getFirstUserPrompt
- if (!canLoadFull || typeof getFirstUserPrompt !== 'function') {
+ if (!canLoadFullFirstPrompt({ executionHostId, filePath })) {
return Promise.resolve(null)
}
+ const getFirstUserPrompt = window.api.aiVault.getFirstUserPrompt
- setLoading(true)
const generation = generationRef.current
const isStale = (): boolean => generationRef.current !== generation
const promise = getFirstUserPrompt({
- agent: session.agent,
- filePath: session.filePath,
- sessionId: session.sessionId,
- executionHostId: session.executionHostId,
- codexHome: session.codexHome
+ agent,
+ filePath,
+ sessionId,
+ executionHostId,
+ codexHome
})
.then((result) => {
if (isStale()) {
@@ -78,24 +88,17 @@ export function FirstPromptCard({
loadPromiseRef.current = promise
return promise
- }, [
- session.agent,
- session.codexHome,
- session.executionHostId,
- session.filePath,
- session.sessionId
- ])
+ }, [agent, codexHome, executionHostId, filePath, sessionId])
// Why: list rows never carry the full first prompt (payload/perf). Load the
- // untruncated body once when this details card mounts.
+ // untruncated body once when this details card mounts. Parent keys this card by
+ // session.id so session switches remount with fresh state.
useEffect(() => {
- generationRef.current += 1
- fullTextRef.current = null
- loadPromiseRef.current = null
- setFullText(null)
- setLoading(false)
void loadFullPrompt()
- }, [loadFullPrompt, session.id])
+ return () => {
+ generationRef.current += 1
+ }
+ }, [loadFullPrompt])
const displayText = (fullText ?? previewText).trim()
const showEmpty = !loading && !displayText