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

@@ -153,7 +153,7 @@ export function AccountSettingsPage() {
useEffect(() => {
loadData()
}, [])
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- initial account load; mutations call loadData explicitly
const loadData = async () => {
setIsLoading(true)

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

View File

@@ -40,7 +40,7 @@ export function MyTreesPage() {
useEffect(() => {
loadMyTrees()
}, [user?.id])
}, [user?.id]) // eslint-disable-line react-hooks/exhaustive-deps -- reload only when the owner identity changes
const loadMyTrees = async () => {
if (!user?.id) return

View File

@@ -118,7 +118,7 @@ export function ProceduralEditorPage() {
}
return () => { reset() }
}, [id])
}, [id]) // eslint-disable-line react-hooks/exhaustive-deps -- editor init is keyed to route id; store actions are stable
useEffect(() => {
useProceduralEditorStore.getState().validate()

View File

@@ -155,7 +155,7 @@ export function ProceduralNavigationPage() {
return () => {
if (timerRef.current) clearInterval(timerRef.current)
}
}, [treeId])
}, [treeId]) // eslint-disable-line react-hooks/exhaustive-deps -- session load is keyed to route tree id
// Check for PSA connection on mount
useEffect(() => {

View File

@@ -57,7 +57,7 @@ export function SessionDetailPage() {
if (id) {
loadSession()
}
}, [id])
}, [id]) // eslint-disable-line react-hooks/exhaustive-deps -- detail reload is keyed to route session id
// Auto-show rating modal for completed sessions with library steps
useEffect(() => {

View File

@@ -269,7 +269,7 @@ export default function SessionHistoryPage() {
<PageMeta title="Sessions" />
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
{/* Page heading */}
<div className="mb-6">
<div className="mb-6" data-testid="session-history-heading">
<h1 className="text-2xl font-heading font-bold text-foreground sm:text-3xl">Session History</h1>
<p className="mt-1 text-sm text-muted-foreground">View and manage your sessions</p>
</div>
@@ -279,6 +279,7 @@ export default function SessionHistoryPage() {
{TABS.map((tab) => (
<button
key={tab.id}
data-testid={`session-history-tab-${tab.id}`}
onClick={() => setActiveTab(tab.id)}
className={cn(
'px-4 py-2 text-sm transition-colors whitespace-nowrap',
@@ -614,6 +615,7 @@ export default function SessionHistoryPage() {
Close
</button>
<button
data-testid="flow-session-resume"
onClick={() => navigate(getSessionResumePath(session.tree_id, session.tree_snapshot?.tree_type), { state: { sessionId: session.id } })}
className="rounded-md bg-primary px-3 py-2 text-sm font-medium text-white hover:brightness-110 transition-all"
>

View File

@@ -234,7 +234,7 @@ export function TreeEditorPage() {
return () => {
reset()
}
}, [id, isEditMode, canCreateTrees])
}, [id, isEditMode, canCreateTrees]) // eslint-disable-line react-hooks/exhaustive-deps -- initialization is keyed to route/editability state
// Handle unsaved changes warning
useEffect(() => {
@@ -391,7 +391,7 @@ export function TreeEditorPage() {
} finally {
setSaving(false)
}
}, [isSaving, isEditMode, id, editorMode, getTreeForSave, markSaved, navigate])
}, [isSaving, isEditMode, id, editorMode, getTreeForSave, markSaved, navigate, setSaving])
const handlePublish = useCallback(async () => {
if (isSaving) return
@@ -472,7 +472,7 @@ export function TreeEditorPage() {
} finally {
setSaving(false)
}
}, [isSaving, isEditMode, id, editorMode, validate, getTreeForSave, markSaved, navigate])
}, [isSaving, isEditMode, id, editorMode, validate, getTreeForSave, markSaved, navigate, setSaving])
// Keep handleSave for backward compatibility (Ctrl+S shortcut)
const handleSave = useCallback(async () => {

View File

@@ -292,7 +292,7 @@ export function TreeNavigationPage() {
if (treeId) {
loadTreeAndSession()
}
}, [treeId])
}, [treeId]) // eslint-disable-line react-hooks/exhaustive-deps -- route tree id is the load boundary
// Check for PSA connection on mount
useEffect(() => {