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:
2026-04-24 03:52:43 -04:00
parent 82db1c78e4
commit 0386fa1fd5

View File

@@ -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