fix(pilot): Phase 9 review — partial-outcome notes + per-fix script-builder remount
All checks were successful
Mirror to GitHub / mirror (push) Successful in 3s

Addresses docs/FlowAssist_Migration/Issues/phase-9-review-issues.md.

Issue #1 (High): "Applied partially" from the escalation intercept silently
dropped because the backend requires notes on applied_partial and the dialog
sent none. The catch was silent and the UI advanced into the conclude flow
as if the outcome were recorded.
- EscalateInterceptDialog now has a two-step flow: clicking the partial
  choice reveals a notes textarea (autofocused, required non-empty) plus
  Back / "Record partial & escalate" buttons.
- onChoose signature extended to (choice, notes?).
- handleInterceptChoice passes notes to patchOutcome; on failure it
  surfaces a toast and does NOT advance to the conclude modal, so the
  intercept stays open for retry.

Issue #2 (Medium/High): ScriptBuilderTab kept local state across active-fix
changes within the same pilot session, so a stale draft could PATCH against
a newer fix.id. Added key={activeFix.id} on the mount — forces a clean
remount per fix; backend get-or-create (keyed on user+ai_session_id) still
returns the same session row, which is the intended resume-on-refresh
semantic; but messages/editorBuffer/latestScript local state resets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 11:08:00 -04:00
parent d386d11af2
commit 24972e8444
2 changed files with 113 additions and 46 deletions

View File

@@ -8,14 +8,16 @@
* Visual reference: docs/FlowAssist_Migration/mockups/07-verify-states.html
* (panel C).
*/
import { useState } from 'react'
import { X, AlertCircle, Check, Info } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { FixOutcome } from '@/api/sessionSuggestedFixes'
export type InterceptChoice = FixOutcome | 'never_applied'
export interface EscalateInterceptDialogProps {
fixTitle: string
onChoose: (choice: InterceptChoice) => void
onChoose: (choice: InterceptChoice, notes?: string) => void
onClose: () => void
}
@@ -24,6 +26,11 @@ export function EscalateInterceptDialog({
onChoose,
onClose,
}: EscalateInterceptDialogProps) {
const [partialStep, setPartialStep] = useState(false)
const [partialNotes, setPartialNotes] = useState('')
const notesValid = partialNotes.trim().length > 0
return (
<>
<div
@@ -36,45 +43,86 @@ export function EscalateInterceptDialog({
aria-label="Capture fix outcome before escalating"
className="absolute bottom-full mb-2 left-0 z-50 w-[340px] rounded-lg border border-white/15 bg-card p-3.5 shadow-[0_18px_40px_rgba(0,0,0,0.55)]"
>
<div className="font-heading font-semibold text-[13px] text-heading mb-1">
Before escalating what happened with the fix?
</div>
<div className="text-[12px] text-muted-foreground leading-[1.5] mb-3">
&ldquo;{fixTitle}&rdquo; is still in the Verifying state. Tag its outcome so
the senior picking this up knows what&apos;s been tried.
</div>
<div className="flex flex-col gap-1.5">
<button
autoFocus
onClick={() => onChoose('applied_failed')}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-danger/30 bg-danger-dim text-[12.5px] text-primary hover:bg-danger-dim/80 hover:border-danger transition-colors text-left"
>
<X size={13} strokeWidth={2.5} className="text-danger" />
<span className="flex-1">The fix didn&apos;t work</span>
<span className="text-[10.5px] text-muted-foreground font-mono px-1.5 py-[2px] rounded bg-white/[0.05]"></span>
</button>
<button
onClick={() => onChoose('applied_partial')}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
>
<Info size={13} strokeWidth={2} />
<span className="flex-1">I applied some of it partial</span>
</button>
<button
onClick={() => onChoose('applied_success')}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
>
<Check size={13} strokeWidth={2} />
<span className="flex-1">It worked escalating for another reason</span>
</button>
<button
onClick={() => onChoose('never_applied')}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
>
<AlertCircle size={13} strokeWidth={2} />
<span className="flex-1">Never actually applied it</span>
</button>
</div>
{!partialStep ? (
<>
<div className="font-heading font-semibold text-[13px] text-heading mb-1">
Before escalating what happened with the fix?
</div>
<div className="text-[12px] text-muted-foreground leading-[1.5] mb-3">
&ldquo;{fixTitle}&rdquo; is still in the Verifying state. Tag its outcome so
the senior picking this up knows what&apos;s been tried.
</div>
<div className="flex flex-col gap-1.5">
<button
autoFocus
onClick={() => onChoose('applied_failed')}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-danger/30 bg-danger-dim text-[12.5px] text-primary hover:bg-danger-dim/80 hover:border-danger transition-colors text-left"
>
<X size={13} strokeWidth={2.5} className="text-danger" />
<span className="flex-1">The fix didn&apos;t work</span>
<span className="text-[10.5px] text-muted-foreground font-mono px-1.5 py-[2px] rounded bg-white/[0.05]"></span>
</button>
<button
onClick={() => setPartialStep(true)}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
>
<Info size={13} strokeWidth={2} />
<span className="flex-1">I applied some of it partial</span>
</button>
<button
onClick={() => onChoose('applied_success')}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
>
<Check size={13} strokeWidth={2} />
<span className="flex-1">It worked escalating for another reason</span>
</button>
<button
onClick={() => onChoose('never_applied')}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
>
<AlertCircle size={13} strokeWidth={2} />
<span className="flex-1">Never actually applied it</span>
</button>
</div>
</>
) : (
<>
<div className="font-heading font-semibold text-[13px] text-heading mb-1">
What partially worked?
</div>
<div className="text-[12px] text-muted-foreground leading-[1.5] mb-3">
A short note for whoever picks this up what you tried, what worked, what&apos;s still broken.
</div>
<textarea
autoFocus
value={partialNotes}
onChange={(e) => setPartialNotes(e.target.value)}
placeholder="e.g. Cleared cached creds; rebuild profile still hung on sync."
className="w-full h-24 resize-none rounded-md border border-white/10 bg-elevated px-2.5 py-2 text-[12.5px] text-primary placeholder:text-muted-foreground focus:outline-none focus:border-accent"
/>
<div className="flex items-center justify-between gap-2 mt-3">
<button
onClick={() => {
setPartialStep(false)
setPartialNotes('')
}}
className="text-[12px] text-muted-foreground hover:text-primary transition-colors"
>
Back
</button>
<button
onClick={() => onChoose('applied_partial', partialNotes.trim())}
disabled={!notesValid}
className={cn(
'px-3 py-1.5 rounded text-[12.5px] font-semibold transition-colors',
'bg-accent text-[#0a0d14] hover:brightness-110 disabled:opacity-50 disabled:cursor-not-allowed',
)}
>
Record partial &amp; escalate
</button>
</div>
</>
)}
</div>
</>
)