fix(pilot): wipe full task-lane state on chat switch + extract palette event
All checks were successful
Mirror to GitHub / mirror (push) Successful in 10s
All checks were successful
Mirror to GitHub / mirror (push) Successful in 10s
Two fixes from the Phase 5 shakedown:
1. Stale lane data leaking across chats. handleNewChat, sendPrefill, and
handleResumeNew were each missed when Phase 3/5 added activeFix,
previewKind, previewData, and scriptPanelOpen — only selectChat reset
the full set. Result: starting a new chat while a Suggested Fix card
was active showed the previous session's fix card (and any open
preview/script panel) until the next backend refresh swept it.
Consolidated all four entry points behind a single
resetSessionDerivedState() helper so adding new lane state in future
phases only requires touching one place.
2. CommandPalette TDZ on cold load. SCRIPTS_INLINE_QUICK_ACTION (line 66)
referenced PILOT_INLINE_SCRIPT_PATH declared at line 94 — module-level
evaluation hit the use before the declaration. Browser blanked with
"Cannot access 'PILOT_INLINE_SCRIPT_PATH' before initialization".
Moved the path const above its first use; also extracted
PILOT_INLINE_SCRIPT_EVENT into a tiny @/lib/pilotEvents module so
AssistantChatPage doesn't import the palette component just to read a
string — that mixed-export pattern broke Fast Refresh ("consistent
components exports") and added an unnecessary import edge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { PILOT_INLINE_SCRIPT_EVENT } from '@/lib/pilotEvents'
|
||||||
import {
|
import {
|
||||||
Search, Loader2, ArrowRight, FileText, Clock,
|
Search, Loader2, ArrowRight, FileText, Clock,
|
||||||
Sparkles, LayoutDashboard, Tag, Plus, BookOpen, Terminal, Zap,
|
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
|
// 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 = {
|
const SCRIPTS_INLINE_QUICK_ACTION: PaletteItem = {
|
||||||
id: 'action-scripts-inline',
|
id: 'action-scripts-inline',
|
||||||
group: 'quick-actions',
|
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) {
|
export function CommandPalette({ open, onClose }: CommandPaletteProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|||||||
14
frontend/src/lib/pilotEvents.ts
Normal file
14
frontend/src/lib/pilotEvents.ts
Normal file
@@ -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'
|
||||||
@@ -18,7 +18,7 @@ import { SuggestedFix } from '@/components/pilot/sections/SuggestedFix'
|
|||||||
import { ResolutionNotePreview as ResolutionNotePreviewPopover } from '@/components/pilot/ResolutionNotePreview'
|
import { ResolutionNotePreview as ResolutionNotePreviewPopover } from '@/components/pilot/ResolutionNotePreview'
|
||||||
import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel'
|
import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel'
|
||||||
import { NoTemplateDialog } from '@/components/pilot/script/NoTemplateDialog'
|
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 { sessionFactsApi, type SessionFact } from '@/api/sessionFacts'
|
||||||
import {
|
import {
|
||||||
sessionSuggestedFixesApi,
|
sessionSuggestedFixesApi,
|
||||||
@@ -163,9 +163,7 @@ export default function AssistantChatPage() {
|
|||||||
|
|
||||||
const sendPrefill = async () => {
|
const sendPrefill = async () => {
|
||||||
// Clear stale task lane from previous session
|
// Clear stale task lane from previous session
|
||||||
setShowTaskLane(false)
|
resetSessionDerivedState()
|
||||||
setActiveQuestions([])
|
|
||||||
setActiveActions([])
|
|
||||||
setActiveSessionStatus('active')
|
setActiveSessionStatus('active')
|
||||||
setActivePsaTicketId(null)
|
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
|
// Phase 2 facts — fetch + handlers. `refreshFacts` is called from selectChat
|
||||||
// and after each chat send, because the AI may have emitted [PROMOTE] markers
|
// and after each chat send, because the AI may have emitted [PROMOTE] markers
|
||||||
// that synthesized new facts server-side (see unified_chat_service.
|
// that synthesized new facts server-side (see unified_chat_service.
|
||||||
@@ -503,17 +519,9 @@ export default function AssistantChatPage() {
|
|||||||
currentChatRef.current = chatId
|
currentChatRef.current = chatId
|
||||||
setActiveChatId(chatId)
|
setActiveChatId(chatId)
|
||||||
// Clear TaskLane when switching chats — will restore from backend if available
|
// Clear TaskLane when switching chats — will restore from backend if available
|
||||||
setShowTaskLane(false)
|
resetSessionDerivedState()
|
||||||
setActiveQuestions([])
|
|
||||||
setActiveActions([])
|
|
||||||
setActiveSessionStatus(null)
|
setActiveSessionStatus(null)
|
||||||
setActivePsaTicketId(null)
|
setActivePsaTicketId(null)
|
||||||
setFacts([])
|
|
||||||
setActiveFix(null)
|
|
||||||
setPreviewData(null)
|
|
||||||
setPreviewError(null)
|
|
||||||
setPreviewKind(null)
|
|
||||||
setScriptPanelOpen(false)
|
|
||||||
// Fire facts + active-fix fetches in parallel with session detail.
|
// Fire facts + active-fix fetches in parallel with session detail.
|
||||||
refreshSessionDerived(chatId)
|
refreshSessionDerived(chatId)
|
||||||
try {
|
try {
|
||||||
@@ -558,12 +566,8 @@ export default function AssistantChatPage() {
|
|||||||
// for the previous session sees a mismatch and bails — prevents stale task lane appearing
|
// 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).
|
// in the new empty session (same pattern as selectChat, which sets ref before its await).
|
||||||
currentChatRef.current = null
|
currentChatRef.current = null
|
||||||
// Clear stale state immediately — don't wait for API to return
|
// Clear stale state immediately — don't wait for API to return.
|
||||||
setShowTaskLane(false)
|
resetSessionDerivedState()
|
||||||
setActiveQuestions([])
|
|
||||||
setActiveActions([])
|
|
||||||
setFacts([])
|
|
||||||
setScriptPanelOpen(false)
|
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setActiveSessionStatus('active')
|
setActiveSessionStatus('active')
|
||||||
setActivePsaTicketId(null)
|
setActivePsaTicketId(null)
|
||||||
@@ -763,10 +767,8 @@ export default function AssistantChatPage() {
|
|||||||
const handleResumeNew = async (summary: string) => {
|
const handleResumeNew = async (summary: string) => {
|
||||||
// Invalidate currentChatRef BEFORE the await — same guard as handleNewChat
|
// Invalidate currentChatRef BEFORE the await — same guard as handleNewChat
|
||||||
currentChatRef.current = null
|
currentChatRef.current = null
|
||||||
// Clear stale state immediately — don't wait for API to return
|
// Clear stale state immediately — don't wait for API to return.
|
||||||
setShowTaskLane(false)
|
resetSessionDerivedState()
|
||||||
setActiveQuestions([])
|
|
||||||
setActiveActions([])
|
|
||||||
setActiveSessionStatus('active')
|
setActiveSessionStatus('active')
|
||||||
setActivePsaTicketId(null)
|
setActivePsaTicketId(null)
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user