fix(assistant-chat): kill stale task-lane flash on new-session entry
Two compounding bugs caused the previous session's questions/actions
to render briefly when entering a new chat — visible as "the new
session instantly pops with old session task-lane data" the user
reported.
The race
- AssistantChatPage's activeQuestions / activeActions / showTaskLane
useState initializers synchronously read sessionStorage's
rf-tasklane-meta. They restore the persisted task-lane state if its
saved chatId matches the freshly-resolved activeChatId.
- On dashboard prefill flow, the page mounts on /pilot with
location.state.prefill set; activeChatId initializes from
sessionStorage's rf-active-chat-id (the previous session). The
previous session's task-lane meta matches that chatId — so the
initializer restores it. First paint shows old questions/actions.
sendPrefill's resetSessionDerivedState fires later from a useEffect,
but only after the flash.
- Same pattern hits the senior-pickup flow: ?pickup=true means we're
about to render the magic-moment screen and discard whatever chat
the senior was previously on, but the underlying chat surface still
initializes with their old task-lane meta.
The amplifier
- resetSessionDerivedState wiped the in-memory state but never
removed sessionStorage's rf-tasklane-meta. Any remount or reload
before the next persistence-effect write could re-hydrate the
cleared state from the still-stale sessionStorage entry.
Fixes
- Initializer guard: when location.state.prefill is set OR
?pickup=true is in the URL, skip the sessionStorage restore
entirely. Kills the first-paint flash for both entry paths.
- Eager wipe: resetSessionDerivedState now also calls
sessionStorage.removeItem('rf-tasklane-meta'). The persistence
effect re-saves on the next state change anyway, so the only
window where sessionStorage is empty is the exact window where
stale-tag leakage was happening.
tsc -b clean. No backend changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -97,7 +97,21 @@ export default function AssistantChatPage() {
|
||||
const [logContent, setLogContent] = useState('')
|
||||
const [pendingUploads, setPendingUploads] = useState<PendingUpload[]>([])
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
// Task-lane mount restoration is gated on (a) the persisted chatId
|
||||
// matching whatever activeChatId resolved to, AND (b) the page not being
|
||||
// entered with a prefill in location.state. The prefill case means we're
|
||||
// about to create a brand-new session and discard the previous one's
|
||||
// task lane anyway — restoring it just causes the previous chat's
|
||||
// questions/actions to flash on the first paint before sendPrefill's
|
||||
// resetSessionDerivedState clears them. Same logic for the bell-icon
|
||||
// pickup flow (?pickup=true): the senior is entering an unrelated
|
||||
// session and any leftover task-lane meta from their own prior chat is
|
||||
// noise. Both gates collapse to "are we about to leave the previous
|
||||
// chat behind?" — if yes, start clean.
|
||||
const incomingPrefill = !!(location.state as { prefill?: string } | null)?.prefill
|
||||
const skipTaskLaneRestore = incomingPrefill || isPickup
|
||||
const [activeQuestions, setActiveQuestions] = useState<QuestionItem[]>(() => {
|
||||
if (skipTaskLaneRestore) return []
|
||||
try {
|
||||
const saved = sessionStorage.getItem('rf-tasklane-meta')
|
||||
if (saved) { const d = JSON.parse(saved); if (d.chatId === activeChatId) return d.questions || [] }
|
||||
@@ -105,6 +119,7 @@ export default function AssistantChatPage() {
|
||||
return []
|
||||
})
|
||||
const [activeActions, setActiveActions] = useState<ActionItem[]>(() => {
|
||||
if (skipTaskLaneRestore) return []
|
||||
try {
|
||||
const saved = sessionStorage.getItem('rf-tasklane-meta')
|
||||
if (saved) { const d = JSON.parse(saved); if (d.chatId === activeChatId) return d.actions || [] }
|
||||
@@ -112,6 +127,7 @@ export default function AssistantChatPage() {
|
||||
return []
|
||||
})
|
||||
const [showTaskLane, setShowTaskLane] = useState(() => {
|
||||
if (skipTaskLaneRestore) return false
|
||||
try {
|
||||
const saved = sessionStorage.getItem('rf-tasklane-meta')
|
||||
if (saved) { const d = JSON.parse(saved); return d.show === true && d.chatId === activeChatId }
|
||||
@@ -479,6 +495,16 @@ export default function AssistantChatPage() {
|
||||
// Phase 9: tab strip reset
|
||||
setChatTab('chat')
|
||||
setScriptBuilderHasProgress(false)
|
||||
// Belt-and-braces: also wipe the persisted task-lane meta. Without this,
|
||||
// a remount or page reload before the next AI response can re-hydrate
|
||||
// the previous session's questions/actions from sessionStorage even
|
||||
// though the in-memory state has been cleared. The persistence effect
|
||||
// re-saves on the next state change anyway, so the only window where
|
||||
// sessionStorage is empty is between this reset and the next response —
|
||||
// which is exactly the window where stale-tag leakage was happening.
|
||||
try {
|
||||
sessionStorage.removeItem('rf-tasklane-meta')
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
// Phase 2 facts — fetch + handlers. `refreshFacts` is called from selectChat
|
||||
|
||||
Reference in New Issue
Block a user