feat: persist task lane state across reloads and session switches
TaskLane now saves user's in-progress answers (typed text, checked items) to sessionStorage keyed by session ID. On reload or session switch, the full task lane state restores — including partial work. - TaskLane: saves tasks array to sessionStorage on every change, restores from sessionStorage on mount - AssistantChatPage: saves task lane metadata (visibility, questions, actions, chatId) to sessionStorage, restores on mount - Closing the task lane clears its sessionStorage entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,22 +33,51 @@ type TaskResponse = QuestionResponse | ActionResponse
|
||||
interface TaskLaneProps {
|
||||
questions: QuestionItem[]
|
||||
actions: ActionItem[]
|
||||
sessionId?: string | null
|
||||
onSubmit: (responses: TaskResponse[]) => void
|
||||
onClose: () => void
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
// ── Storage helpers ──
|
||||
|
||||
const TASK_LANE_STORAGE_KEY = 'rf-tasklane-state'
|
||||
|
||||
function saveTaskState(sessionId: string, tasks: TaskResponse[]) {
|
||||
try {
|
||||
sessionStorage.setItem(`${TASK_LANE_STORAGE_KEY}:${sessionId}`, JSON.stringify(tasks))
|
||||
} catch { /* quota exceeded — ignore */ }
|
||||
}
|
||||
|
||||
function loadTaskState(sessionId: string): TaskResponse[] | null {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(`${TASK_LANE_STORAGE_KEY}:${sessionId}`)
|
||||
return stored ? JSON.parse(stored) : null
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
export function clearTaskState(sessionId: string) {
|
||||
try { sessionStorage.removeItem(`${TASK_LANE_STORAGE_KEY}:${sessionId}`) } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ── Component ──
|
||||
|
||||
export function TaskLane({ questions, actions, onSubmit, onClose, loading }: TaskLaneProps) {
|
||||
const [tasks, setTasks] = useState<TaskResponse[]>(() => [
|
||||
...questions.map((q): QuestionResponse => ({
|
||||
type: 'question', text: q.text, context: q.context, state: 'pending', value: '',
|
||||
})),
|
||||
...actions.map((a): ActionResponse => ({
|
||||
type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '',
|
||||
})),
|
||||
])
|
||||
export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loading }: TaskLaneProps) {
|
||||
const [tasks, setTasks] = useState<TaskResponse[]>(() => {
|
||||
// Try to restore saved state for this session (preserves user's in-progress answers)
|
||||
if (sessionId) {
|
||||
const saved = loadTaskState(sessionId)
|
||||
if (saved && saved.length > 0) return saved
|
||||
}
|
||||
return [
|
||||
...questions.map((q): QuestionResponse => ({
|
||||
type: 'question', text: q.text, context: q.context, state: 'pending', value: '',
|
||||
})),
|
||||
...actions.map((a): ActionResponse => ({
|
||||
type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '',
|
||||
})),
|
||||
]
|
||||
})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [showRunAll, setShowRunAll] = useState(false)
|
||||
const [showPreview, setShowPreview] = useState(false)
|
||||
@@ -100,6 +129,11 @@ export function TaskLane({ questions, actions, onSubmit, onClose, loading }: Tas
|
||||
}
|
||||
}, [handleMouseMove, handleMouseUp])
|
||||
|
||||
// Save task state to sessionStorage on every change
|
||||
useEffect(() => {
|
||||
if (sessionId) saveTaskState(sessionId, tasks)
|
||||
}, [sessionId, tasks])
|
||||
|
||||
// Reset when new tasks come in from AI response
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: syncs derived state from prop changes
|
||||
|
||||
Reference in New Issue
Block a user