diff --git a/frontend/src/components/assistant/FlowPilotAsks.tsx b/frontend/src/components/assistant/FlowPilotAsks.tsx
new file mode 100644
index 00000000..7042f4dd
--- /dev/null
+++ b/frontend/src/components/assistant/FlowPilotAsks.tsx
@@ -0,0 +1,74 @@
+import { useState } from 'react'
+import { Send, HelpCircle } from 'lucide-react'
+import type { QuestionItem } from '@/types/ai-session'
+
+interface FlowPilotAsksProps {
+ questions: QuestionItem[]
+ onAnswer: (answer: string) => void
+ loading?: boolean
+}
+
+export function FlowPilotAsks({ questions, onAnswer, loading }: FlowPilotAsksProps) {
+ const [freeText, setFreeText] = useState('')
+
+ // Show first unanswered question
+ const question = questions.length > 0 ? questions[0] : null
+
+ if (!question) return null
+
+ const handleFreeTextSubmit = () => {
+ if (!freeText.trim()) return
+ onAnswer(freeText.trim())
+ setFreeText('')
+ }
+
+ return (
+
+
+
+ FlowPilot Asks
+
+
+ {question.text}
+
+ {question.context && (
+
+ {question.context}
+
+ )}
+ {question.options ? (
+
+ {question.options.map((option, idx) => (
+
+ ))}
+
+ ) : (
+
+ setFreeText(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleFreeTextSubmit()}
+ placeholder="Type your answer..."
+ disabled={loading}
+ className="flex-1 bg-input border border-default rounded px-2.5 py-1.5 text-sm text-primary outline-none focus:border-accent placeholder:text-muted-foreground/50"
+ />
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/assistant/IncidentHeader.tsx b/frontend/src/components/assistant/IncidentHeader.tsx
new file mode 100644
index 00000000..38d09220
--- /dev/null
+++ b/frontend/src/components/assistant/IncidentHeader.tsx
@@ -0,0 +1,158 @@
+import { useState, useRef, useEffect } from 'react'
+import { Pencil, X, Check, ExternalLink } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import type { TriageMeta } from '@/types/ai-session'
+
+interface IncidentHeaderProps {
+ triageMeta: TriageMeta
+ psaTicketId: string | null
+ sessionId: string
+ onFieldSave: (field: keyof TriageMeta, value: string) => void
+ onResolve: () => void
+ onOverflow: () => void
+}
+
+interface EditPopoverProps {
+ value: string
+ onSave: (value: string) => void
+ onCancel: () => void
+}
+
+function EditPopover({ value, onSave, onCancel }: EditPopoverProps) {
+ const [editValue, setEditValue] = useState(value)
+ const inputRef = useRef(null)
+
+ useEffect(() => {
+ inputRef.current?.focus()
+ inputRef.current?.select()
+ }, [])
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') onSave(editValue)
+ if (e.key === 'Escape') onCancel()
+ }
+
+ return (
+
+ setEditValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ className="flex-1 bg-input border border-default rounded px-2 py-1 text-sm text-primary outline-none focus:border-accent"
+ />
+
+
+
+ )
+}
+
+interface HeaderFieldProps {
+ label: string
+ value: string | null
+ placeholder: string
+ onSave: (value: string) => void
+ isHypothesis?: boolean
+}
+
+function HeaderField({ label, value, placeholder, onSave, isHypothesis }: HeaderFieldProps) {
+ const [editing, setEditing] = useState(false)
+
+ return (
+
+
+ {label}
+
+
+
+ {value || placeholder}
+
+
+
+ {editing && (
+
{ onSave(v); setEditing(false) }}
+ onCancel={() => setEditing(false)}
+ />
+ )}
+
+ )
+}
+
+export function IncidentHeader({
+ triageMeta,
+ psaTicketId,
+ onFieldSave,
+ onResolve,
+ onOverflow,
+}: IncidentHeaderProps) {
+ return (
+
+
onFieldSave('client_name', v)}
+ />
+
+ onFieldSave('asset_name', v)}
+ />
+
+ onFieldSave('issue_category', v)}
+ />
+
+ onFieldSave('triage_hypothesis', v)}
+ isHypothesis
+ />
+
+
+ {psaTicketId && (
+
+
+ CW #{psaTicketId}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/assistant/StepsPanel.tsx b/frontend/src/components/assistant/StepsPanel.tsx
new file mode 100644
index 00000000..e059b904
--- /dev/null
+++ b/frontend/src/components/assistant/StepsPanel.tsx
@@ -0,0 +1,70 @@
+import { Check, ArrowRight, Circle, Terminal } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import type { ActionItem } from '@/types/ai-session'
+
+interface StepsPanelProps {
+ actions: ActionItem[]
+ activeIndex: number
+ onGenerateScript?: () => void
+}
+
+export function StepsPanel({ actions, activeIndex, onGenerateScript }: StepsPanelProps) {
+ if (actions.length === 0) {
+ return (
+
+ No steps yet — start troubleshooting
+
+ )
+ }
+
+ const showScriptCta = actions[activeIndex]?.command?.toLowerCase().includes('script') ||
+ actions[activeIndex]?.description?.toLowerCase().includes('script')
+
+ return (
+
+
+ Steps
+
+
+ {actions.map((action, idx) => {
+ const isCompleted = idx < activeIndex
+ const isActive = idx === activeIndex
+ const isPending = idx > activeIndex
+
+ return (
+
+
+ {isCompleted && }
+ {isActive && }
+ {isPending && }
+
+
+
{action.label}
+ {isActive && action.description && (
+
{action.description}
+ )}
+
+
+ )
+ })}
+
+ {showScriptCta && onGenerateScript && (
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/assistant/WhatWeKnow.tsx b/frontend/src/components/assistant/WhatWeKnow.tsx
new file mode 100644
index 00000000..cb881d01
--- /dev/null
+++ b/frontend/src/components/assistant/WhatWeKnow.tsx
@@ -0,0 +1,139 @@
+import { useState } from 'react'
+import { Check, X, HelpCircle, Plus } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import type { EvidenceItem } from '@/types/ai-session'
+
+interface WhatWeKnowProps {
+ items: EvidenceItem[]
+ onAdd: (text: string, status: EvidenceItem['status']) => void
+ onEdit: (index: number, text: string, status: EvidenceItem['status']) => void
+}
+
+const STATUS_CONFIG = {
+ confirmed: { icon: Check, color: 'text-success', label: '✓' },
+ ruled_out: { icon: X, color: 'text-danger', label: '✗' },
+ pending: { icon: HelpCircle, color: 'text-muted-foreground', label: '?' },
+} as const
+
+const STATUS_CYCLE: EvidenceItem['status'][] = ['confirmed', 'ruled_out', 'pending']
+
+export function WhatWeKnow({ items, onAdd, onEdit }: WhatWeKnowProps) {
+ const [addingText, setAddingText] = useState('')
+ const [showAddInput, setShowAddInput] = useState(false)
+ const [editingIdx, setEditingIdx] = useState(null)
+ const [editText, setEditText] = useState('')
+
+ const handleAdd = () => {
+ if (!addingText.trim()) return
+ onAdd(addingText.trim(), 'pending')
+ setAddingText('')
+ setShowAddInput(false)
+ }
+
+ const handleStatusToggle = (idx: number) => {
+ const item = items[idx]
+ const currentIdx = STATUS_CYCLE.indexOf(item.status)
+ const nextStatus = STATUS_CYCLE[(currentIdx + 1) % STATUS_CYCLE.length]
+ onEdit(idx, item.text, nextStatus)
+ }
+
+ const handleEditStart = (idx: number) => {
+ setEditingIdx(idx)
+ setEditText(items[idx].text)
+ }
+
+ const handleEditSave = (idx: number) => {
+ if (editText.trim()) {
+ onEdit(idx, editText.trim(), items[idx].status)
+ }
+ setEditingIdx(null)
+ }
+
+ return (
+
+
+ What We Know
+
+ {items.length === 0 ? (
+
No evidence collected yet
+ ) : (
+
+ {items.map((item, idx) => {
+ const config = STATUS_CONFIG[item.status]
+ const Icon = config.icon
+
+ return (
+
+
+ {editingIdx === idx ? (
+ setEditText(e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') handleEditSave(idx); if (e.key === 'Escape') setEditingIdx(null) }}
+ onBlur={() => handleEditSave(idx)}
+ autoFocus
+ className="flex-1 bg-input border border-default rounded px-1.5 py-0.5 text-xs text-primary outline-none focus:border-accent"
+ />
+ ) : (
+ handleEditStart(idx)}
+ className={cn(
+ 'text-xs leading-relaxed cursor-pointer hover:text-foreground transition-colors',
+ item.status === 'confirmed' && 'text-success/80',
+ item.status === 'ruled_out' && 'text-danger/80',
+ item.status === 'pending' && 'text-muted-foreground',
+ )}
+ >
+ {item.text}
+
+ )}
+
+ )
+ })}
+
+ )}
+
+ {showAddInput ? (
+
+ setAddingText(e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') handleAdd(); if (e.key === 'Escape') setShowAddInput(false) }}
+ placeholder="New finding..."
+ autoFocus
+ className="flex-1 bg-input border border-default rounded px-2 py-1 text-xs text-primary outline-none focus:border-accent placeholder:text-muted-foreground/50"
+ />
+
+
+
+ ) : (
+
+ )}
+
+ )
+}