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 prefillHandledRef = useRef(false)
|
||||
const currentChatRef = useRef<string | null>(activeChatId)
|
||||
const loadingRef = useRef(false)
|
||||
const initialLoadDoneRef = useRef(false)
|
||||
|
||||
const toggleSidebarCollapse = () => {
|
||||
const next = !sidebarCollapsed
|
||||
@@ -91,20 +93,22 @@ export function useAssistantSession() {
|
||||
// Load chat list on mount
|
||||
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(() => {
|
||||
if (urlSessionId && urlSessionId !== activeChatId) {
|
||||
selectChat(urlSessionId)
|
||||
if (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
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -206,6 +210,8 @@ export function useAssistantSession() {
|
||||
}, [])
|
||||
|
||||
const handleNewChat = async () => {
|
||||
if (loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
try {
|
||||
const session = await aiSessionsApi.createChatSession({
|
||||
intake_type: 'free_text',
|
||||
@@ -228,6 +234,8 @@ export function useAssistantSession() {
|
||||
setActiveActions([])
|
||||
} catch {
|
||||
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 handleSend = async () => {
|
||||
if (!input.trim() || !activeChatId || loading) return
|
||||
if (!input.trim() || !activeChatId || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
|
||||
const sendChatId = activeChatId
|
||||
const userMessage = input.trim()
|
||||
@@ -315,6 +324,7 @@ export function useAssistantSession() {
|
||||
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
|
||||
])
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
if (currentChatRef.current === sendChatId) {
|
||||
setLoading(false)
|
||||
requestAnimationFrame(() => inputRef.current?.focus())
|
||||
@@ -333,9 +343,12 @@ export function useAssistantSession() {
|
||||
navigate(location.pathname, { replace: true, state: {} })
|
||||
|
||||
const sendPrefill = async () => {
|
||||
if (loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setShowTaskLane(false)
|
||||
setActiveQuestions([])
|
||||
setActiveActions([])
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const session = await aiSessionsApi.createChatSession({
|
||||
@@ -355,7 +368,6 @@ export function useAssistantSession() {
|
||||
setChats(prev => [chatItem, ...prev])
|
||||
setActiveChatId(prefillChatId)
|
||||
setMessages([{ role: 'user', content: prefill }])
|
||||
setLoading(true)
|
||||
|
||||
const response = await aiSessionsApi.sendChatMessage(prefillChatId, {
|
||||
message: prefill,
|
||||
@@ -374,6 +386,7 @@ export function useAssistantSession() {
|
||||
} catch {
|
||||
toast.error('Failed to start AI conversation')
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -401,6 +414,9 @@ export function useAssistantSession() {
|
||||
}
|
||||
|
||||
const handleResumeNew = async (summary: string) => {
|
||||
if (loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
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 session = await aiSessionsApi.createChatSession({
|
||||
@@ -419,7 +435,6 @@ export function useAssistantSession() {
|
||||
setChats(prev => [chatItem, ...prev])
|
||||
setActiveChatId(session.session_id)
|
||||
setMessages([{ role: 'user', content: resumePrompt }])
|
||||
setLoading(true)
|
||||
|
||||
const resumeChatId = session.session_id
|
||||
const response = await aiSessionsApi.sendChatMessage(resumeChatId, { message: resumePrompt })
|
||||
@@ -436,6 +451,7 @@ export function useAssistantSession() {
|
||||
} catch {
|
||||
toast.error('Failed to create resume chat')
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -528,7 +544,7 @@ export function useAssistantSession() {
|
||||
handleFileSelect, handleRemoveUpload, retryUpload,
|
||||
toggleSidebarCollapse, handlePrefill, processResponse,
|
||||
// Refs
|
||||
messagesEndRef, inputRef, fileInputRef, currentChatRef,
|
||||
messagesEndRef, inputRef, fileInputRef, currentChatRef, loadingRef,
|
||||
// Page-specific callbacks
|
||||
onSessionLoadedRef, onTriageUpdateRef,
|
||||
// Constants
|
||||
|
||||
@@ -99,20 +99,26 @@ export default function CockpitPage() {
|
||||
|
||||
const handleEvidenceAdd = useCallback(async (text: string, status: EvidenceItem['status']) => {
|
||||
const newItem: EvidenceItem = { text, status }
|
||||
const updated = [...triageMeta.evidence_items, newItem]
|
||||
setTriageMeta(prev => ({ ...prev, evidence_items: updated }))
|
||||
let updated: EvidenceItem[] = []
|
||||
setTriageMeta(prev => {
|
||||
updated = [...prev.evidence_items, newItem]
|
||||
return { ...prev, evidence_items: updated }
|
||||
})
|
||||
if (session.activeChatId) {
|
||||
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 updated = triageMeta.evidence_items.map((item, i) => i === index ? { text, status } : item)
|
||||
setTriageMeta(prev => ({ ...prev, evidence_items: updated }))
|
||||
let updated: EvidenceItem[] = []
|
||||
setTriageMeta(prev => {
|
||||
updated = prev.evidence_items.map((item, i) => i === index ? { text, status } : item)
|
||||
return { ...prev, evidence_items: updated }
|
||||
})
|
||||
if (session.activeChatId) {
|
||||
try { await aiSessionsApi.updateTriage(session.activeChatId, { evidence_items: updated }) } catch { /* best-effort */ }
|
||||
}
|
||||
}, [session.activeChatId, triageMeta.evidence_items])
|
||||
}, [session.activeChatId])
|
||||
|
||||
const handleStepComplete = useCallback((index: number) => {
|
||||
setCompletedSteps(prev => {
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function FlowPilotPage() {
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
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[] = []
|
||||
for (const r of responses) {
|
||||
|
||||
Reference in New Issue
Block a user