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 type { InterceptChoice } from '@/components/pilot/EscalateInterceptDialog'
|
||||
import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel'
|
||||
import { NoTemplateDialog } from '@/components/pilot/script/NoTemplateDialog'
|
||||
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 { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery'
|
||||
@@ -138,6 +140,9 @@ export default function AssistantChatPage() {
|
||||
const [postApplyMsgCount, setPostApplyMsgCount] = useState(0)
|
||||
const [nudgeSilenced, setNudgeSilenced] = useState(false)
|
||||
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.
|
||||
// applied_at is now persisted on the server (stamped by POST /apply),
|
||||
// so bannerMode is derived entirely from server state — no client-side flag.
|
||||
@@ -154,6 +159,22 @@ export default function AssistantChatPage() {
|
||||
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 next = !sidebarCollapsed
|
||||
setSidebarCollapsed(next)
|
||||
@@ -343,6 +364,9 @@ export default function AssistantChatPage() {
|
||||
setPostApplyMsgCount(0)
|
||||
setNudgeSilenced(false)
|
||||
setEscalateIntercept(null)
|
||||
// Phase 9: tab strip reset
|
||||
setChatTab('chat')
|
||||
setScriptBuilderHasProgress(false)
|
||||
}, [])
|
||||
|
||||
// Phase 2 facts — fetch + handlers. `refreshFacts` is called from selectChat
|
||||
@@ -511,23 +535,22 @@ export default function AssistantChatPage() {
|
||||
refreshPreview(activeChatId, kind)
|
||||
}, [activeChatId, previewKind, refreshPreview])
|
||||
|
||||
// Phase 8: handleApplyFix — stamps applied_at on the server so Verifying state
|
||||
// survives refresh/reselect/multi-tab, then opens the script panel.
|
||||
const handleApplyFix = useCallback(async () => {
|
||||
if (!activeFix || !activeChatId) return
|
||||
setPostApplyMsgCount(0)
|
||||
setNudgeSilenced(false)
|
||||
try {
|
||||
const updated = await sessionSuggestedFixesApi.applyFix(activeChatId, activeFix.id)
|
||||
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)
|
||||
// Phase 9: handleApplyFix — routes to the appropriate surface based on
|
||||
// fix state. applyFix() call site moves to Task 13 (handleScriptDecision
|
||||
// and TemplateMatchPanel.onMarkRun).
|
||||
const handleApplyFix = useCallback(() => {
|
||||
if (!activeFix) return
|
||||
if (activeFix.script_template_id) {
|
||||
setScriptPanelOpen(true) // existing TemplateMatchPanel flow in task lane
|
||||
return
|
||||
}
|
||||
setScriptPanelOpen(true)
|
||||
}, [activeFix, activeChatId])
|
||||
if (activeFix.ai_drafted_script) {
|
||||
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
|
||||
// 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 */}
|
||||
<div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 space-y-4">
|
||||
{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 */}
|
||||
<div className="px-3 sm:px-6 py-3 shrink-0">
|
||||
<div
|
||||
@@ -1550,6 +1598,24 @@ export default function AssistantChatPage() {
|
||||
</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">
|
||||
@@ -1626,21 +1692,12 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
bottomSlot={
|
||||
<>
|
||||
{scriptPanelOpen && activeFix && activeChatId && (
|
||||
activeFix.script_template_id ? (
|
||||
<TemplateMatchPanel
|
||||
fix={activeFix}
|
||||
sessionId={activeChatId}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<NoTemplateDialog
|
||||
fix={activeFix}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
onDecide={handleScriptDecision}
|
||||
busy={scriptDecisionBusy}
|
||||
/>
|
||||
)
|
||||
{scriptPanelOpen && activeFix && activeChatId && activeFix.script_template_id && (
|
||||
<TemplateMatchPanel
|
||||
fix={activeFix}
|
||||
sessionId={activeChatId}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-3 px-3 mt-1">
|
||||
<button
|
||||
@@ -1705,21 +1762,12 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
bottomSlot={
|
||||
<>
|
||||
{scriptPanelOpen && activeFix && activeChatId && (
|
||||
activeFix.script_template_id ? (
|
||||
<TemplateMatchPanel
|
||||
fix={activeFix}
|
||||
sessionId={activeChatId}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<NoTemplateDialog
|
||||
fix={activeFix}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
onDecide={handleScriptDecision}
|
||||
busy={scriptDecisionBusy}
|
||||
/>
|
||||
)
|
||||
{scriptPanelOpen && activeFix && activeChatId && activeFix.script_template_id && (
|
||||
<TemplateMatchPanel
|
||||
fix={activeFix}
|
||||
sessionId={activeChatId}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-3 px-3 mt-1">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user