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:
Michael Chihlas
2026-04-06 20:23:36 -04:00
51 changed files with 4039 additions and 2656 deletions

View File

@@ -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 }])