wip(handoff): start issue cleanup plan sections 1 and 2

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
2026-05-01 02:04:19 -04:00
parent a21fe93454
commit 4d8b107121
23 changed files with 231 additions and 105 deletions

View File

@@ -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 },