From e8e12cc7e5e8daaf4f1edb4b6349e83ec6f52714 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 7 Apr 2026 06:31:24 +0000 Subject: [PATCH] fix: move session lifecycle actions to header bar in AssistantChatPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add persistent session header with title, status badge, Resolve, Escalate, and Update Ticket/Share Update buttons — mirrors FlowPilotSessionPage pattern exactly - Update Ticket label when psa_ticket_id present, Share Update otherwise - Full mobile support via ⋯ overflow menu (Resolve, Escalate, Update, Pause) - Strip _(not yet completed)_ markers from stored conversation_messages in unified_chat_service to prevent stale task lane items from prior turns leaking into new sessions via the AI's re-include instruction - Add currentChatRef guard to handleResumeNew (was missing unlike handleSend) - Remove Update/Conclude from chatbar — toolbar is now input utilities only Co-Authored-By: Claude Sonnet 4.6 --- backend/app/services/unified_chat_service.py | 12 +- frontend/src/pages/AssistantChatPage.tsx | 179 +++++++++++++++++-- 2 files changed, 176 insertions(+), 15 deletions(-) diff --git a/backend/app/services/unified_chat_service.py b/backend/app/services/unified_chat_service.py index 44236e66..4ed397ef 100644 --- a/backend/app/services/unified_chat_service.py +++ b/backend/app/services/unified_chat_service.py @@ -237,8 +237,10 @@ async def send_chat_message( ai_content, input_tokens, output_tokens = await _call_ai(**prompt_args) # Update branch conversation + # Strip _(not yet completed)_ markers before storage (same reason as main path) + stored_message = message.replace("_(not yet completed)_", "(pending)").replace("_(skipped)_", "(skipped)") msgs = list(branch.conversation_messages or []) - msgs.append({"role": "user", "content": message}) + msgs.append({"role": "user", "content": stored_message}) msgs.append({"role": "assistant", "content": ai_content}) branch.conversation_messages = msgs @@ -350,8 +352,14 @@ async def send_chat_message( # Store DISPLAY content (markers stripped) in conversation_messages. # The format reminder in the user message + system prompt final reminder # are sufficient to keep the AI emitting markers on subsequent turns. + # + # Strip _(not yet completed)_ task markers from the stored user message. + # The AI processes them correctly on the current turn, but persisting them + # into history causes the AI to re-inject stale task lane items from prior + # turns — even across unrelated topics in a long session. + stored_message = message.replace("_(not yet completed)_", "(pending)").replace("_(skipped)_", "(skipped)") msgs = list(session.conversation_messages or []) - msgs.append({"role": "user", "content": message}) + msgs.append({"role": "user", "content": stored_message}) msgs.append({"role": "assistant", "content": display_content}) session.conversation_messages = msgs session.step_count += 2 # message count for display diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index ebf0e2c7..ab500fec 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom' -import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText } from 'lucide-react' +import { Sparkles, Send, Loader2, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText, CheckCircle2, ArrowUpRight, MoreHorizontal, Pause } from 'lucide-react' import { cn } from '@/lib/utils' import { uploadsApi } from '@/api/uploads' import type { PendingUpload } from '@/types/upload' @@ -71,6 +71,9 @@ export default function AssistantChatPage() { const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem('rf-chat-sidebar-collapsed') === 'true' ) + const [activeSessionStatus, setActiveSessionStatus] = useState(null) + const [activePsaTicketId, setActivePsaTicketId] = useState(null) + const [showOverflow, setShowOverflow] = useState(false) const toggleSidebarCollapse = () => { const next = !sidebarCollapsed setSidebarCollapsed(next) @@ -127,6 +130,8 @@ export default function AssistantChatPage() { setShowTaskLane(false) setActiveQuestions([]) setActiveActions([]) + setActiveSessionStatus('active') + setActivePsaTicketId(null) try { const session = await aiSessionsApi.createChatSession({ @@ -223,12 +228,16 @@ export default function AssistantChatPage() { setShowTaskLane(false) setActiveQuestions([]) setActiveActions([]) + setActiveSessionStatus(null) + setActivePsaTicketId(null) try { const detail = await aiSessionsApi.getSession(chatId) // 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 + setActiveSessionStatus(detail.status) + setActivePsaTicketId(detail.psa_ticket_id) setMessages( (detail.conversation_messages || []).map(m => ({ role: m.role as 'user' | 'assistant', @@ -268,6 +277,8 @@ export default function AssistantChatPage() { setActiveQuestions([]) setActiveActions([]) setMessages([]) + setActiveSessionStatus('active') + setActivePsaTicketId(null) try { const session = await aiSessionsApi.createChatSession({ intake_type: 'free_text', @@ -427,14 +438,17 @@ export default function AssistantChatPage() { await aiSessionsApi.resolveSession(activeChatId, { resolution_summary: _notes || 'Resolved via assistant chat', }) + setActiveSessionStatus('resolved') return activeChatId } else if (outcome === 'escalated') { await aiSessionsApi.escalateSession(activeChatId, { escalation_reason: _notes || 'Escalated from assistant chat', }) + setActiveSessionStatus('escalated') return activeChatId } else { await aiSessionsApi.pauseSession(activeChatId) + setActiveSessionStatus('paused') return activeChatId } } @@ -446,6 +460,8 @@ export default function AssistantChatPage() { setShowTaskLane(false) setActiveQuestions([]) setActiveActions([]) + setActiveSessionStatus('active') + setActivePsaTicketId(null) try { const resumePrompt = `I'm continuing a previous troubleshooting session. Here's where we left off:\n\n${summary}\n\nPlease review this context and help me continue from where we stopped.` const session = await aiSessionsApi.createChatSession({ @@ -467,6 +483,8 @@ export default function AssistantChatPage() { setLoading(true) 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 setMessages(prev => [ ...prev, { role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions }, @@ -645,6 +663,153 @@ export default function AssistantChatPage() { {activeChatId ? ( <> + {/* Session header — title + lifecycle actions */} + {(() => { + const chatTitle = chats.find(c => c.id === activeChatId)?.title + const isActive = activeSessionStatus === 'active' || activeSessionStatus === null + const canAct = messages.length >= 2 && isActive && !loading + const updateLabel = activePsaTicketId ? 'Update Ticket' : 'Share Update' + return ( +
+ + + +
+

+ {chatTitle || 'AI Assistant'} +

+ {activeSessionStatus && activeSessionStatus !== 'active' && ( + + {activeSessionStatus === 'requesting_escalation' ? 'Escalated' : activeSessionStatus.charAt(0).toUpperCase() + activeSessionStatus.slice(1)} + + )} +
+ + {/* Desktop actions — shown when session is active and has messages */} +
+ {isActive && ( + <> + + + + )} + {messages.length >= 2 && ( + + )} + {/* Overflow: Pause / — */} + {isActive && messages.length >= 2 && ( +
+ + {showOverflow && ( + <> +
setShowOverflow(false)} /> +
+ +
+ + )} +
+ )} +
+ + {/* Mobile: single overflow menu */} + {messages.length >= 2 && ( +
+ + {showOverflow && ( + <> +
setShowOverflow(false)} /> +
+ {isActive && ( + <> + + + + )} + + {isActive && ( + + )} +
+ + )} +
+ )} +
+ ) + })()} + {/* Messages */}
{messages.length === 0 && !loading && ( @@ -784,18 +949,6 @@ export default function AssistantChatPage() { Paste Logs )} - {messages.length >= 2 && ( - <> - - - - )} {!showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (