refactor: resolve merge conflicts — combine main improvements with token normalization
- .gitignore: keep both graphify-out/ entries and main's .gitnexus entry - ScriptCodeBlock/ScriptPreviewModal: take main's border-border and text-accent-text for filename labels; use neutral ghost style for Save button in ScriptCodeBlock; use bg-accent (normalized from bg-primary) for Save button in ScriptPreviewModal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import { analytics } from '@/lib/analytics'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
|
||||
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 { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
||||
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
||||
@@ -81,6 +81,9 @@ export default function AssistantChatPage() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const dragCounterRef = useRef(0)
|
||||
const prefillHandledRef = useRef(false)
|
||||
// Tracks the most recently requested active chat ID so in-flight selectChat
|
||||
// calls that complete after the user switches chats don't clobber new state.
|
||||
const currentChatRef = useRef<string | null>(activeChatId)
|
||||
|
||||
// Persist active chat ID to sessionStorage
|
||||
useEffect(() => {
|
||||
@@ -214,6 +217,7 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
|
||||
const selectChat = useCallback(async (chatId: string) => {
|
||||
currentChatRef.current = chatId
|
||||
setActiveChatId(chatId)
|
||||
// Clear TaskLane when switching chats — will restore from backend if available
|
||||
setShowTaskLane(false)
|
||||
@@ -221,6 +225,10 @@ export default function AssistantChatPage() {
|
||||
setActiveActions([])
|
||||
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
|
||||
setMessages(
|
||||
(detail.conversation_messages || []).map(m => ({
|
||||
role: m.role as 'user' | 'assistant',
|
||||
@@ -234,7 +242,7 @@ export default function AssistantChatPage() {
|
||||
if (q.length > 0 || a.length > 0) {
|
||||
// Pre-load user's saved responses into sessionStorage BEFORE setting props
|
||||
// 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) {
|
||||
try {
|
||||
sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses))
|
||||
@@ -251,6 +259,15 @@ export default function AssistantChatPage() {
|
||||
}, [])
|
||||
|
||||
const handleNewChat = async () => {
|
||||
// Invalidate currentChatRef BEFORE the await so any in-flight handleSend/handleTaskSubmit
|
||||
// for the previous session sees a mismatch and bails — prevents stale task lane appearing
|
||||
// in the new empty session (same pattern as selectChat, which sets ref before its await).
|
||||
currentChatRef.current = null
|
||||
// Clear stale state immediately — don't wait for API to return
|
||||
setShowTaskLane(false)
|
||||
setActiveQuestions([])
|
||||
setActiveActions([])
|
||||
setMessages([])
|
||||
try {
|
||||
const session = await aiSessionsApi.createChatSession({
|
||||
intake_type: 'free_text',
|
||||
@@ -264,13 +281,9 @@ export default function AssistantChatPage() {
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
currentChatRef.current = session.session_id
|
||||
setChats(prev => [chatItem, ...prev])
|
||||
setActiveChatId(session.session_id)
|
||||
setMessages([])
|
||||
// Clear TaskLane from previous session
|
||||
setShowTaskLane(false)
|
||||
setActiveQuestions([])
|
||||
setActiveActions([])
|
||||
} catch {
|
||||
toast.error('Failed to create chat')
|
||||
}
|
||||
@@ -306,11 +319,14 @@ export default function AssistantChatPage() {
|
||||
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
|
||||
setLoading(true)
|
||||
|
||||
const sentForChatId = activeChatId
|
||||
try {
|
||||
const response = await aiSessionsApi.sendChatMessage(activeChatId, {
|
||||
message: userMessage,
|
||||
upload_ids: completedUploadIds.length > 0 ? completedUploadIds : undefined,
|
||||
})
|
||||
// Guard: discard if user switched to a different chat while this was in flight
|
||||
if (currentChatRef.current !== sentForChatId) return
|
||||
analytics.aiFeatureUsed({ feature: 'assistant_chat' })
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
@@ -318,19 +334,20 @@ export default function AssistantChatPage() {
|
||||
])
|
||||
setChats(prev =>
|
||||
prev.map(c =>
|
||||
c.id === activeChatId
|
||||
c.id === sentForChatId
|
||||
? { ...c, message_count: c.message_count + 2, title: c.message_count === 0 ? userMessage.slice(0, 100) : c.title, updated_at: new Date().toISOString() }
|
||||
: c
|
||||
)
|
||||
)
|
||||
// Load branches if fork was created
|
||||
if (response.fork && activeChatId) {
|
||||
branching.loadBranches(activeChatId)
|
||||
if (response.fork && sentForChatId) {
|
||||
branching.loadBranches(sentForChatId)
|
||||
}
|
||||
// Show task lane if AI sent questions or actions
|
||||
const hasQuestions = response.questions && response.questions.length > 0
|
||||
const hasActions = response.actions && response.actions.length > 0
|
||||
if (hasQuestions || hasActions) {
|
||||
clearTaskState(sentForChatId)
|
||||
setActiveQuestions(response.questions || [])
|
||||
setActiveActions(response.actions || [])
|
||||
setShowTaskLane(true)
|
||||
@@ -349,7 +366,8 @@ export default function AssistantChatPage() {
|
||||
const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
|
||||
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[] = []
|
||||
for (const r of responses) {
|
||||
const name = r.type === 'question' ? `Q: ${r.text}` : r.label || 'Check'
|
||||
@@ -357,6 +375,8 @@ export default function AssistantChatPage() {
|
||||
parts.push(`**${name}:**\n\`\`\`\n${r.value.trim()}\n\`\`\``)
|
||||
} else if (r.state === 'skipped') {
|
||||
parts.push(`**${name}:** _(skipped)_`)
|
||||
} else {
|
||||
parts.push(`**${name}:** _(not yet completed)_`)
|
||||
}
|
||||
}
|
||||
const userMessage = parts.join('\n\n')
|
||||
@@ -364,18 +384,22 @@ export default function AssistantChatPage() {
|
||||
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
|
||||
setLoading(true)
|
||||
|
||||
const sentForChatId = activeChatId
|
||||
try {
|
||||
const response = await aiSessionsApi.sendChatMessage(activeChatId, { message: userMessage })
|
||||
// Guard: discard if user switched to a different chat while this was in flight
|
||||
if (currentChatRef.current !== sentForChatId) return
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||
])
|
||||
if (response.fork && activeChatId) {
|
||||
branching.loadBranches(activeChatId)
|
||||
if (response.fork && sentForChatId) {
|
||||
branching.loadBranches(sentForChatId)
|
||||
}
|
||||
// Update task lane based on AI response
|
||||
const hasQuestions = response.questions && response.questions.length > 0
|
||||
const hasActions = response.actions && response.actions.length > 0
|
||||
clearTaskState(sentForChatId)
|
||||
if (hasQuestions || hasActions) {
|
||||
setActiveQuestions(response.questions || [])
|
||||
setActiveActions(response.actions || [])
|
||||
@@ -416,6 +440,12 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
|
||||
const handleResumeNew = async (summary: string) => {
|
||||
// Invalidate currentChatRef BEFORE the await — same guard as handleNewChat
|
||||
currentChatRef.current = null
|
||||
// Clear stale state immediately — don't wait for API to return
|
||||
setShowTaskLane(false)
|
||||
setActiveQuestions([])
|
||||
setActiveActions([])
|
||||
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({
|
||||
@@ -430,6 +460,7 @@ export default function AssistantChatPage() {
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
currentChatRef.current = session.session_id
|
||||
setChats(prev => [chatItem, ...prev])
|
||||
setActiveChatId(session.session_id)
|
||||
setMessages([{ role: 'user', content: resumePrompt }])
|
||||
|
||||
Reference in New Issue
Block a user