wip(handoff): start issue cleanup plan sections 1 and 2
Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
@@ -267,6 +267,15 @@ export default function AssistantChatPage() {
|
||||
// path: post-claim the chat surface had no messages and the senior
|
||||
// landed on a blank pane).
|
||||
const loadedChatIdsRef = useRef<Set<string>>(new Set())
|
||||
const guardCurrentChat = useCallback((expectedChatId: string, source: string) => {
|
||||
if (currentChatRef.current === expectedChatId) return true
|
||||
console.warn('[AssistantChat] Discarded stale async result', {
|
||||
source,
|
||||
expectedChatId,
|
||||
currentChatId: currentChatRef.current,
|
||||
})
|
||||
return false
|
||||
}, [])
|
||||
|
||||
// Persist active chat ID to sessionStorage
|
||||
useEffect(() => {
|
||||
@@ -612,7 +621,7 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
window.addEventListener(PILOT_INLINE_SCRIPT_EVENT, handler as EventListener)
|
||||
return () => window.removeEventListener(PILOT_INLINE_SCRIPT_EVENT, handler as EventListener)
|
||||
}, [activeFix])
|
||||
}, [activeFix, activeChatId])
|
||||
|
||||
const loadChats = async () => {
|
||||
try {
|
||||
@@ -684,7 +693,7 @@ export default function AssistantChatPage() {
|
||||
try {
|
||||
const list = await sessionFactsApi.list(chatId)
|
||||
// Guard: discard stale fetch if the user switched chats mid-flight.
|
||||
if (currentChatRef.current !== chatId) return
|
||||
if (!guardCurrentChat(chatId, 'refreshFacts')) return
|
||||
setFacts(list)
|
||||
// Auto-open the task lane when the session has facts so the engineer
|
||||
// can see them — without this, a session with only facts (no open
|
||||
@@ -699,7 +708,7 @@ export default function AssistantChatPage() {
|
||||
// Best-effort — facts are accessory state. Surfacing a toast on every
|
||||
// refetch failure would be noisy; the empty state explains the absence.
|
||||
}
|
||||
}, [])
|
||||
}, [guardCurrentChat])
|
||||
|
||||
// Phase 3 — active suggested fix + resolution-note preview.
|
||||
// Declared BEFORE refreshSessionDerived / handleAddNote so the useCallback
|
||||
@@ -707,7 +716,7 @@ export default function AssistantChatPage() {
|
||||
const refreshActiveFix = useCallback(async (chatId: string) => {
|
||||
try {
|
||||
const fix = await sessionSuggestedFixesApi.getActive(chatId)
|
||||
if (currentChatRef.current !== chatId) return
|
||||
if (!guardCurrentChat(chatId, 'refreshActiveFix')) return
|
||||
setActiveFix((prev) => {
|
||||
// If the active fix changed (AI emitted a new SUGGEST_FIX that
|
||||
// superseded the prior), close the script panel so the engineer
|
||||
@@ -719,7 +728,7 @@ export default function AssistantChatPage() {
|
||||
// No-fix-yet (404) is normalized to null inside the client. Genuine
|
||||
// failures stay silent — accessory state, not load-bearing.
|
||||
}
|
||||
}, [])
|
||||
}, [guardCurrentChat])
|
||||
|
||||
// Kind-aware preview fetch: Resolve hits /resolution-note/preview,
|
||||
// Escalate hits /escalation-package/preview. They're cached separately
|
||||
@@ -733,7 +742,7 @@ export default function AssistantChatPage() {
|
||||
const p = effectiveKind === 'resolve'
|
||||
? await sessionSuggestedFixesApi.getResolutionNotePreview(chatId)
|
||||
: await sessionSuggestedFixesApi.getEscalationPackagePreview(chatId)
|
||||
if (currentChatRef.current !== chatId) return
|
||||
if (!guardCurrentChat(chatId, 'refreshPreview')) return
|
||||
setPreviewData(p)
|
||||
} catch (err: unknown) {
|
||||
const status = (err as { response?: { status?: number } })?.response?.status
|
||||
@@ -745,7 +754,7 @@ export default function AssistantChatPage() {
|
||||
} finally {
|
||||
setPreviewLoading(false)
|
||||
}
|
||||
}, [previewKind])
|
||||
}, [guardCurrentChat, previewKind])
|
||||
|
||||
// Trigger preview refresh with a 500ms debounce. The backend cache short-
|
||||
// circuits same-state calls, but the network round-trip is still avoidable
|
||||
@@ -880,7 +889,7 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
// No draft, no template — route to the Script Builder tab.
|
||||
setChatTab('script_builder')
|
||||
}, [activeFix])
|
||||
}, [activeFix, activeChatId])
|
||||
|
||||
// Phase 9 Task 13: TemplateMatchPanel "I ran this" — stamps applied_at so the
|
||||
// ProposalBanner transitions from Proposed to Verifying. Shared useCallback so
|
||||
@@ -1108,13 +1117,13 @@ export default function AssistantChatPage() {
|
||||
// Guard: if the user switched to a different chat while this API call was
|
||||
// in flight (e.g. clicked "New Chat"), discard stale results so we don't
|
||||
// clobber the new session's task lane state.
|
||||
if (currentChatRef.current !== chatId) return
|
||||
if (!guardCurrentChat(chatId, 'selectChat')) return
|
||||
setActiveSessionStatus(detail.status)
|
||||
setActivePsaTicketId(detail.psa_ticket_id)
|
||||
if (detail.psa_ticket_id) {
|
||||
integrationsApi.getTicket(detail.psa_ticket_id)
|
||||
.then(ticket => {
|
||||
if (currentChatRef.current !== chatId) return
|
||||
if (!guardCurrentChat(chatId, 'selectChat.ticket')) return
|
||||
setLinkedTicket(ticket)
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -1149,7 +1158,7 @@ export default function AssistantChatPage() {
|
||||
} catch {
|
||||
setMessages([])
|
||||
}
|
||||
}, [refreshSessionDerived])
|
||||
}, [guardCurrentChat, refreshSessionDerived, resetSessionDerivedState])
|
||||
|
||||
const handleAIAnalysis = useCallback(async () => {
|
||||
if (!urlSessionId || !magicHandoff) return
|
||||
@@ -1162,7 +1171,7 @@ export default function AssistantChatPage() {
|
||||
setMagicState('dismissed')
|
||||
void loadChats()
|
||||
await selectChat(urlSessionId)
|
||||
if (currentChatRef.current !== sentForChatId) return
|
||||
if (!guardCurrentChat(sentForChatId, 'handleAIAnalysis.afterSelect')) return
|
||||
|
||||
const assessment = magicHandoff.ai_assessment_data
|
||||
const snapshot = magicHandoff.snapshot as Record<string, unknown>
|
||||
@@ -1192,7 +1201,7 @@ export default function AssistantChatPage() {
|
||||
setMessages(prev => [...prev, { role: 'user', content: briefing }])
|
||||
setLoading(true)
|
||||
const response = await aiSessionsApi.sendChatMessage(urlSessionId, { message: briefing })
|
||||
if (currentChatRef.current !== sentForChatId) return
|
||||
if (!guardCurrentChat(sentForChatId, 'handleAIAnalysis.chatResponse')) return
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
@@ -1233,7 +1242,7 @@ export default function AssistantChatPage() {
|
||||
setActiveOptionKey(null)
|
||||
setLoading(false)
|
||||
}
|
||||
}, [urlSessionId, magicHandoff, setSearchParams, selectChat])
|
||||
}, [guardCurrentChat, urlSessionId, magicHandoff, setSearchParams, selectChat])
|
||||
|
||||
const handleNewChat = async () => {
|
||||
// Invalidate currentChatRef BEFORE the await so any in-flight handleSend/handleTaskSubmit
|
||||
@@ -1306,7 +1315,7 @@ export default function AssistantChatPage() {
|
||||
upload_ids: completedUploadIds.length > 0 ? completedUploadIds : undefined,
|
||||
})
|
||||
// Guard: discard if user switched to a different chat while this was in flight
|
||||
if (currentChatRef.current !== sentForChatId) return
|
||||
if (!guardCurrentChat(sentForChatId, 'handleSend')) return
|
||||
analytics.aiFeatureUsed({ feature: 'assistant_chat' })
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
@@ -1396,7 +1405,7 @@ export default function AssistantChatPage() {
|
||||
try {
|
||||
const response = await aiSessionsApi.sendChatMessage(activeChatId, { message: userMessage })
|
||||
// Guard: discard if user switched to a different chat while this was in flight
|
||||
if (currentChatRef.current !== sentForChatId) return
|
||||
if (!guardCurrentChat(sentForChatId, 'handleTaskSubmit')) return
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||
@@ -1491,7 +1500,7 @@ export default function AssistantChatPage() {
|
||||
|
||||
const response = await aiSessionsApi.sendChatMessage(session.session_id, { message: resumePrompt })
|
||||
// Guard: discard if user switched to a different chat while this was in flight
|
||||
if (currentChatRef.current !== session.session_id) return
|
||||
if (!guardCurrentChat(session.session_id, 'handleResumeNew')) return
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||
|
||||
Reference in New Issue
Block a user