fix: resolve task lane stale state, partial submit, and closure bugs
- Import and call clearTaskState before updating questions/actions in handleSend and handleTaskSubmit so new AI tasks always replace stale sessionStorage cache instead of being overridden by it - Include pending (not yet completed) tasks in the AI message on partial submit so the AI knows which tasks were left unanswered - Fix stale closure in TaskLane saveTaskLane useEffect — use refs for questions/actions so the debounced backend save always uses current values - Add responses field to pending_task_lane TypeScript type, removing the unsafe double-cast in selectChat - Instruct the AI to re-surface incomplete tasks unless ≥75% confident the information is no longer needed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,9 @@ scope narrows it to this endpoint.
|
|||||||
- JSON array of objects with `text` (required) and `context` (optional, 1 sentence)
|
- JSON array of objects with `text` (required) and `context` (optional, 1 sentence)
|
||||||
- 1-3 questions per response
|
- 1-3 questions per response
|
||||||
- Do NOT ask questions inline in your prose. ALL questions go in the marker.
|
- Do NOT ask questions inline in your prose. ALL questions go in the marker.
|
||||||
|
- If the engineer's message contains tasks marked `_(not yet completed)_`, re-include \
|
||||||
|
those as questions/actions in your next response UNLESS you are ≥75% confident the \
|
||||||
|
information is no longer needed to resolve the issue. Default to keeping them.
|
||||||
|
|
||||||
**[ACTIONS] marker format:**
|
**[ACTIONS] marker format:**
|
||||||
- JSON array of objects with `label` (required), `command` (optional), `description` (required)
|
- JSON array of objects with `label` (required), `command` (optional), `description` (required)
|
||||||
@@ -155,6 +158,8 @@ To create a fork, append this marker AFTER your [QUESTIONS]/[ACTIONS] markers:
|
|||||||
Every single response MUST contain [QUESTIONS] and/or [ACTIONS] markers with valid JSON. \
|
Every single response MUST contain [QUESTIONS] and/or [ACTIONS] markers with valid JSON. \
|
||||||
No exceptions. Not even when forking. A response without at least one of these markers \
|
No exceptions. Not even when forking. A response without at least one of these markers \
|
||||||
will crash the UI. If you are unsure, include both. The markers are REQUIRED output, not optional.
|
will crash the UI. If you are unsure, include both. The markers are REQUIRED output, not optional.
|
||||||
|
If any tasks in the engineer's message are marked `_(not yet completed)_`, re-include them \
|
||||||
|
in your markers unless you are ≥75% confident that information is no longer relevant.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,14 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
}
|
}
|
||||||
}, [handleMouseMove, handleMouseUp])
|
}, [handleMouseMove, handleMouseUp])
|
||||||
|
|
||||||
|
// Refs so the debounced save always uses the latest questions/actions/tasks
|
||||||
|
const questionsRef = useRef(questions)
|
||||||
|
const actionsRef = useRef(actions)
|
||||||
|
const tasksRef = useRef(tasks)
|
||||||
|
useEffect(() => { questionsRef.current = questions }, [questions])
|
||||||
|
useEffect(() => { actionsRef.current = actions }, [actions])
|
||||||
|
useEffect(() => { tasksRef.current = tasks }, [tasks])
|
||||||
|
|
||||||
// Save task state to sessionStorage on every change + debounce to backend
|
// Save task state to sessionStorage on every change + debounce to backend
|
||||||
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -139,9 +147,9 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
|
||||||
saveTimerRef.current = setTimeout(() => {
|
saveTimerRef.current = setTimeout(() => {
|
||||||
aiSessionsApi.saveTaskLane(sessionId, {
|
aiSessionsApi.saveTaskLane(sessionId, {
|
||||||
questions: questions.map(q => ({ text: q.text, context: q.context })),
|
questions: questionsRef.current.map(q => ({ text: q.text, context: q.context })),
|
||||||
actions: actions.map(a => ({ label: a.label, command: a.command, description: a.description })),
|
actions: actionsRef.current.map(a => ({ label: a.label, command: a.command, description: a.description })),
|
||||||
responses: tasks as unknown as Array<Record<string, unknown>>,
|
responses: tasksRef.current as unknown as Array<Record<string, unknown>>,
|
||||||
}).catch(() => { /* silent — best-effort save */ })
|
}).catch(() => { /* silent — best-effort save */ })
|
||||||
}, 2000)
|
}, 2000)
|
||||||
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) }
|
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { analytics } from '@/lib/analytics'
|
|||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
|
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
|
||||||
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
||||||
import { TaskLane } from '@/components/assistant/TaskLane'
|
import { TaskLane, clearTaskState } from '@/components/assistant/TaskLane'
|
||||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||||
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
||||||
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
||||||
@@ -242,7 +242,7 @@ export default function AssistantChatPage() {
|
|||||||
if (q.length > 0 || a.length > 0) {
|
if (q.length > 0 || a.length > 0) {
|
||||||
// Pre-load user's saved responses into sessionStorage BEFORE setting props
|
// Pre-load user's saved responses into sessionStorage BEFORE setting props
|
||||||
// so TaskLane can restore them on mount/prop-change
|
// so TaskLane can restore them on mount/prop-change
|
||||||
const responses = (detail.pending_task_lane as Record<string, unknown>).responses as unknown[] | undefined
|
const responses = detail.pending_task_lane.responses
|
||||||
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))
|
||||||
@@ -340,6 +340,7 @@ export default function AssistantChatPage() {
|
|||||||
const hasQuestions = response.questions && response.questions.length > 0
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
const hasActions = response.actions && response.actions.length > 0
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
if (hasQuestions || hasActions) {
|
if (hasQuestions || hasActions) {
|
||||||
|
if (activeChatId) clearTaskState(activeChatId)
|
||||||
setActiveQuestions(response.questions || [])
|
setActiveQuestions(response.questions || [])
|
||||||
setActiveActions(response.actions || [])
|
setActiveActions(response.actions || [])
|
||||||
setShowTaskLane(true)
|
setShowTaskLane(true)
|
||||||
@@ -358,7 +359,8 @@ export default function AssistantChatPage() {
|
|||||||
const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
|
const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
|
||||||
if (!activeChatId || loading) return
|
if (!activeChatId || loading) return
|
||||||
|
|
||||||
// Format task responses into a structured message for the AI
|
// Format task responses into a structured message for the AI.
|
||||||
|
// Pending tasks are included so the AI knows they weren't completed yet.
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
for (const r of responses) {
|
for (const r of responses) {
|
||||||
const name = r.type === 'question' ? `Q: ${r.text}` : r.label || 'Check'
|
const name = r.type === 'question' ? `Q: ${r.text}` : r.label || 'Check'
|
||||||
@@ -366,6 +368,8 @@ export default function AssistantChatPage() {
|
|||||||
parts.push(`**${name}:**\n\`\`\`\n${r.value.trim()}\n\`\`\``)
|
parts.push(`**${name}:**\n\`\`\`\n${r.value.trim()}\n\`\`\``)
|
||||||
} else if (r.state === 'skipped') {
|
} else if (r.state === 'skipped') {
|
||||||
parts.push(`**${name}:** _(skipped)_`)
|
parts.push(`**${name}:** _(skipped)_`)
|
||||||
|
} else {
|
||||||
|
parts.push(`**${name}:** _(not yet completed)_`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userMessage = parts.join('\n\n')
|
const userMessage = parts.join('\n\n')
|
||||||
@@ -385,6 +389,7 @@ export default function AssistantChatPage() {
|
|||||||
// Update task lane based on AI response
|
// Update task lane based on AI response
|
||||||
const hasQuestions = response.questions && response.questions.length > 0
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
const hasActions = response.actions && response.actions.length > 0
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
|
if (activeChatId) clearTaskState(activeChatId)
|
||||||
if (hasQuestions || hasActions) {
|
if (hasQuestions || hasActions) {
|
||||||
setActiveQuestions(response.questions || [])
|
setActiveQuestions(response.questions || [])
|
||||||
setActiveActions(response.actions || [])
|
setActiveActions(response.actions || [])
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export interface AISessionDetail extends AISessionSummary {
|
|||||||
ticket_data: Record<string, unknown> | null
|
ticket_data: Record<string, unknown> | null
|
||||||
steps: AISessionStepResponse[]
|
steps: AISessionStepResponse[]
|
||||||
conversation_messages: Array<{ role: string; content: string }>
|
conversation_messages: Array<{ role: string; content: string }>
|
||||||
pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[] } | null
|
pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[]; responses?: Array<Record<string, unknown>> } | null
|
||||||
is_branching: boolean
|
is_branching: boolean
|
||||||
active_branch_id: string | null
|
active_branch_id: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user