fix: resolve race conditions in assistant/cockpit session loading
- Always load session data on mount even when urlSessionId matches activeChatId, fixing empty state after view toggle between /assistant and /cockpit (tasks/messages not showing until sidebar click) - Add loadingRef for synchronous guards preventing duplicate sends, duplicate session creation, and prefill races - Fix stale evidence_items closure in CockpitPage handlers - Move setLoading(true) before first await in handlePrefill and handleResumeNew Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,8 @@ export function useAssistantSession() {
|
|||||||
const dragCounterRef = useRef(0)
|
const dragCounterRef = useRef(0)
|
||||||
const prefillHandledRef = useRef(false)
|
const prefillHandledRef = useRef(false)
|
||||||
const currentChatRef = useRef<string | null>(activeChatId)
|
const currentChatRef = useRef<string | null>(activeChatId)
|
||||||
|
const loadingRef = useRef(false)
|
||||||
|
const initialLoadDoneRef = useRef(false)
|
||||||
|
|
||||||
const toggleSidebarCollapse = () => {
|
const toggleSidebarCollapse = () => {
|
||||||
const next = !sidebarCollapsed
|
const next = !sidebarCollapsed
|
||||||
@@ -91,20 +93,22 @@ export function useAssistantSession() {
|
|||||||
// Load chat list on mount
|
// Load chat list on mount
|
||||||
useEffect(() => { loadChats() }, [])
|
useEffect(() => { loadChats() }, [])
|
||||||
|
|
||||||
// If URL has a session ID, load it
|
// Load session data on mount or when URL session changes.
|
||||||
|
// On initial mount, always load even if activeChatId matches urlSessionId
|
||||||
|
// (state is empty after view toggle between /assistant and /cockpit).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlSessionId && urlSessionId !== activeChatId) {
|
if (urlSessionId) {
|
||||||
selectChat(urlSessionId)
|
if (!initialLoadDoneRef.current || urlSessionId !== activeChatId) {
|
||||||
|
selectChat(urlSessionId)
|
||||||
|
}
|
||||||
|
initialLoadDoneRef.current = true
|
||||||
|
} else if (!initialLoadDoneRef.current && activeChatId) {
|
||||||
|
// Restore session from sessionStorage on mount (when URL has no session ID)
|
||||||
|
selectChat(activeChatId)
|
||||||
|
initialLoadDoneRef.current = true
|
||||||
}
|
}
|
||||||
}, [urlSessionId]) // eslint-disable-line react-hooks/exhaustive-deps
|
}, [urlSessionId]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Restore session from sessionStorage on mount (when URL has no session ID)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!urlSessionId && activeChatId) {
|
|
||||||
selectChat(activeChatId)
|
|
||||||
}
|
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
// Persist task lane metadata to sessionStorage
|
// Persist task lane metadata to sessionStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
@@ -206,6 +210,8 @@ export function useAssistantSession() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleNewChat = async () => {
|
const handleNewChat = async () => {
|
||||||
|
if (loadingRef.current) return
|
||||||
|
loadingRef.current = true
|
||||||
try {
|
try {
|
||||||
const session = await aiSessionsApi.createChatSession({
|
const session = await aiSessionsApi.createChatSession({
|
||||||
intake_type: 'free_text',
|
intake_type: 'free_text',
|
||||||
@@ -228,6 +234,8 @@ export function useAssistantSession() {
|
|||||||
setActiveActions([])
|
setActiveActions([])
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to create chat')
|
toast.error('Failed to create chat')
|
||||||
|
} finally {
|
||||||
|
loadingRef.current = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +288,8 @@ export function useAssistantSession() {
|
|||||||
const onTriageUpdateRef = useRef<((update: TriageUpdate) => void) | null>(null)
|
const onTriageUpdateRef = useRef<((update: TriageUpdate) => void) | null>(null)
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!input.trim() || !activeChatId || loading) return
|
if (!input.trim() || !activeChatId || loadingRef.current) return
|
||||||
|
loadingRef.current = true
|
||||||
|
|
||||||
const sendChatId = activeChatId
|
const sendChatId = activeChatId
|
||||||
const userMessage = input.trim()
|
const userMessage = input.trim()
|
||||||
@@ -315,6 +324,7 @@ export function useAssistantSession() {
|
|||||||
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
|
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
|
||||||
])
|
])
|
||||||
} finally {
|
} finally {
|
||||||
|
loadingRef.current = false
|
||||||
if (currentChatRef.current === sendChatId) {
|
if (currentChatRef.current === sendChatId) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
requestAnimationFrame(() => inputRef.current?.focus())
|
requestAnimationFrame(() => inputRef.current?.focus())
|
||||||
@@ -333,9 +343,12 @@ export function useAssistantSession() {
|
|||||||
navigate(location.pathname, { replace: true, state: {} })
|
navigate(location.pathname, { replace: true, state: {} })
|
||||||
|
|
||||||
const sendPrefill = async () => {
|
const sendPrefill = async () => {
|
||||||
|
if (loadingRef.current) return
|
||||||
|
loadingRef.current = true
|
||||||
setShowTaskLane(false)
|
setShowTaskLane(false)
|
||||||
setActiveQuestions([])
|
setActiveQuestions([])
|
||||||
setActiveActions([])
|
setActiveActions([])
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await aiSessionsApi.createChatSession({
|
const session = await aiSessionsApi.createChatSession({
|
||||||
@@ -355,7 +368,6 @@ export function useAssistantSession() {
|
|||||||
setChats(prev => [chatItem, ...prev])
|
setChats(prev => [chatItem, ...prev])
|
||||||
setActiveChatId(prefillChatId)
|
setActiveChatId(prefillChatId)
|
||||||
setMessages([{ role: 'user', content: prefill }])
|
setMessages([{ role: 'user', content: prefill }])
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
const response = await aiSessionsApi.sendChatMessage(prefillChatId, {
|
const response = await aiSessionsApi.sendChatMessage(prefillChatId, {
|
||||||
message: prefill,
|
message: prefill,
|
||||||
@@ -374,6 +386,7 @@ export function useAssistantSession() {
|
|||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to start AI conversation')
|
toast.error('Failed to start AI conversation')
|
||||||
} finally {
|
} finally {
|
||||||
|
loadingRef.current = false
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,6 +414,9 @@ export function useAssistantSession() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleResumeNew = async (summary: string) => {
|
const handleResumeNew = async (summary: string) => {
|
||||||
|
if (loadingRef.current) return
|
||||||
|
loadingRef.current = true
|
||||||
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const resumePrompt = `I'm continuing a previous troubleshooting session. Here's where we left off:\n\n${summary}\n\nPlease review this context and help me continue from where we stopped.`
|
const resumePrompt = `I'm continuing a previous troubleshooting session. Here's where we left off:\n\n${summary}\n\nPlease review this context and help me continue from where we stopped.`
|
||||||
const session = await aiSessionsApi.createChatSession({
|
const session = await aiSessionsApi.createChatSession({
|
||||||
@@ -419,7 +435,6 @@ export function useAssistantSession() {
|
|||||||
setChats(prev => [chatItem, ...prev])
|
setChats(prev => [chatItem, ...prev])
|
||||||
setActiveChatId(session.session_id)
|
setActiveChatId(session.session_id)
|
||||||
setMessages([{ role: 'user', content: resumePrompt }])
|
setMessages([{ role: 'user', content: resumePrompt }])
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
const resumeChatId = session.session_id
|
const resumeChatId = session.session_id
|
||||||
const response = await aiSessionsApi.sendChatMessage(resumeChatId, { message: resumePrompt })
|
const response = await aiSessionsApi.sendChatMessage(resumeChatId, { message: resumePrompt })
|
||||||
@@ -436,6 +451,7 @@ export function useAssistantSession() {
|
|||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to create resume chat')
|
toast.error('Failed to create resume chat')
|
||||||
} finally {
|
} finally {
|
||||||
|
loadingRef.current = false
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,7 +544,7 @@ export function useAssistantSession() {
|
|||||||
handleFileSelect, handleRemoveUpload, retryUpload,
|
handleFileSelect, handleRemoveUpload, retryUpload,
|
||||||
toggleSidebarCollapse, handlePrefill, processResponse,
|
toggleSidebarCollapse, handlePrefill, processResponse,
|
||||||
// Refs
|
// Refs
|
||||||
messagesEndRef, inputRef, fileInputRef, currentChatRef,
|
messagesEndRef, inputRef, fileInputRef, currentChatRef, loadingRef,
|
||||||
// Page-specific callbacks
|
// Page-specific callbacks
|
||||||
onSessionLoadedRef, onTriageUpdateRef,
|
onSessionLoadedRef, onTriageUpdateRef,
|
||||||
// Constants
|
// Constants
|
||||||
|
|||||||
@@ -99,20 +99,26 @@ export default function CockpitPage() {
|
|||||||
|
|
||||||
const handleEvidenceAdd = useCallback(async (text: string, status: EvidenceItem['status']) => {
|
const handleEvidenceAdd = useCallback(async (text: string, status: EvidenceItem['status']) => {
|
||||||
const newItem: EvidenceItem = { text, status }
|
const newItem: EvidenceItem = { text, status }
|
||||||
const updated = [...triageMeta.evidence_items, newItem]
|
let updated: EvidenceItem[] = []
|
||||||
setTriageMeta(prev => ({ ...prev, evidence_items: updated }))
|
setTriageMeta(prev => {
|
||||||
|
updated = [...prev.evidence_items, newItem]
|
||||||
|
return { ...prev, evidence_items: updated }
|
||||||
|
})
|
||||||
if (session.activeChatId) {
|
if (session.activeChatId) {
|
||||||
try { await aiSessionsApi.updateTriage(session.activeChatId, { evidence_items: updated }) } catch { /* best-effort */ }
|
try { await aiSessionsApi.updateTriage(session.activeChatId, { evidence_items: updated }) } catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
}, [session.activeChatId, triageMeta.evidence_items])
|
}, [session.activeChatId])
|
||||||
|
|
||||||
const handleEvidenceEdit = useCallback(async (index: number, text: string, status: EvidenceItem['status']) => {
|
const handleEvidenceEdit = useCallback(async (index: number, text: string, status: EvidenceItem['status']) => {
|
||||||
const updated = triageMeta.evidence_items.map((item, i) => i === index ? { text, status } : item)
|
let updated: EvidenceItem[] = []
|
||||||
setTriageMeta(prev => ({ ...prev, evidence_items: updated }))
|
setTriageMeta(prev => {
|
||||||
|
updated = prev.evidence_items.map((item, i) => i === index ? { text, status } : item)
|
||||||
|
return { ...prev, evidence_items: updated }
|
||||||
|
})
|
||||||
if (session.activeChatId) {
|
if (session.activeChatId) {
|
||||||
try { await aiSessionsApi.updateTriage(session.activeChatId, { evidence_items: updated }) } catch { /* best-effort */ }
|
try { await aiSessionsApi.updateTriage(session.activeChatId, { evidence_items: updated }) } catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
}, [session.activeChatId, triageMeta.evidence_items])
|
}, [session.activeChatId])
|
||||||
|
|
||||||
const handleStepComplete = useCallback((index: number) => {
|
const handleStepComplete = useCallback((index: number) => {
|
||||||
setCompletedSteps(prev => {
|
setCompletedSteps(prev => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function FlowPilotPage() {
|
|||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
|
const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
|
||||||
if (!session.activeChatId || session.loading) return
|
if (!session.activeChatId || session.loading || session.loadingRef.current) return
|
||||||
|
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
for (const r of responses) {
|
for (const r of responses) {
|
||||||
|
|||||||
Reference in New Issue
Block a user