feat(pilot): Phase 5 — inline Script Generator integration
All checks were successful
Mirror to GitHub / mirror (push) Successful in 10s
All checks were successful
Mirror to GitHub / mirror (push) Successful in 10s
Wires the SuggestedFix card to an inline panel that handles both cases:
template-matched fixes open the Script Library generator with parameters
pre-filled from session context; un-matched fixes open the three-option
dialog (one_off / draft_template / build_template). The decision endpoint
records the path choice with side effects: draft_template persists a
draft_templates row via a Sonnet-driven TemplateExtractionService;
build_template returns a redirect to the Script Builder; one_off just
records the choice.
Backend:
- TemplateExtractionService: drafts a parameter schema from a concrete
rendered script. Conservative by default ("prefer fewer parameters").
Round-trip-validates that templated_body only references declared
parameters; missing-key mismatch falls back to the original script
with no params. LLM/parse failures fall back identically — the
engineer can still create a draft and refine in the post-resolve
prompt (Phase 6).
- /suggested-fixes/{fix_id}/decision side effects:
* one_off → returns rendered_script (engineer's edited version or the
fix's ai_drafted_script verbatim)
* draft_template → same + creates draft_templates row with extracted
params, returns draft_template_id
* build_template → returns redirect_path=/scripts/builder?from_session=
&fix= so the frontend can navigate to the builder pre-loaded
- 400 when a non-template fix has no ai_drafted_script (template-matched
fixes take the dedicated /scripts/generate path, not this endpoint).
- 12 tests: TemplateExtractionService parse + fallback paths, all four
decision branches, edited_script override, missing-script 400.
Frontend:
- src/components/pilot/script/{TemplateMatchPanel, NoTemplateDialog,
ParameterizationPreview}.tsx — inline panels rendered in the task
lane's bottom slot when the engineer clicks a SuggestedFix card.
- TemplateMatchPanel: loads template via /scripts/templates/{id},
pre-fills params from fix.ai_drafted_parameters with cyan "from
session" tags, generates via existing /scripts/generate (already
bumps state_version on ai_session_id from Phase 3). 404 falls back
with a clear message instead of erroring.
- NoTemplateDialog: shows the AI-drafted script with proposed parameter
values highlighted in amber via ParameterizationPreview; three option
cards with the middle (draft_template) flagged Recommended; inline
edit on the script body before deciding.
- SuggestedFix card now clickable: onActivate toggles the inline panel.
- AssistantChatPage: scriptPanelOpen state + handleScriptDecision that
navigates on build_template and toasts on the other paths. Active fix
changes auto-close the panel so engineers don't act on stale state.
- Cmd+K → "Open inline Script Generator" palette entry surfaces only on
/pilot/:id routes; fires a window event the chat page subscribes to.
No Resolve shortcut added per Section 14 decision (browser ⌘R conflict).
Verified 2026-04-22 against the dev stack:
- one_off / draft_template / build_template all return the right shape
with real Sonnet TemplateExtractionService for the draft path.
- Conservative extraction confirmed: cmdkey + Restart-Process script
yielded zero proposed parameters as intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,15 @@ import { TaskLane, clearTaskState } from '@/components/assistant/TaskLane'
|
||||
import { WhatWeKnow } from '@/components/pilot/sections/WhatWeKnow'
|
||||
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 { sessionFactsApi, type SessionFact } from '@/api/sessionFacts'
|
||||
import {
|
||||
sessionSuggestedFixesApi,
|
||||
type SessionSuggestedFix,
|
||||
type ResolutionNotePreview as ResolutionNotePreviewData,
|
||||
type UserDecision,
|
||||
} from '@/api/sessionSuggestedFixes'
|
||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
||||
@@ -100,6 +104,11 @@ export default function AssistantChatPage() {
|
||||
// dups, but the request itself still costs HTTP RTT).
|
||||
const previewDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const previewOpen = previewKind !== null
|
||||
// Phase 5: inline Script Generator panel state. Open <=> the engineer
|
||||
// clicked the Suggested Fix card. Which panel renders is decided by
|
||||
// whether the active fix has a script_template_id.
|
||||
const [scriptPanelOpen, setScriptPanelOpen] = useState(false)
|
||||
const [scriptDecisionBusy, setScriptDecisionBusy] = useState(false)
|
||||
const [showOverflow, setShowOverflow] = useState(false)
|
||||
const toggleSidebarCollapse = () => {
|
||||
const next = !sidebarCollapsed
|
||||
@@ -234,6 +243,21 @@ export default function AssistantChatPage() {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
// Phase 5: Cmd+K → "Open inline Script Generator". Only acts when there
|
||||
// is an active suggested fix on this session — otherwise we'd open an
|
||||
// empty panel.
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
if (activeFix) {
|
||||
setScriptPanelOpen(true)
|
||||
} else {
|
||||
toast.info('No active suggested fix yet — wait for the AI to propose a resolution.')
|
||||
}
|
||||
}
|
||||
window.addEventListener(PILOT_INLINE_SCRIPT_EVENT, handler as EventListener)
|
||||
return () => window.removeEventListener(PILOT_INLINE_SCRIPT_EVENT, handler as EventListener)
|
||||
}, [activeFix])
|
||||
|
||||
const loadChats = async () => {
|
||||
try {
|
||||
const sessions = await aiSessionsApi.listSessions({ session_type: 'chat', limit: 100 })
|
||||
@@ -277,7 +301,13 @@ export default function AssistantChatPage() {
|
||||
try {
|
||||
const fix = await sessionSuggestedFixesApi.getActive(chatId)
|
||||
if (currentChatRef.current !== chatId) return
|
||||
setActiveFix(fix)
|
||||
setActiveFix((prev) => {
|
||||
// If the active fix changed (AI emitted a new SUGGEST_FIX that
|
||||
// superseded the prior), close the script panel so the engineer
|
||||
// isn't acting on stale draft state.
|
||||
if (prev?.id !== fix?.id) setScriptPanelOpen(false)
|
||||
return fix
|
||||
})
|
||||
} catch {
|
||||
// No-fix-yet (404) is normalized to null inside the client. Genuine
|
||||
// failures stay silent — accessory state, not load-bearing.
|
||||
@@ -368,6 +398,7 @@ export default function AssistantChatPage() {
|
||||
try {
|
||||
await sessionSuggestedFixesApi.recordDecision(activeChatId, activeFix.id, 'dismissed')
|
||||
setActiveFix(null)
|
||||
setScriptPanelOpen(false)
|
||||
// Dismissal bumps state_version on the server; reflect in preview.
|
||||
schedulePreviewRefresh(activeChatId)
|
||||
} catch {
|
||||
@@ -375,6 +406,41 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: handle a path choice from NoTemplateDialog. one_off and
|
||||
// draft_template just record the decision (returning the rendered script
|
||||
// for display); build_template returns a redirect_path to the Script
|
||||
// Builder, which we navigate to.
|
||||
const handleScriptDecision = async (
|
||||
decision: UserDecision,
|
||||
options: { editedScript: string; parametersUsed: Record<string, string> },
|
||||
) => {
|
||||
if (!activeChatId || !activeFix) return
|
||||
setScriptDecisionBusy(true)
|
||||
try {
|
||||
const out = await sessionSuggestedFixesApi.recordDecision(
|
||||
activeChatId, activeFix.id, decision,
|
||||
{ editedScript: options.editedScript, parametersUsed: options.parametersUsed },
|
||||
)
|
||||
// Decision endpoint bumps state_version — reflect in preview.
|
||||
schedulePreviewRefresh(activeChatId)
|
||||
|
||||
if (decision === 'build_template' && out.redirect_path) {
|
||||
navigate(out.redirect_path)
|
||||
return
|
||||
}
|
||||
if (decision === 'one_off') {
|
||||
toast.success('Recorded as one-off — script not added to library')
|
||||
} else if (decision === 'draft_template') {
|
||||
toast.success('Draft template queued — review after Resolve')
|
||||
}
|
||||
// Keep the panel open so the engineer can copy the rendered script.
|
||||
} catch {
|
||||
toast.error('Failed to record decision')
|
||||
} finally {
|
||||
setScriptDecisionBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenPreview = (kind: 'resolve' | 'escalate') => {
|
||||
if (!activeChatId) return
|
||||
// Opening a different kind clobbers the cached markdown so the popover
|
||||
@@ -447,6 +513,7 @@ export default function AssistantChatPage() {
|
||||
setPreviewData(null)
|
||||
setPreviewError(null)
|
||||
setPreviewKind(null)
|
||||
setScriptPanelOpen(false)
|
||||
// Fire facts + active-fix fetches in parallel with session detail.
|
||||
refreshSessionDerived(chatId)
|
||||
try {
|
||||
@@ -496,6 +563,7 @@ export default function AssistantChatPage() {
|
||||
setActiveQuestions([])
|
||||
setActiveActions([])
|
||||
setFacts([])
|
||||
setScriptPanelOpen(false)
|
||||
setMessages([])
|
||||
setActiveSessionStatus('active')
|
||||
setActivePsaTicketId(null)
|
||||
@@ -1260,11 +1328,32 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
suggestedFixSlot={
|
||||
activeFix && (
|
||||
<SuggestedFix fix={activeFix} onDismiss={handleDismissFix} />
|
||||
<SuggestedFix
|
||||
fix={activeFix}
|
||||
onDismiss={handleDismissFix}
|
||||
onActivate={() => setScriptPanelOpen((prev) => !prev)}
|
||||
panelOpen={scriptPanelOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
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}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex items-center gap-3 px-3 mt-1">
|
||||
<button
|
||||
onClick={() => handleOpenPreview('resolve')}
|
||||
|
||||
Reference in New Issue
Block a user