diff --git a/frontend/src/components/layout/CommandPalette.tsx b/frontend/src/components/layout/CommandPalette.tsx index ab753b72..d8c1d225 100644 --- a/frontend/src/components/layout/CommandPalette.tsx +++ b/frontend/src/components/layout/CommandPalette.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { useLocation, useNavigate } from 'react-router-dom' +import { PILOT_INLINE_SCRIPT_EVENT } from '@/lib/pilotEvents' import { Search, Loader2, ArrowRight, FileText, Clock, Sparkles, LayoutDashboard, Tag, Plus, BookOpen, Terminal, Zap, @@ -61,7 +62,11 @@ const QUICK_ACTIONS: PaletteItem[] = [ ] // Phase 5: only surfaced when on a /pilot/:id route. Fires the inline-script -// open event instead of navigating away to /scripts. +// open event instead of navigating away to /scripts. The path is a sentinel +// — handleSelect intercepts it and dispatches a window event rather than +// navigating, so the chat page can toggle its inline panel without coupling +// the global palette to chat-page state. +const PILOT_INLINE_SCRIPT_PATH = '__pilot_inline_script__' const SCRIPTS_INLINE_QUICK_ACTION: PaletteItem = { id: 'action-scripts-inline', group: 'quick-actions', @@ -86,12 +91,6 @@ function ItemIcon({ icon, className }: { icon: PaletteItem['icon'], className?: } } -// Phase 5: sentinel path the palette uses to fire the inline-script-generator -// open event instead of navigating. Listened for by AssistantChatPage when -// the user is in an active session. -export const PILOT_INLINE_SCRIPT_EVENT = 'flowpilot:open-inline-script' -const PILOT_INLINE_SCRIPT_PATH = '__pilot_inline_script__' - export function CommandPalette({ open, onClose }: CommandPaletteProps) { const navigate = useNavigate() const location = useLocation() diff --git a/frontend/src/lib/pilotEvents.ts b/frontend/src/lib/pilotEvents.ts new file mode 100644 index 00000000..92c92e50 --- /dev/null +++ b/frontend/src/lib/pilotEvents.ts @@ -0,0 +1,14 @@ +/** + * Cross-component event names for the FlowPilot session UI. + * + * Lives in /lib (not /components) so importing the event name does NOT + * pull in any React component module. AssistantChatPage and CommandPalette + * both reference it without forming an import cycle, and Vite's + * react-fast-refresh "consistent components exports" check stays happy. + */ + +// Phase 5: dispatched by the global Cmd+K palette when the engineer picks +// "Open inline Script Generator" while on a /pilot/:id route. The chat +// page subscribes via `window.addEventListener` and toggles its inline +// Script Generator panel if there's an active suggested fix. +export const PILOT_INLINE_SCRIPT_EVENT = 'flowpilot:open-inline-script' diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index 78bbc6b9..4edc528a 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -18,7 +18,7 @@ import { SuggestedFix } from '@/components/pilot/sections/SuggestedFix' import { ResolutionNotePreview as ResolutionNotePreviewPopover } from '@/components/pilot/ResolutionNotePreview' import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel' import { NoTemplateDialog } from '@/components/pilot/script/NoTemplateDialog' -import { PILOT_INLINE_SCRIPT_EVENT } from '@/components/layout/CommandPalette' +import { PILOT_INLINE_SCRIPT_EVENT } from '@/lib/pilotEvents' import { sessionFactsApi, type SessionFact } from '@/api/sessionFacts' import { sessionSuggestedFixesApi, @@ -163,9 +163,7 @@ export default function AssistantChatPage() { const sendPrefill = async () => { // Clear stale task lane from previous session - setShowTaskLane(false) - setActiveQuestions([]) - setActiveActions([]) + resetSessionDerivedState() setActiveSessionStatus('active') setActivePsaTicketId(null) @@ -274,6 +272,24 @@ export default function AssistantChatPage() { } } + // Single source of truth for "wipe every per-session task-lane state field" + // before switching to a different chat. Called from selectChat, handleNewChat, + // sendPrefill, and handleResumeNew so adding new lane-scoped state in future + // phases only requires touching this one helper. Forgetting to clear a field + // leaks the previous session's data into the new one (Phase 5 regression). + const resetSessionDerivedState = useCallback(() => { + setShowTaskLane(false) + setActiveQuestions([]) + setActiveActions([]) + setFacts([]) + setActiveFix(null) + setPreviewKind(null) + setPreviewData(null) + setPreviewError(null) + setPreviewPosting(false) + setScriptPanelOpen(false) + }, []) + // Phase 2 facts — fetch + handlers. `refreshFacts` is called from selectChat // and after each chat send, because the AI may have emitted [PROMOTE] markers // that synthesized new facts server-side (see unified_chat_service. @@ -503,17 +519,9 @@ export default function AssistantChatPage() { currentChatRef.current = chatId setActiveChatId(chatId) // Clear TaskLane when switching chats — will restore from backend if available - setShowTaskLane(false) - setActiveQuestions([]) - setActiveActions([]) + resetSessionDerivedState() setActiveSessionStatus(null) setActivePsaTicketId(null) - setFacts([]) - setActiveFix(null) - setPreviewData(null) - setPreviewError(null) - setPreviewKind(null) - setScriptPanelOpen(false) // Fire facts + active-fix fetches in parallel with session detail. refreshSessionDerived(chatId) try { @@ -558,12 +566,8 @@ export default function AssistantChatPage() { // 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([]) - setFacts([]) - setScriptPanelOpen(false) + // Clear stale state immediately — don't wait for API to return. + resetSessionDerivedState() setMessages([]) setActiveSessionStatus('active') setActivePsaTicketId(null) @@ -763,10 +767,8 @@ 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([]) + // Clear stale state immediately — don't wait for API to return. + resetSessionDerivedState() setActiveSessionStatus('active') setActivePsaTicketId(null) try {