feat(pilot): mount ChatTabStrip + ScriptBuilderTab + InlineNoTemplateDialog
Wires the three new components into AssistantChatPage: - ChatTabStrip renders when the active fix needs a script drafted. - ScriptBuilderTab sits alongside chat via display:none toggling so chat scroll position + builder state both persist. - InlineNoTemplateDialog replaces the task-lane bottomSlot render for the drafted-script evaluation case; three cards finally fit. - Banner Apply routing updated: no-draft/no-template → Script Builder tab; drafted → InlineNoTemplateDialog; template → unchanged path. applyFix() call site moves land in the next task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,10 @@ import type { BannerMode } from '@/components/pilot/ProposalBanner'
|
|||||||
import { EscalateInterceptDialog } from '@/components/pilot/EscalateInterceptDialog'
|
import { EscalateInterceptDialog } from '@/components/pilot/EscalateInterceptDialog'
|
||||||
import type { InterceptChoice } from '@/components/pilot/EscalateInterceptDialog'
|
import type { InterceptChoice } from '@/components/pilot/EscalateInterceptDialog'
|
||||||
import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel'
|
import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel'
|
||||||
import { NoTemplateDialog } from '@/components/pilot/script/NoTemplateDialog'
|
|
||||||
import { TemplatizePrompt } from '@/components/pilot/script/TemplatizePrompt'
|
import { TemplatizePrompt } from '@/components/pilot/script/TemplatizePrompt'
|
||||||
|
import { ChatTabStrip, type ChatTab } from '@/components/pilot/ChatTabStrip'
|
||||||
|
import { ScriptBuilderTab } from '@/components/pilot/ScriptBuilderTab'
|
||||||
|
import { InlineNoTemplateDialog } from '@/components/pilot/InlineNoTemplateDialog'
|
||||||
import { ShortcutsHelpOverlay } from '@/components/pilot/ShortcutsHelpOverlay'
|
import { ShortcutsHelpOverlay } from '@/components/pilot/ShortcutsHelpOverlay'
|
||||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery'
|
import { useMediaQuery } from '@/hooks/useMediaQuery'
|
||||||
@@ -138,6 +140,9 @@ export default function AssistantChatPage() {
|
|||||||
const [postApplyMsgCount, setPostApplyMsgCount] = useState(0)
|
const [postApplyMsgCount, setPostApplyMsgCount] = useState(0)
|
||||||
const [nudgeSilenced, setNudgeSilenced] = useState(false)
|
const [nudgeSilenced, setNudgeSilenced] = useState(false)
|
||||||
const [escalateIntercept, setEscalateIntercept] = useState<{ fixId: string; fixTitle: string } | null>(null)
|
const [escalateIntercept, setEscalateIntercept] = useState<{ fixId: string; fixTitle: string } | null>(null)
|
||||||
|
// Phase 9: ChatTabStrip + ScriptBuilderTab state.
|
||||||
|
const [chatTab, setChatTab] = useState<ChatTab>('chat')
|
||||||
|
const [scriptBuilderHasProgress, setScriptBuilderHasProgress] = useState(false)
|
||||||
// Phase 8: compute the current banner mode from activeFix.
|
// Phase 8: compute the current banner mode from activeFix.
|
||||||
// applied_at is now persisted on the server (stamped by POST /apply),
|
// applied_at is now persisted on the server (stamped by POST /apply),
|
||||||
// so bannerMode is derived entirely from server state — no client-side flag.
|
// so bannerMode is derived entirely from server state — no client-side flag.
|
||||||
@@ -154,6 +159,22 @@ export default function AssistantChatPage() {
|
|||||||
return 'proposed'
|
return 'proposed'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
// Phase 9: show the tab strip when the fix needs a script drafted (no template,
|
||||||
|
// no drafted script yet, and still in a live state).
|
||||||
|
const showTabStrip =
|
||||||
|
activeFix != null
|
||||||
|
&& activeFix.status !== 'dismissed'
|
||||||
|
&& activeFix.status !== 'applied_success'
|
||||||
|
&& activeFix.status !== 'applied_failed'
|
||||||
|
&& !activeFix.script_template_id
|
||||||
|
&& !activeFix.ai_drafted_script
|
||||||
|
|
||||||
|
// Defensive: if the strip hides (fix resolved/dismissed/script-drafted),
|
||||||
|
// snap back to the Chat tab so the user doesn't land on a blank panel.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showTabStrip && chatTab === 'script_builder') setChatTab('chat')
|
||||||
|
}, [showTabStrip, chatTab])
|
||||||
|
|
||||||
const toggleSidebarCollapse = () => {
|
const toggleSidebarCollapse = () => {
|
||||||
const next = !sidebarCollapsed
|
const next = !sidebarCollapsed
|
||||||
setSidebarCollapsed(next)
|
setSidebarCollapsed(next)
|
||||||
@@ -343,6 +364,9 @@ export default function AssistantChatPage() {
|
|||||||
setPostApplyMsgCount(0)
|
setPostApplyMsgCount(0)
|
||||||
setNudgeSilenced(false)
|
setNudgeSilenced(false)
|
||||||
setEscalateIntercept(null)
|
setEscalateIntercept(null)
|
||||||
|
// Phase 9: tab strip reset
|
||||||
|
setChatTab('chat')
|
||||||
|
setScriptBuilderHasProgress(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Phase 2 facts — fetch + handlers. `refreshFacts` is called from selectChat
|
// Phase 2 facts — fetch + handlers. `refreshFacts` is called from selectChat
|
||||||
@@ -511,23 +535,22 @@ export default function AssistantChatPage() {
|
|||||||
refreshPreview(activeChatId, kind)
|
refreshPreview(activeChatId, kind)
|
||||||
}, [activeChatId, previewKind, refreshPreview])
|
}, [activeChatId, previewKind, refreshPreview])
|
||||||
|
|
||||||
// Phase 8: handleApplyFix — stamps applied_at on the server so Verifying state
|
// Phase 9: handleApplyFix — routes to the appropriate surface based on
|
||||||
// survives refresh/reselect/multi-tab, then opens the script panel.
|
// fix state. applyFix() call site moves to Task 13 (handleScriptDecision
|
||||||
const handleApplyFix = useCallback(async () => {
|
// and TemplateMatchPanel.onMarkRun).
|
||||||
if (!activeFix || !activeChatId) return
|
const handleApplyFix = useCallback(() => {
|
||||||
setPostApplyMsgCount(0)
|
if (!activeFix) return
|
||||||
setNudgeSilenced(false)
|
if (activeFix.script_template_id) {
|
||||||
try {
|
setScriptPanelOpen(true) // existing TemplateMatchPanel flow in task lane
|
||||||
const updated = await sessionSuggestedFixesApi.applyFix(activeChatId, activeFix.id)
|
return
|
||||||
setActiveFix(updated)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
// Non-fatal: the script panel still opens. The banner will stay in
|
|
||||||
// 'proposed' mode until the next refreshActiveFix succeeds, which is
|
|
||||||
// a cosmetic gap only — no data loss.
|
|
||||||
console.error('[AssistantChat] applyFix failed:', err)
|
|
||||||
}
|
}
|
||||||
setScriptPanelOpen(true)
|
if (activeFix.ai_drafted_script) {
|
||||||
}, [activeFix, activeChatId])
|
setScriptPanelOpen(true) // InlineNoTemplateDialog, now in chat region (Step 5)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// No draft, no template — route to the Script Builder tab.
|
||||||
|
setChatTab('script_builder')
|
||||||
|
}, [activeFix])
|
||||||
|
|
||||||
// Phase 8: record a terminal outcome for the active fix. Updates local state
|
// Phase 8: record a terminal outcome for the active fix. Updates local state
|
||||||
// on success. For applied_success also opens the Resolve preview.
|
// on success. For applied_success also opens the Resolve preview.
|
||||||
@@ -1372,6 +1395,19 @@ export default function AssistantChatPage() {
|
|||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
{/* Phase 9: ChatTabStrip — shown when the fix needs a script drafted */}
|
||||||
|
{showTabStrip && (
|
||||||
|
<ChatTabStrip
|
||||||
|
active={chatTab}
|
||||||
|
onChange={setChatTab}
|
||||||
|
scriptBuilderHasProgress={scriptBuilderHasProgress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Chat tab content — messages + banner + composer.
|
||||||
|
Hidden (not unmounted) when Script Builder tab is active so
|
||||||
|
scroll position and input state are preserved. */}
|
||||||
|
<div className={cn('flex-1 min-h-0 flex flex-col', chatTab !== 'chat' && 'hidden')}>
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 space-y-4">
|
<div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 space-y-4">
|
||||||
{messages.length === 0 && !loading && (
|
{messages.length === 0 && !loading && (
|
||||||
@@ -1426,6 +1462,18 @@ export default function AssistantChatPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Phase 9: InlineNoTemplateDialog — drafted-script evaluation case,
|
||||||
|
rendered in the chat region above the composer so all three
|
||||||
|
option cards fit side-by-side without the TaskLane's narrow width. */}
|
||||||
|
{scriptPanelOpen && activeFix && activeChatId && !activeFix.script_template_id && activeFix.ai_drafted_script && (
|
||||||
|
<InlineNoTemplateDialog
|
||||||
|
fix={activeFix}
|
||||||
|
onClose={() => setScriptPanelOpen(false)}
|
||||||
|
onDecide={handleScriptDecision}
|
||||||
|
busy={scriptDecisionBusy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Rich Input */}
|
{/* Rich Input */}
|
||||||
<div className="px-3 sm:px-6 py-3 shrink-0">
|
<div className="px-3 sm:px-6 py-3 shrink-0">
|
||||||
<div
|
<div
|
||||||
@@ -1550,6 +1598,24 @@ export default function AssistantChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>{/* end chat-tab content wrapper */}
|
||||||
|
|
||||||
|
{/* Phase 9: Script Builder tab — mounted alongside chat via display:none
|
||||||
|
so both scroll positions and state are preserved across tab switches. */}
|
||||||
|
{showTabStrip && activeFix && activeChatId && (
|
||||||
|
<div className={cn('flex-1 min-h-0 flex flex-col', chatTab !== 'script_builder' && 'hidden')}>
|
||||||
|
<ScriptBuilderTab
|
||||||
|
fix={activeFix}
|
||||||
|
pilotSessionId={activeChatId}
|
||||||
|
onProgressChange={setScriptBuilderHasProgress}
|
||||||
|
onScriptDrafted={(updated) => {
|
||||||
|
setActiveFix(updated)
|
||||||
|
setChatTab('chat')
|
||||||
|
setScriptBuilderHasProgress(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||||
@@ -1626,21 +1692,12 @@ export default function AssistantChatPage() {
|
|||||||
}
|
}
|
||||||
bottomSlot={
|
bottomSlot={
|
||||||
<>
|
<>
|
||||||
{scriptPanelOpen && activeFix && activeChatId && (
|
{scriptPanelOpen && activeFix && activeChatId && activeFix.script_template_id && (
|
||||||
activeFix.script_template_id ? (
|
<TemplateMatchPanel
|
||||||
<TemplateMatchPanel
|
fix={activeFix}
|
||||||
fix={activeFix}
|
sessionId={activeChatId}
|
||||||
sessionId={activeChatId}
|
onClose={() => setScriptPanelOpen(false)}
|
||||||
onClose={() => setScriptPanelOpen(false)}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<NoTemplateDialog
|
|
||||||
fix={activeFix}
|
|
||||||
onClose={() => setScriptPanelOpen(false)}
|
|
||||||
onDecide={handleScriptDecision}
|
|
||||||
busy={scriptDecisionBusy}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3 px-3 mt-1">
|
<div className="flex items-center gap-3 px-3 mt-1">
|
||||||
<button
|
<button
|
||||||
@@ -1705,21 +1762,12 @@ export default function AssistantChatPage() {
|
|||||||
}
|
}
|
||||||
bottomSlot={
|
bottomSlot={
|
||||||
<>
|
<>
|
||||||
{scriptPanelOpen && activeFix && activeChatId && (
|
{scriptPanelOpen && activeFix && activeChatId && activeFix.script_template_id && (
|
||||||
activeFix.script_template_id ? (
|
<TemplateMatchPanel
|
||||||
<TemplateMatchPanel
|
fix={activeFix}
|
||||||
fix={activeFix}
|
sessionId={activeChatId}
|
||||||
sessionId={activeChatId}
|
onClose={() => setScriptPanelOpen(false)}
|
||||||
onClose={() => setScriptPanelOpen(false)}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<NoTemplateDialog
|
|
||||||
fix={activeFix}
|
|
||||||
onClose={() => setScriptPanelOpen(false)}
|
|
||||||
onDecide={handleScriptDecision}
|
|
||||||
busy={scriptDecisionBusy}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3 px-3 mt-1">
|
<div className="flex items-center gap-3 px-3 mt-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user