fix: preserve task lane answers across page reload and browser close

Two issues fixed:

1. TaskLane useEffect on [questions, actions] was resetting all tasks
   to pending with empty values, wiping restored user answers. Now
   checks sessionStorage for saved state before resetting.

2. selectChat was setting activeQuestions/activeActions before writing
   responses to sessionStorage, causing a race where TaskLane mounted
   with new props but empty sessionStorage. Now writes responses to
   sessionStorage first so TaskLane can restore them on prop change.

The backend saveTaskLane debounce (2s) persists responses to the DB,
and selectChat restores them via pending_task_lane.responses. This
chain now survives browser close.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-29 12:53:16 +00:00
parent bc6afbc90a
commit d5122123c2
2 changed files with 15 additions and 6 deletions

View File

@@ -147,8 +147,16 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) } return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) }
}, [sessionId, tasks]) // eslint-disable-line react-hooks/exhaustive-deps }, [sessionId, tasks]) // eslint-disable-line react-hooks/exhaustive-deps
// Reset when new tasks come in from AI response // Reset when new tasks come in from AI response — but preserve saved state
useEffect(() => { useEffect(() => {
if (sessionId) {
const saved = loadTaskState(sessionId)
if (saved && saved.length > 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: syncs derived state from prop changes
setTasks(saved)
return
}
}
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: syncs derived state from prop changes // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: syncs derived state from prop changes
setTasks([ setTasks([
...questions.map((q): QuestionResponse => ({ ...questions.map((q): QuestionResponse => ({
@@ -158,7 +166,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '', type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '',
})), })),
]) ])
}, [questions, actions]) }, [questions, actions]) // eslint-disable-line react-hooks/exhaustive-deps
const updateTask = (idx: number, updates: Partial<TaskResponse>) => { const updateTask = (idx: number, updates: Partial<TaskResponse>) => {
setTasks(prev => prev.map((t, i) => i === idx ? { ...t, ...updates } as TaskResponse : t)) setTasks(prev => prev.map((t, i) => i === idx ? { ...t, ...updates } as TaskResponse : t))

View File

@@ -232,16 +232,17 @@ export default function AssistantChatPage() {
const q = detail.pending_task_lane.questions || [] const q = detail.pending_task_lane.questions || []
const a = detail.pending_task_lane.actions || [] const a = detail.pending_task_lane.actions || []
if (q.length > 0 || a.length > 0) { if (q.length > 0 || a.length > 0) {
setActiveQuestions(q) // Pre-load user's saved responses into sessionStorage BEFORE setting props
setActiveActions(a) // so TaskLane can restore them on mount/prop-change
setShowTaskLane(true)
// Pre-load user's saved responses into sessionStorage so TaskLane restores them
const responses = (detail.pending_task_lane as Record<string, unknown>).responses as unknown[] | undefined const responses = (detail.pending_task_lane as Record<string, unknown>).responses as unknown[] | undefined
if (responses && responses.length > 0) { if (responses && responses.length > 0) {
try { try {
sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses)) sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses))
} catch { /* ignore */ } } catch { /* ignore */ }
} }
setActiveQuestions(q)
setActiveActions(a)
setShowTaskLane(true)
} }
} }
} catch { } catch {