# FlowPilot / FlowPilot Cockpit Side-by-Side — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Run the production AI chat page (FlowPilot) and the cockpit triage page (FlowPilot Cockpit) side-by-side on separate routes with shared sessions and a view toggle.

**Architecture:** Extract shared session logic from the 1000+ line `AssistantChatPage` into a `useAssistantSession` hook. Create two thin page components — `FlowPilotPage` (classic chat + TaskLane) and `CockpitPage` (cockpit layout) — that both consume the hook. Wire up routing, sidebar nav, view toggle, and dashboard launch preference.

**Tech Stack:** React 19, TypeScript, Zustand, React Router v7, Tailwind CSS v4, Lucide icons

**Design spec:** `docs/superpowers/specs/2026-04-02-flowpilot-cockpit-side-by-side-design.md`

---

## File Structure

| File | Responsibility |
|------|---------------|
| `frontend/src/hooks/useAssistantSession.ts` | **New.** All shared session logic: CRUD, chat messaging, file uploads, branching, conclude, prefill, race-condition guards |
| `frontend/src/pages/FlowPilotPage.tsx` | **New.** Classic chat layout (ChatMessage bubbles, TaskLane side panel). Consumes `useAssistantSession` |
| `frontend/src/pages/CockpitPage.tsx` | **Rename** from `AssistantChatPage.tsx`. Cockpit layout (IncidentHeader, StepsPanel, FlowPilotAsks, WhatWeKnow, conversation log). Consumes `useAssistantSession` |
| `frontend/src/components/assistant/ViewToggle.tsx` | **New.** Segmented control to switch between `/assistant/:id` and `/cockpit/:id` |
| `frontend/src/router.tsx` | **Modify.** Add `/cockpit` routes, update `/assistant` routes to `FlowPilotPage` |
| `frontend/src/components/layout/Sidebar.tsx` | **Modify.** Add FlowPilot rail entry with cockpit flyout child |
| `frontend/src/store/userPreferencesStore.ts` | **Modify.** Add `preferredFlowPilotView` preference |
| `frontend/src/components/dashboard/StartSessionInput.tsx` | **Modify.** Add launch view toggle when cockpit flag is enabled |
| `frontend/src/hooks/useFeatureFlag.ts` | **Exists.** Already created in sub-project 1. Used for cockpit gating |

---

### Task 1: Extract `useAssistantSession` hook

This is the foundation task. Extract all shared session management logic from the current `AssistantChatPage.tsx` into a reusable hook so both pages can consume it.

**Files:**
- Create: `frontend/src/hooks/useAssistantSession.ts`
- Reference: `frontend/src/pages/AssistantChatPage.tsx` (current cockpit version on this branch)

The hook must expose everything both pages need. Study the current `AssistantChatPage.tsx` to extract these pieces:

**State the hook manages:**
- `chats`, `activeChatId`, `messages` — session list and active conversation
- `input`, `loading` — compose area state
- `showConclude`, `showStatusUpdate` — modal visibility
- `branching` — from `useBranching()` hook
- `mobileSidebarOpen` — mobile sidebar toggle
- `showLogs`, `logContent` — log paste area
- `pendingUploads`, `isDragOver` — file upload state
- `activeQuestions`, `activeActions`, `showTaskLane` — task lane / AI question state
- `sidebarCollapsed` — chat sidebar collapse state

**Functions the hook exposes:**
- `loadChats`, `selectChat`, `handleNewChat`, `handleDeleteChat`, `handleTogglePin`
- `handleSend`, `handleConclude`, `handleResumeNew`
- `handleKeyDown`, `handlePaste`, `handleDragOver`, `handleDragEnter`, `handleDragLeave`, `handleDrop`, `handleFileSelect`, `handleRemoveUpload`, `retryUpload`
- `setInput`, `setShowConclude`, `setShowStatusUpdate`, `setMobileSidebarOpen`, `setShowLogs`, `setLogContent`, `setShowTaskLane`

**Refs the hook manages:**
- `messagesEndRef`, `inputRef`, `fileInputRef`, `dragCounterRef`, `prefillHandledRef`, `currentChatRef`

**What the hook does NOT include (page-specific):**
- Cockpit triage state (`triageMeta`, `workZonePct`, `activeStepIndex`, `completedSteps`, `showOnboarding`, `mergeTriageUpdate`, `handleTriageFieldSave`, etc.)
- TaskLane `handleTaskSubmit` (only production page uses TaskLane component; cockpit uses StepsPanel)
- All JSX rendering

- [ ] **Step 1: Create the hook file with the full extracted logic**

Create `frontend/src/hooks/useAssistantSession.ts`:

```typescript
import { useState, useEffect, useRef, useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { uploadsApi } from '@/api/uploads'
import type { PendingUpload } from '@/types/upload'
import type { ForkMetadata, ActionItem, QuestionItem, ChatMessageResponse, TriageUpdate } from '@/types/ai-session'
import { aiSessionsApi } from '@/api/aiSessions'
import { useBranching } from '@/hooks/useBranching'
import { analytics } from '@/lib/analytics'
import { toast } from '@/lib/toast'
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
import type { SuggestedFlow } from '@/types/copilot'

export interface MessageWithMeta {
  role: 'user' | 'assistant'
  content: string
  suggestedFlows?: SuggestedFlow[]
  fork?: ForkMetadata | null
  actions?: ActionItem[] | null
  questions?: QuestionItem[] | null
}

const ACCEPTED_FILE_TYPES = 'image/png,image/jpeg,image/gif,image/webp,.txt,.log,.csv,.pdf,.docx'

export function useAssistantSession() {
  const location = useLocation()
  const navigate = useNavigate()
  const { sessionId: urlSessionId } = useParams<{ sessionId?: string }>()

  const [chats, setChats] = useState<ChatListItem[]>([])
  const [activeChatId, setActiveChatId] = useState<string | null>(() => {
    if (urlSessionId) return urlSessionId
    try { return sessionStorage.getItem('rf-active-chat-id') } catch { return null }
  })
  const [messages, setMessages] = useState<MessageWithMeta[]>([])
  const [input, setInput] = useState('')
  const [loading, setLoading] = useState(false)
  const [showConclude, setShowConclude] = useState(false)
  const [showStatusUpdate, setShowStatusUpdate] = useState(false)
  const branching = useBranching()
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
  const [showLogs, setShowLogs] = useState(false)
  const [logContent, setLogContent] = useState('')
  const [pendingUploads, setPendingUploads] = useState<PendingUpload[]>([])
  const [isDragOver, setIsDragOver] = useState(false)
  const [activeQuestions, setActiveQuestions] = useState<QuestionItem[]>(() => {
    try {
      const saved = sessionStorage.getItem('rf-tasklane-meta')
      if (saved) { const d = JSON.parse(saved); if (d.chatId === urlSessionId || d.chatId === sessionStorage.getItem('rf-active-chat-id')) return d.questions || [] }
    } catch { /* ignore */ }
    return []
  })
  const [activeActions, setActiveActions] = useState<ActionItem[]>(() => {
    try {
      const saved = sessionStorage.getItem('rf-tasklane-meta')
      if (saved) { const d = JSON.parse(saved); if (d.chatId === urlSessionId || d.chatId === sessionStorage.getItem('rf-active-chat-id')) return d.actions || [] }
    } catch { /* ignore */ }
    return []
  })
  const [showTaskLane, setShowTaskLane] = useState(() => {
    try {
      const saved = sessionStorage.getItem('rf-tasklane-meta')
      if (saved) { const d = JSON.parse(saved); return d.show === true && (d.chatId === urlSessionId || d.chatId === sessionStorage.getItem('rf-active-chat-id')) }
    } catch { /* ignore */ }
    return false
  })
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
    localStorage.getItem('rf-chat-sidebar-collapsed') === 'true'
  )

  const messagesEndRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLTextAreaElement>(null)
  const fileInputRef = useRef<HTMLInputElement>(null)
  const dragCounterRef = useRef(0)
  const prefillHandledRef = useRef(false)
  const currentChatRef = useRef<string | null>(activeChatId)

  const toggleSidebarCollapse = () => {
    const next = !sidebarCollapsed
    setSidebarCollapsed(next)
    localStorage.setItem('rf-chat-sidebar-collapsed', String(next))
  }

  // Persist active chat ID to sessionStorage
  useEffect(() => {
    try {
      if (activeChatId) sessionStorage.setItem('rf-active-chat-id', activeChatId)
      else sessionStorage.removeItem('rf-active-chat-id')
    } catch { /* ignore */ }
  }, [activeChatId])

  // Load chat list on mount
  useEffect(() => { loadChats() }, [])

  // If URL has a session ID, load it
  useEffect(() => {
    if (urlSessionId && urlSessionId !== activeChatId) {
      selectChat(urlSessionId)
    }
  }, [urlSessionId]) // eslint-disable-line react-hooks/exhaustive-deps

  // Restore session from sessionStorage on mount (when URL has no session ID)
  useEffect(() => {
    if (!urlSessionId && activeChatId) {
      selectChat(activeChatId)
    }
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  // Persist task lane metadata to sessionStorage
  useEffect(() => {
    try {
      sessionStorage.setItem('rf-tasklane-meta', JSON.stringify({
        show: showTaskLane,
        chatId: activeChatId,
        questions: activeQuestions,
        actions: activeActions,
      }))
    } catch { /* ignore */ }
  }, [showTaskLane, activeChatId, activeQuestions, activeActions])

  // Auto-scroll
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  // Auto-grow textarea
  useEffect(() => {
    const el = inputRef.current
    if (!el) return
    el.style.height = 'auto'
    el.style.height = `${Math.min(el.scrollHeight, 150)}px`
  }, [input])

  // Cleanup blob URLs on unmount
  useEffect(() => {
    return () => { pendingUploads.forEach((u) => { if (u.preview) URL.revokeObjectURL(u.preview) }) }
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  const loadChats = async () => {
    try {
      const sessions = await aiSessionsApi.listSessions({ session_type: 'chat', limit: 100 })
      setChats(sessions.map(s => ({
        id: s.id,
        title: s.title || s.problem_summary || 'New Chat',
        message_count: s.step_count,
        pinned: false,
        created_at: s.created_at,
        updated_at: s.created_at,
      })))
    } catch {
      // silently handle
    }
  }

  // Callback for pages to handle triage data from selectChat.
  // Pages that care about triage (CockpitPage) set this; FlowPilotPage ignores it.
  const onSessionLoadedRef = useRef<((detail: {
    client_name: string | null
    asset_name: string | null
    issue_category: string | null
    triage_hypothesis: string | null
    evidence_items: Array<{ text: string; status: string }> | null
  }) => void) | null>(null)

  const selectChat = useCallback(async (chatId: string) => {
    currentChatRef.current = chatId
    setActiveChatId(chatId)
    setShowTaskLane(false)
    setActiveQuestions([])
    setActiveActions([])
    try {
      const detail = await aiSessionsApi.getSession(chatId)
      if (currentChatRef.current !== chatId) return
      setMessages(
        (detail.conversation_messages || []).map(m => ({
          role: m.role as 'user' | 'assistant',
          content: m.content,
        }))
      )
      // Notify page of triage data
      onSessionLoadedRef.current?.({
        client_name: detail.client_name ?? null,
        asset_name: detail.asset_name ?? null,
        issue_category: detail.issue_category ?? null,
        triage_hypothesis: detail.triage_hypothesis ?? null,
        evidence_items: detail.evidence_items ?? null,
      })
      // Restore task lane from persisted state
      if (detail.pending_task_lane) {
        const q = detail.pending_task_lane.questions || []
        const a = detail.pending_task_lane.actions || []
        if (q.length > 0 || a.length > 0) {
          const responses = (detail.pending_task_lane as Record<string, unknown>).responses as unknown[] | undefined
          if (responses && responses.length > 0) {
            try {
              sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses))
            } catch { /* ignore */ }
          }
          setActiveQuestions(q)
          setActiveActions(a)
          setShowTaskLane(true)
        }
      }
    } catch {
      setMessages([])
    }
  }, [])

  const handleNewChat = async () => {
    try {
      const session = await aiSessionsApi.createChatSession({
        intake_type: 'free_text',
        intake_content: { text: '' },
      })
      const chatItem: ChatListItem = {
        id: session.session_id,
        title: session.title,
        message_count: 0,
        pinned: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      }
      currentChatRef.current = session.session_id
      setChats(prev => [chatItem, ...prev])
      setActiveChatId(session.session_id)
      setMessages([])
      setShowTaskLane(false)
      setActiveQuestions([])
      setActiveActions([])
    } catch {
      toast.error('Failed to create chat')
    }
  }

  const handleDeleteChat = async (chatId: string) => {
    try {
      await aiSessionsApi.deleteSession(chatId)
      setChats(prev => prev.filter(c => c.id !== chatId))
      if (activeChatId === chatId) {
        setActiveChatId(null)
        setMessages([])
      }
    } catch {
      toast.error('Failed to delete chat')
    }
  }

  const handleTogglePin = async () => {
    toast.info('Pin feature coming soon')
  }

  // Process an AI chat response — updates messages, task lane, branching.
  // Returns the response for page-specific handling (e.g. triage_update).
  const processResponse = useCallback((response: ChatMessageResponse, chatId: string) => {
    if (currentChatRef.current !== chatId) return null
    setMessages(prev => [
      ...prev,
      {
        role: 'assistant',
        content: response.content,
        suggestedFlows: response.suggested_flows,
        fork: response.fork,
        actions: response.actions,
        questions: response.questions,
      },
    ])
    if (response.fork && chatId) {
      branching.loadBranches(chatId)
    }
    const hasQuestions = response.questions && response.questions.length > 0
    const hasActions = response.actions && response.actions.length > 0
    if (hasQuestions || hasActions) {
      setActiveQuestions(response.questions || [])
      setActiveActions(response.actions || [])
      setShowTaskLane(true)
    }
    return response
  }, [branching])

  // Callback for pages to handle triage updates from chat responses.
  const onTriageUpdateRef = useRef<((update: TriageUpdate) => void) | null>(null)

  const handleSend = async () => {
    if (!input.trim() || !activeChatId || loading) return

    const sendChatId = activeChatId
    const userMessage = input.trim()
    const completedUploadIds = pendingUploads
      .filter((u) => u.status === 'done' && u.result?.id)
      .map((u) => u.result!.id)
    setInput('')
    setPendingUploads([])
    setMessages(prev => [...prev, { role: 'user', content: userMessage }])
    setLoading(true)

    try {
      const response = await aiSessionsApi.sendChatMessage(sendChatId, {
        message: userMessage,
        upload_ids: completedUploadIds.length > 0 ? completedUploadIds : undefined,
      })
      if (currentChatRef.current !== sendChatId) return
      analytics.aiFeatureUsed({ feature: 'assistant_chat' })
      processResponse(response, sendChatId)
      setChats(prev =>
        prev.map(c =>
          c.id === sendChatId
            ? { ...c, message_count: c.message_count + 2, title: c.message_count === 0 ? userMessage.slice(0, 100) : c.title, updated_at: new Date().toISOString() }
            : c
        )
      )
      if (response.triage_update) onTriageUpdateRef.current?.(response.triage_update)
    } catch {
      if (currentChatRef.current !== sendChatId) return
      setMessages(prev => [
        ...prev,
        { role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
      ])
    } finally {
      if (currentChatRef.current === sendChatId) {
        setLoading(false)
        requestAnimationFrame(() => inputRef.current?.focus())
      }
    }
  }

  // Handle prefill from command palette / dashboard handoff
  const handlePrefill = useCallback((prefillRoute: string) => {
    const state = location.state as { prefill?: string; uploadIds?: string[] } | null
    const prefill = state?.prefill
    const uploadIds = state?.uploadIds
    if (!prefill || prefillHandledRef.current) return
    prefillHandledRef.current = true

    navigate(location.pathname, { replace: true, state: {} })

    const sendPrefill = async () => {
      setShowTaskLane(false)
      setActiveQuestions([])
      setActiveActions([])

      try {
        const session = await aiSessionsApi.createChatSession({
          intake_type: 'free_text',
          intake_content: { text: prefill },
        })
        const prefillChatId = session.session_id
        currentChatRef.current = prefillChatId
        const chatItem: ChatListItem = {
          id: prefillChatId,
          title: session.title,
          message_count: 0,
          pinned: false,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        }
        setChats(prev => [chatItem, ...prev])
        setActiveChatId(prefillChatId)
        setMessages([{ role: 'user', content: prefill }])
        setLoading(true)

        const response = await aiSessionsApi.sendChatMessage(prefillChatId, {
          message: prefill,
          upload_ids: uploadIds?.length ? uploadIds : undefined,
        })
        if (currentChatRef.current !== prefillChatId) return
        processResponse(response, prefillChatId)
        setChats(prev =>
          prev.map(c =>
            c.id === prefillChatId
              ? { ...c, message_count: 2, title: prefill.slice(0, 100), updated_at: new Date().toISOString() }
              : c
          )
        )
        if (response.triage_update) onTriageUpdateRef.current?.(response.triage_update)
      } catch {
        toast.error('Failed to start AI conversation')
      } finally {
        setLoading(false)
      }
    }

    sendPrefill()
  }, [location.state, navigate, processResponse])

  const handleConclude = async (outcome: ConclusionOutcome, _notes: string): Promise<string> => {
    if (!activeChatId) throw new Error('No active chat')

    if (outcome === 'resolved') {
      await aiSessionsApi.resolveSession(activeChatId, {
        resolution_summary: _notes || 'Resolved via assistant chat',
      })
      return activeChatId
    } else if (outcome === 'escalated') {
      await aiSessionsApi.escalateSession(activeChatId, {
        escalation_reason: _notes || 'Escalated from assistant chat',
      })
      return activeChatId
    } else {
      await aiSessionsApi.pauseSession(activeChatId)
      return activeChatId
    }
  }

  const handleResumeNew = async (summary: string) => {
    try {
      const resumePrompt = `I'm continuing a previous troubleshooting session. Here's where we left off:\n\n${summary}\n\nPlease review this context and help me continue from where we stopped.`
      const session = await aiSessionsApi.createChatSession({
        intake_type: 'free_text',
        intake_content: { text: resumePrompt },
      })
      const chatItem: ChatListItem = {
        id: session.session_id,
        title: session.title,
        message_count: 0,
        pinned: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      }
      currentChatRef.current = session.session_id
      setChats(prev => [chatItem, ...prev])
      setActiveChatId(session.session_id)
      setMessages([{ role: 'user', content: resumePrompt }])
      setLoading(true)

      const resumeChatId = session.session_id
      const response = await aiSessionsApi.sendChatMessage(resumeChatId, { message: resumePrompt })
      if (currentChatRef.current !== resumeChatId) return
      processResponse(response, resumeChatId)
      setChats(prev =>
        prev.map(c =>
          c.id === resumeChatId
            ? { ...c, message_count: 2, title: resumePrompt.slice(0, 100), updated_at: new Date().toISOString() }
            : c
        )
      )
      if (response.triage_update) onTriageUpdateRef.current?.(response.triage_update)
    } catch {
      toast.error('Failed to create resume chat')
    } finally {
      setLoading(false)
    }
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      handleSend()
    }
  }

  // ── File handling ──────────────────────────────

  const processFiles = useCallback((files: File[]) => {
    if (files.length === 0) return
    const newUploads: PendingUpload[] = files.map((file) => ({
      id: crypto.randomUUID(),
      file,
      preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : '',
      status: 'uploading' as const,
    }))
    setPendingUploads((prev) => [...prev, ...newUploads])
    newUploads.forEach((upload) => {
      uploadsApi.upload(upload.file)
        .then((result) => {
          setPendingUploads((prev) => prev.map((u) => u.id === upload.id ? { ...u, status: 'done' as const, result } : u))
        })
        .catch((err) => {
          const is503 = err?.response?.status === 503
          if (is503) {
            toast.warning('Image attachments are not available yet — describe the issue in text instead')
          } else {
            toast.error(`Upload failed: ${err?.message || 'Unknown error'}`)
          }
          setPendingUploads((prev) => prev.filter((u) => u.id !== upload.id))
        })
    })
  }, [])

  const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
    const items = e.clipboardData?.items
    if (!items) return
    const imageFiles: File[] = []
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.startsWith('image/')) {
        const file = items[i].getAsFile()
        if (file) imageFiles.push(file)
      }
    }
    if (imageFiles.length > 0) {
      e.preventDefault()
      processFiles(imageFiles)
    }
  }, [processFiles])

  const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy' }, [])
  const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); dragCounterRef.current++; if (dragCounterRef.current === 1) setIsDragOver(true) }, [])
  const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); dragCounterRef.current--; if (dragCounterRef.current === 0) setIsDragOver(false) }, [])
  const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); dragCounterRef.current = 0; setIsDragOver(false); processFiles(Array.from(e.dataTransfer.files)) }, [processFiles])
  const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files) { processFiles(Array.from(e.target.files)); e.target.value = '' } }, [processFiles])
  const handleRemoveUpload = useCallback((uploadId: string) => {
    setPendingUploads((prev) => { const toRemove = prev.find((u) => u.id === uploadId); if (toRemove?.preview) URL.revokeObjectURL(toRemove.preview); return prev.filter((u) => u.id !== uploadId) })
  }, [])
  const retryUpload = useCallback((uploadId: string) => {
    const upload = pendingUploads.find((u) => u.id === uploadId)
    if (!upload) return
    setPendingUploads((prev) => prev.map((u) => u.id === uploadId ? { ...u, status: 'uploading' as const, error: undefined } : u))
    uploadsApi.upload(upload.file)
      .then((result) => { setPendingUploads((prev) => prev.map((u) => u.id === uploadId ? { ...u, status: 'done' as const, result } : u)) })
      .catch((err) => { setPendingUploads((prev) => prev.map((u) => u.id === uploadId ? { ...u, status: 'error' as const, error: err?.message || 'Upload failed' } : u)) })
  }, [pendingUploads])

  return {
    // State
    chats, activeChatId, messages, input, loading,
    showConclude, showStatusUpdate, branching,
    mobileSidebarOpen, showLogs, logContent,
    pendingUploads, isDragOver,
    activeQuestions, activeActions, showTaskLane,
    sidebarCollapsed,
    // Setters
    setInput, setShowConclude, setShowStatusUpdate,
    setMobileSidebarOpen, setShowLogs, setLogContent,
    setShowTaskLane, setActiveQuestions, setActiveActions,
    // Handlers
    selectChat, handleNewChat, handleDeleteChat, handleTogglePin,
    handleSend, handleConclude, handleResumeNew,
    handleKeyDown, handlePaste,
    handleDragOver, handleDragEnter, handleDragLeave, handleDrop,
    handleFileSelect, handleRemoveUpload, retryUpload,
    toggleSidebarCollapse, handlePrefill, processResponse,
    // Refs
    messagesEndRef, inputRef, fileInputRef, currentChatRef,
    // Page-specific callbacks
    onSessionLoadedRef, onTriageUpdateRef,
    // Constants
    ACCEPTED_FILE_TYPES,
  }
}
```

- [ ] **Step 2: Verify the hook compiles**

Run: `cd frontend && npx tsc --noEmit`

Expected: No errors from `useAssistantSession.ts`. There may be pre-existing errors elsewhere — focus only on the new file.

- [ ] **Step 3: Commit**

```bash
git add frontend/src/hooks/useAssistantSession.ts
git commit -m "feat: extract useAssistantSession hook from AssistantChatPage"
```

---

### Task 2: Create `FlowPilotPage.tsx` (classic chat layout)

This page recreates the production `AssistantChatPage` from `origin/main` using the shared hook. It's the classic chat interface with ChatMessage bubbles and a TaskLane side panel.

**Files:**
- Create: `frontend/src/pages/FlowPilotPage.tsx`
- Reference: `git show origin/main:frontend/src/pages/AssistantChatPage.tsx` (production version)
- Reference: `frontend/src/hooks/useAssistantSession.ts` (from Task 1)

Key differences from the cockpit version:
- Uses `ChatMessage` component for message bubbles (not compact conversation log)
- Uses `TaskLane` side panel (not StepsPanel/FlowPilotAsks/WhatWeKnow)
- Has `handleTaskSubmit` for TaskLane responses
- No IncidentHeader, no triage state, no drag-resizable split
- Page title: "FlowPilot"
- Empty state heading: "FlowPilot"

- [ ] **Step 1: Create the FlowPilotPage component**

Create `frontend/src/pages/FlowPilotPage.tsx`:

```typescript
import { useEffect } from 'react'
import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText } from 'lucide-react'
import { cn } from '@/lib/utils'
import { PageMeta } from '@/components/common/PageMeta'
import { aiSessionsApi } from '@/api/aiSessions'
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
import { ChatMessage } from '@/components/assistant/ChatMessage'
import { TaskLane } from '@/components/assistant/TaskLane'
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
import { ViewToggle } from '@/components/assistant/ViewToggle'
import { useAssistantSession } from '@/hooks/useAssistantSession'

export default function FlowPilotPage() {
  const session = useAssistantSession()

  // Handle prefill from dashboard / command palette
  useEffect(() => {
    session.handlePrefill('/assistant')
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
    if (!session.activeChatId || session.loading) return

    const parts: string[] = []
    for (const r of responses) {
      const name = r.type === 'question' ? `Q: ${r.text}` : r.label || 'Check'
      if (r.state === 'done' && r.value.trim()) {
        parts.push(`**${name}:**\n\`\`\`\n${r.value.trim()}\n\`\`\``)
      } else if (r.state === 'skipped') {
        parts.push(`**${name}:** _(skipped)_`)
      }
    }
    const userMessage = parts.join('\n\n')
    const sendChatId = session.activeChatId

    session.setInput('')
    // We need to directly call the API here since handleSend reads from input state
    session.setShowTaskLane(false)
    session.setActiveQuestions([])
    session.setActiveActions([])

    try {
      // Add user message to messages manually
      // (This is page-specific flow — not in the shared hook)
      const response = await aiSessionsApi.sendChatMessage(sendChatId, { message: userMessage })
      if (session.currentChatRef.current !== sendChatId) return
      session.processResponse(response, sendChatId)

      const hasQuestions = response.questions && response.questions.length > 0
      const hasActions = response.actions && response.actions.length > 0
      if (!hasQuestions && !hasActions) {
        session.setShowTaskLane(false)
        session.setActiveQuestions([])
        session.setActiveActions([])
      }
    } catch {
      // Error handled by processResponse guard
    }
  }

  return (
    <>
      <PageMeta title="FlowPilot" />
      <div className="flex h-[calc(100vh-3.5rem)]">
        {/* Chat Sidebar — desktop */}
        {!session.sidebarCollapsed && (
          <div className="hidden sm:block">
            <ChatSidebar
              chats={session.chats}
              activeChatId={session.activeChatId}
              onSelectChat={session.selectChat}
              onNewChat={session.handleNewChat}
              onDeleteChat={session.handleDeleteChat}
              onTogglePin={session.handleTogglePin}
              onToggleCollapse={session.toggleSidebarCollapse}
            />
          </div>
        )}
        {/* Chat Sidebar — mobile */}
        <div className="sm:hidden">
          <ChatSidebar
            chats={session.chats}
            activeChatId={session.activeChatId}
            onSelectChat={session.selectChat}
            onNewChat={session.handleNewChat}
            onDeleteChat={session.handleDeleteChat}
            onTogglePin={session.handleTogglePin}
            mobileOpen={session.mobileSidebarOpen}
            onMobileClose={() => session.setMobileSidebarOpen(false)}
          />
        </div>

        {/* Main area */}
        <div className="flex-1 flex flex-col min-w-0">
          {/* Collapsed sidebar bar */}
          {session.sidebarCollapsed && (
            <div className="hidden sm:block">
              <ChatSidebarCollapsedBar
                chats={session.chats}
                activeChatId={session.activeChatId}
                onNewChat={session.handleNewChat}
                onExpand={session.toggleSidebarCollapse}
              />
            </div>
          )}

          {/* Chat content row */}
          <div className="flex-1 flex min-w-0 min-h-0">
            <div className="flex-1 flex flex-col min-w-0">
              {/* Mobile header */}
              <div className="sm:hidden flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
                <button
                  onClick={() => session.setMobileSidebarOpen(true)}
                  className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-[var(--color-bg-elevated)] transition-colors"
                >
                  <MessageSquare size={16} />
                  Chats
                </button>
                <div className="flex-1" />
                {session.activeChatId && (
                  <ViewToggle currentView="flowpilot" sessionId={session.activeChatId} />
                )}
                <button
                  onClick={session.handleNewChat}
                  className="rounded-lg px-3 py-2 text-sm font-medium text-primary hover:bg-primary/10 transition-colors"
                >
                  + New
                </button>
              </div>

              {session.activeChatId ? (
                <>
                  {/* Desktop view toggle bar */}
                  <div className="hidden sm:flex items-center justify-end px-4 py-1.5 border-b border-border/50">
                    <ViewToggle currentView="flowpilot" sessionId={session.activeChatId} />
                  </div>

                  {/* Messages */}
                  <div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 space-y-4">
                    {session.messages.length === 0 && !session.loading && (
                      <div className="flex flex-col items-center justify-center h-full text-center">
                        <div className="w-16 h-16 rounded-full bg-accent-dim flex items-center justify-center mb-4">
                          <Sparkles size={28} className="text-primary" />
                        </div>
                        <h2 className="text-lg font-heading font-semibold text-foreground mb-2">
                          FlowPilot
                        </h2>
                        <p className="text-sm text-muted-foreground max-w-md">
                          Ask me anything about IT infrastructure, networking, Active Directory,
                          cloud platforms, or troubleshooting. I'll also suggest relevant flows from your team's library.
                        </p>
                      </div>
                    )}
                    {session.messages.map((msg, i) => (
                      <ChatMessage
                        key={i}
                        role={msg.role}
                        content={msg.content}
                        suggestedFlows={msg.suggestedFlows}
                      />
                    ))}
                    {session.loading && (
                      <div className="flex gap-3">
                        <div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center">
                          <Sparkles size={14} className="text-primary" />
                        </div>
                        <div className="bg-input border border-border rounded-2xl px-4 py-3">
                          <Loader2 size={16} className="animate-spin text-primary" />
                        </div>
                      </div>
                    )}
                    <div ref={session.messagesEndRef} />
                  </div>

                  {/* Rich Input — same as production */}
                  <div className="px-3 sm:px-6 py-3 shrink-0">
                    <div
                      className="max-w-3xl mx-auto"
                      onDragOver={session.handleDragOver}
                      onDragEnter={session.handleDragEnter}
                      onDragLeave={session.handleDragLeave}
                      onDrop={session.handleDrop}
                    >
                      <div className={cn(
                        'relative rounded-xl border transition-all',
                        session.loading ? 'border-border/50 opacity-50' :
                        session.isDragOver ? 'border-primary/50 bg-primary/5' :
                        'border-border focus-within:border-[rgba(96,165,250,0.3)] focus-within:ring-1 focus-within:ring-primary/20'
                      )} style={{ background: 'var(--color-bg-card)' }}>
                        {session.isDragOver && (
                          <div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl border-2 border-dashed border-primary/50 bg-primary/5 pointer-events-none">
                            <div className="flex items-center gap-2 text-sm text-primary">
                              <ImagePlus size={18} />
                              Drop files to attach
                            </div>
                          </div>
                        )}

                        <textarea
                          ref={session.inputRef}
                          value={session.input}
                          onChange={e => session.setInput(e.target.value)}
                          onKeyDown={session.handleKeyDown}
                          onPaste={session.handlePaste}
                          placeholder={session.loading ? 'AI is thinking...' : 'Type a message, paste a screenshot, or drag a file...'}
                          disabled={session.loading}
                          rows={1}
                          className="w-full resize-none bg-transparent px-4 pt-3 pb-1 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed"
                          style={{ minHeight: '40px', maxHeight: '150px' }}
                        />

                        {session.pendingUploads.length > 0 && (
                          <div className="flex gap-2 flex-wrap px-4 pb-1">
                            {session.pendingUploads.map((upload) => (
                              <div key={upload.id} className="relative w-12 h-12 rounded-lg overflow-hidden border border-border bg-background">
                                {upload.preview ? (
                                  <img src={upload.preview} alt="" className="w-full h-full object-cover" />
                                ) : (
                                  <div className="w-full h-full flex items-center justify-center text-[0.5rem] text-muted-foreground px-1 text-center">
                                    {upload.file.name.split('.').pop()?.toUpperCase()}
                                  </div>
                                )}
                                {upload.status === 'uploading' && (
                                  <div className="absolute inset-0 bg-background/60 flex items-center justify-center">
                                    <Loader2 size={12} className="animate-spin text-primary" />
                                  </div>
                                )}
                                {upload.status === 'done' && (
                                  <button type="button" onClick={() => session.handleRemoveUpload(upload.id)} className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-background border border-border flex items-center justify-center hover:bg-rose-500/20 transition-colors">
                                    <X size={8} className="text-muted-foreground" />
                                  </button>
                                )}
                                {upload.status === 'error' && (
                                  <div className="absolute inset-0 bg-rose-500/20 border-2 border-rose-500 flex items-center justify-center cursor-pointer" onClick={() => session.retryUpload(upload.id)}>
                                    <RotateCcw size={10} className="text-rose-500" />
                                  </div>
                                )}
                              </div>
                            ))}
                          </div>
                        )}

                        {session.showLogs && (
                          <div className="px-4 pb-1">
                            <div className="flex items-center justify-between mb-1">
                              <span className="text-[0.625rem] uppercase tracking-wide text-muted-foreground font-sans">Paste logs or error output</span>
                              <button type="button" onClick={() => { session.setShowLogs(false); session.setLogContent('') }} className="text-muted-foreground hover:text-foreground"><X size={14} /></button>
                            </div>
                            <textarea
                              value={session.logContent}
                              onChange={(e) => session.setLogContent(e.target.value)}
                              placeholder="Paste event viewer logs, error messages, PowerShell output..."
                              rows={3}
                              className="w-full resize-none rounded-lg border border-border bg-background p-2 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none"
                            />
                          </div>
                        )}

                        <div className="flex items-center justify-between px-3 py-1.5 border-t border-border/50">
                          <div className="flex items-center gap-0.5">
                            <input ref={session.fileInputRef} type="file" multiple accept={session.ACCEPTED_FILE_TYPES} onChange={session.handleFileSelect} className="hidden" />
                            <button type="button" onClick={() => session.fileInputRef.current?.click()} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors disabled:opacity-40" title="Attach files">
                              <Paperclip size={14} />
                              <span className="hidden sm:inline">Attach</span>
                            </button>
                            {!session.showLogs && (
                              <button type="button" onClick={() => session.setShowLogs(true)} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors disabled:opacity-40" title="Paste logs">
                                <Terminal size={14} />
                                <span className="hidden sm:inline">Paste Logs</span>
                              </button>
                            )}
                            {session.messages.length >= 2 && (
                              <>
                                <button type="button" onClick={() => session.setShowStatusUpdate(true)} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-blue-400 hover:bg-blue-500/10 transition-colors disabled:opacity-40" title="Share status update">
                                  <FileText size={14} />
                                  <span className="hidden sm:inline">Update</span>
                                </button>
                                <button type="button" onClick={() => session.setShowConclude(true)} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors disabled:opacity-40" title="Conclude session">
                                  <Flag size={14} />
                                  <span className="hidden sm:inline">Conclude</span>
                                </button>
                              </>
                            )}
                            {!session.showTaskLane && (session.activeQuestions.length > 0 || session.activeActions.length > 0) && (
                              <button
                                type="button"
                                onClick={() => session.setShowTaskLane(true)}
                                className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-accent-text hover:text-foreground hover:bg-accent-dim transition-colors"
                                title="Show task panel"
                              >
                                <ListChecks size={14} />
                                Tasks ({session.activeQuestions.length + session.activeActions.length})
                              </button>
                            )}
                          </div>
                          <button type="button" onClick={session.handleSend} disabled={!session.input.trim() || session.loading} className={cn(
                            'flex h-8 w-8 items-center justify-center rounded-lg transition-all',
                            session.input.trim() && !session.loading ? 'bg-primary text-white hover:brightness-110 active:scale-95' : 'bg-secondary text-muted-foreground cursor-not-allowed'
                          )} title="Send message">
                            <Send size={15} />
                          </button>
                        </div>
                      </div>
                    </div>
                  </div>
                </>
              ) : (
                <div className="flex flex-col items-center justify-center h-full text-center">
                  <div className="w-20 h-20 rounded-full bg-accent-dim flex items-center justify-center mb-4">
                    <Sparkles size={32} className="text-primary" />
                  </div>
                  <h2 className="text-xl font-heading font-semibold text-foreground mb-2">
                    FlowPilot
                  </h2>
                  <p className="text-sm text-muted-foreground max-w-md mb-6">
                    Your Senior Systems & Network Engineer. Ask anything about IT infrastructure,
                    or start a new chat to get personalized help with your team's flows.
                  </p>
                  <button
                    onClick={session.handleNewChat}
                    className="bg-primary text-white font-semibold text-sm rounded-lg px-6 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
                  >
                    Start a Conversation
                  </button>
                </div>
              )}
            </div>

            {/* Task lane */}
            {session.showTaskLane && (session.activeQuestions.length > 0 || session.activeActions.length > 0) && (
              <TaskLane
                questions={session.activeQuestions}
                actions={session.activeActions}
                sessionId={session.activeChatId}
                onSubmit={handleTaskSubmit}
                onClose={() => session.setShowTaskLane(false)}
                loading={session.loading}
              />
            )}
          </div>
        </div>

        {/* Conclude Session Modal */}
        <ConcludeSessionModal
          isOpen={session.showConclude}
          onClose={() => session.setShowConclude(false)}
          onConclude={session.handleConclude}
          onResumeNew={session.handleResumeNew}
          chatTitle={session.chats.find(c => c.id === session.activeChatId)?.title ?? 'Chat'}
          sessionId={session.activeChatId}
        />

        {/* Status Update Modal */}
        {session.activeChatId && (
          <StatusUpdateModal
            open={session.showStatusUpdate}
            onClose={() => session.setShowStatusUpdate(false)}
            onGenerate={(audience, length, context) =>
              aiSessionsApi.generateStatusUpdate(session.activeChatId!, { audience, length, context })
            }
            context="status"
          />
        )}
      </div>
    </>
  )
}
```

- [ ] **Step 2: Verify FlowPilotPage compiles**

Run: `cd frontend && npx tsc --noEmit`

Note: `ViewToggle` doesn't exist yet — expect an error for that import. Comment it out temporarily or create a stub. All other imports should resolve.

- [ ] **Step 3: Commit**

```bash
git add frontend/src/pages/FlowPilotPage.tsx
git commit -m "feat: create FlowPilotPage with classic chat layout"
```

---

### Task 3: Rename `AssistantChatPage` to `CockpitPage` and refactor to use the hook

**Files:**
- Rename: `frontend/src/pages/AssistantChatPage.tsx` → `frontend/src/pages/CockpitPage.tsx`
- Modify: `frontend/src/pages/CockpitPage.tsx` (refactor to use `useAssistantSession`)

The cockpit page keeps its current layout (IncidentHeader, StepsPanel, FlowPilotAsks, WhatWeKnow, drag-resizable conversation log) but replaces all the inline session management with the shared hook.

- [ ] **Step 1: Rename the file**

```bash
cd frontend && git mv src/pages/AssistantChatPage.tsx src/pages/CockpitPage.tsx
```

- [ ] **Step 2: Refactor CockpitPage to use useAssistantSession**

Edit `frontend/src/pages/CockpitPage.tsx`:

1. Change the default export from `AssistantChatPage` to `CockpitPage`
2. Import and call `useAssistantSession()`
3. Remove all the duplicated state and handlers that now live in the hook (everything from `useState` calls for `chats`, `messages`, `input`, `loading`, etc. through the file handling functions)
4. Keep cockpit-specific state: `triageMeta`, `workZonePct`, `activeStepIndex`, `completedSteps`, `showOnboarding`, `splitContainerRef`
5. Keep cockpit-specific handlers: `handleTriageFieldSave`, `handleEvidenceAdd`, `handleEvidenceEdit`, `handleStepComplete`, `handleStepSelect`, `mergeTriageUpdate`, `handleDragStart` (for the split handle), `dismissOnboarding`
6. Wire `onSessionLoadedRef` to populate triage state on session load
7. Wire `onTriageUpdateRef` to call `mergeTriageUpdate` on AI responses
8. Add `ViewToggle` import (will be created in Task 4)
9. Add feature flag redirect: if `useFeatureFlag('flowpilot_cockpit')` returns false, redirect to `/assistant/:sessionId`

The structure becomes:

```typescript
import { useState, useRef, useCallback, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText, GripHorizontal } from 'lucide-react'
import { cn } from '@/lib/utils'
import { PageMeta } from '@/components/common/PageMeta'
import { aiSessionsApi } from '@/api/aiSessions'
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
import { IncidentHeader } from '@/components/assistant/IncidentHeader'
import { StepsPanel } from '@/components/assistant/StepsPanel'
import { FlowPilotAsks } from '@/components/assistant/FlowPilotAsks'
import { WhatWeKnow } from '@/components/assistant/WhatWeKnow'
import { ViewToggle } from '@/components/assistant/ViewToggle'
import { useAssistantSession } from '@/hooks/useAssistantSession'
import { useFeatureFlag } from '@/hooks/useFeatureFlag'
import type { TriageMeta, EvidenceItem, TriageUpdate } from '@/types/ai-session'

export default function CockpitPage() {
  const navigate = useNavigate()
  const hasCockpit = useFeatureFlag('flowpilot_cockpit')
  const session = useAssistantSession()

  // Cockpit-specific state
  const [triageMeta, setTriageMeta] = useState<TriageMeta>({
    client_name: null, asset_name: null, issue_category: null,
    triage_hypothesis: null, evidence_items: [],
  })
  const [workZonePct, setWorkZonePct] = useState(() => {
    const saved = localStorage.getItem('rf-assistant-work-zone-height')
    return saved ? parseFloat(saved) : 55
  })
  const [activeStepIndex, setActiveStepIndex] = useState(0)
  const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set())
  const [showOnboarding, setShowOnboarding] = useState(() =>
    !localStorage.getItem('rf-cockpit-onboarded')
  )
  const splitContainerRef = useRef<HTMLDivElement>(null)

  // Redirect if user doesn't have cockpit access
  useEffect(() => {
    if (!hasCockpit && session.activeChatId) {
      navigate(`/assistant/${session.activeChatId}`, { replace: true })
    } else if (!hasCockpit) {
      navigate('/assistant', { replace: true })
    }
  }, [hasCockpit, session.activeChatId, navigate])

  // Wire up triage data from session loads
  useEffect(() => {
    session.onSessionLoadedRef.current = (detail) => {
      setTriageMeta({
        client_name: detail.client_name ?? null,
        asset_name: detail.asset_name ?? null,
        issue_category: detail.issue_category ?? null,
        triage_hypothesis: detail.triage_hypothesis ?? null,
        evidence_items: (detail.evidence_items as EvidenceItem[]) ?? [],
      })
    }
    return () => { session.onSessionLoadedRef.current = null }
  }, [session.onSessionLoadedRef])

  // Wire up triage updates from AI responses
  useEffect(() => {
    session.onTriageUpdateRef.current = mergeTriageUpdate
    return () => { session.onTriageUpdateRef.current = null }
  }, [session.onTriageUpdateRef]) // eslint-disable-line react-hooks/exhaustive-deps

  // Handle prefill
  useEffect(() => {
    session.handlePrefill('/cockpit')
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  // ... cockpit-specific handlers (handleTriageFieldSave, handleEvidenceAdd,
  //     handleEvidenceEdit, handleStepComplete, handleStepSelect,
  //     mergeTriageUpdate, handleDragStart, dismissOnboarding)
  //     — keep exactly as they are in the current AssistantChatPage

  // ... JSX rendering — keep the cockpit layout exactly as-is,
  //     but replace direct state references with session.xxx
  //     and add <ViewToggle currentView="cockpit" sessionId={session.activeChatId} />
  //     in the header area
}
```

The implementer should:
1. Keep ALL existing cockpit-specific handlers verbatim
2. Keep ALL existing cockpit JSX layout verbatim
3. Replace references like `activeChatId` with `session.activeChatId`, `messages` with `session.messages`, etc.
4. Add the feature flag redirect `useEffect`
5. Add `ViewToggle` next to the existing action buttons in the IncidentHeader area

- [ ] **Step 3: Verify CockpitPage compiles**

Run: `cd frontend && npx tsc --noEmit`

Expect `ViewToggle` import error (not created yet). All other imports should resolve.

- [ ] **Step 4: Commit**

```bash
git add -A
git commit -m "refactor: rename AssistantChatPage to CockpitPage, consume useAssistantSession hook"
```

---

### Task 4: Create `ViewToggle` component

**Files:**
- Create: `frontend/src/components/assistant/ViewToggle.tsx`

A segmented control that lets users switch between FlowPilot and FlowPilot Cockpit.

- [ ] **Step 1: Create the ViewToggle component**

Create `frontend/src/components/assistant/ViewToggle.tsx`:

```typescript
import { useNavigate } from 'react-router-dom'
import { cn } from '@/lib/utils'
import { useFeatureFlag } from '@/hooks/useFeatureFlag'

interface ViewToggleProps {
  currentView: 'flowpilot' | 'cockpit'
  sessionId: string
}

export function ViewToggle({ currentView, sessionId }: ViewToggleProps) {
  const navigate = useNavigate()
  const hasCockpit = useFeatureFlag('flowpilot_cockpit')

  // Only show toggle if user has cockpit access
  if (!hasCockpit) return null

  const handleSwitch = (view: 'flowpilot' | 'cockpit') => {
    if (view === currentView) return
    const path = view === 'cockpit'
      ? `/cockpit/${sessionId}`
      : `/assistant/${sessionId}`
    navigate(path)
  }

  return (
    <div className="flex items-center rounded-lg border border-border bg-card p-0.5 text-xs">
      <button
        onClick={() => handleSwitch('flowpilot')}
        className={cn(
          'rounded-md px-2.5 py-1 font-medium transition-colors',
          currentView === 'flowpilot'
            ? 'bg-elevated text-foreground'
            : 'text-muted-foreground hover:text-foreground'
        )}
      >
        FlowPilot
      </button>
      <button
        onClick={() => handleSwitch('cockpit')}
        className={cn(
          'rounded-md px-2.5 py-1 font-medium transition-colors',
          currentView === 'cockpit'
            ? 'bg-elevated text-foreground'
            : 'text-muted-foreground hover:text-foreground'
        )}
      >
        Cockpit
      </button>
    </div>
  )
}
```

- [ ] **Step 2: Verify it compiles**

Run: `cd frontend && npx tsc --noEmit`

Expected: No errors from `ViewToggle.tsx`.

- [ ] **Step 3: Commit**

```bash
git add frontend/src/components/assistant/ViewToggle.tsx
git commit -m "feat: add ViewToggle component for FlowPilot/Cockpit switching"
```

---

### Task 5: Update router with new routes

**Files:**
- Modify: `frontend/src/router.tsx`

- [ ] **Step 1: Add lazy imports and routes**

In `frontend/src/router.tsx`:

1. Add lazy import for `CockpitPage` and `FlowPilotPage`:
```typescript
const FlowPilotPage = lazyWithRetry(() => import('@/pages/FlowPilotPage'))
const CockpitPage = lazyWithRetry(() => import('@/pages/CockpitPage'))
```

2. Update the `AssistantChatPage` import line — remove it entirely (the file no longer exists).

3. Update the route entries inside the `children` array. Find these lines:
```typescript
{ path: 'assistant', element: page(AssistantChatPage) },
{ path: 'assistant/:sessionId', element: page(AssistantChatPage) },
```

Replace with:
```typescript
{ path: 'assistant', element: page(FlowPilotPage) },
{ path: 'assistant/:sessionId', element: page(FlowPilotPage) },
{ path: 'cockpit', element: page(CockpitPage) },
{ path: 'cockpit/:sessionId', element: page(CockpitPage) },
```

- [ ] **Step 2: Verify it compiles**

Run: `cd frontend && npx tsc --noEmit`

Expected: PASS (no references to `AssistantChatPage` remain).

- [ ] **Step 3: Commit**

```bash
git add frontend/src/router.tsx
git commit -m "feat: add /cockpit routes, update /assistant to use FlowPilotPage"
```

---

### Task 6: Add FlowPilot to sidebar navigation

**Files:**
- Modify: `frontend/src/components/layout/Sidebar.tsx`

- [ ] **Step 1: Add FlowPilot rail entry**

In `frontend/src/components/layout/Sidebar.tsx`:

1. Add import at the top:
```typescript
import { Sparkles } from 'lucide-react'
import { useFeatureFlag } from '@/hooks/useFeatureFlag'
```

2. Inside the `Sidebar` component, add feature flag check:
```typescript
const hasCockpit = useFeatureFlag('flowpilot_cockpit')
```

3. In the `railGroups` array, add a new entry as the **second item** (after Home, before History):

```typescript
{
  href: '/assistant', icon: Sparkles, label: 'FlowPilot', shortLabel: 'FP',
  matchPaths: ['/assistant', '/cockpit'],
  children: [
    { href: '/assistant', label: 'FlowPilot' },
    ...(hasCockpit ? [{ href: '/cockpit', label: 'FlowPilot Cockpit' }] : []),
  ],
},
```

4. In the `sections` array (used when sidebar is pinned), add under the "RESOLVE" section items:

```typescript
{
  href: '/assistant', icon: Sparkles, label: 'FlowPilot', shortLabel: 'FP',
  matchPaths: ['/assistant', '/cockpit'],
},
```

And if `hasCockpit` is true, add a second entry below it:
```typescript
...(hasCockpit ? [{
  href: '/cockpit', icon: Sparkles, label: 'FlowPilot Cockpit', shortLabel: 'Cockpit',
  matchPaths: ['/cockpit'],
}] : []),
```

- [ ] **Step 2: Verify it compiles**

Run: `cd frontend && npx tsc --noEmit`

Expected: PASS.

- [ ] **Step 3: Commit**

```bash
git add frontend/src/components/layout/Sidebar.tsx
git commit -m "feat: add FlowPilot and FlowPilot Cockpit to sidebar navigation"
```

---

### Task 7: Add `preferredFlowPilotView` to user preferences store

**Files:**
- Modify: `frontend/src/store/userPreferencesStore.ts`

- [ ] **Step 1: Add the preference**

In `frontend/src/store/userPreferencesStore.ts`:

1. Add type:
```typescript
type FlowPilotView = 'flowpilot' | 'cockpit'
```

2. Add to interface:
```typescript
preferredFlowPilotView: FlowPilotView
setPreferredFlowPilotView: (view: FlowPilotView) => void
```

3. Add to store implementation:
```typescript
preferredFlowPilotView: 'flowpilot',
setPreferredFlowPilotView: (view) => set({ preferredFlowPilotView: view }),
```

- [ ] **Step 2: Verify it compiles**

Run: `cd frontend && npx tsc --noEmit`

Expected: PASS.

- [ ] **Step 3: Commit**

```bash
git add frontend/src/store/userPreferencesStore.ts
git commit -m "feat: add preferredFlowPilotView to user preferences store"
```

---

### Task 8: Update `StartSessionInput` with launch view toggle

**Files:**
- Modify: `frontend/src/components/dashboard/StartSessionInput.tsx`

- [ ] **Step 1: Add view preference toggle**

In `frontend/src/components/dashboard/StartSessionInput.tsx`:

1. Add imports:
```typescript
import { useFeatureFlag } from '@/hooks/useFeatureFlag'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
```

2. Inside the component, add:
```typescript
const hasCockpit = useFeatureFlag('flowpilot_cockpit')
const preferredView = useUserPreferencesStore(s => s.preferredFlowPilotView)
const setPreferredView = useUserPreferencesStore(s => s.setPreferredFlowPilotView)
```

3. Update `handleSubmit` — change the navigate target:
```typescript
// Replace: navigate('/assistant', { state })
const target = hasCockpit && preferredView === 'cockpit' ? '/cockpit' : '/assistant'
navigate(target, { state })
```

4. Update `handleSuggestionClick` similarly:
```typescript
const handleSuggestionClick = (suggestion: string) => {
  const target = hasCockpit && preferredView === 'cockpit' ? '/cockpit' : '/assistant'
  navigate(target, { state: { prefill: suggestion } })
}
```

5. Add a view toggle near the submit button. Inside the bottom toolbar `<div>`, before the send button, add:

```typescript
{hasCockpit && (
  <div className="flex items-center rounded-lg border border-border bg-card p-0.5 text-xs mr-2">
    <button
      type="button"
      onClick={() => setPreferredView('flowpilot')}
      className={cn(
        'rounded-md px-2 py-1 font-medium transition-colors',
        preferredView === 'flowpilot'
          ? 'bg-elevated text-foreground'
          : 'text-muted-foreground hover:text-foreground'
      )}
    >
      FlowPilot
    </button>
    <button
      type="button"
      onClick={() => setPreferredView('cockpit')}
      className={cn(
        'rounded-md px-2 py-1 font-medium transition-colors',
        preferredView === 'cockpit'
          ? 'bg-elevated text-foreground'
          : 'text-muted-foreground hover:text-foreground'
      )}
    >
      Cockpit
    </button>
  </div>
)}
```

- [ ] **Step 2: Verify it compiles**

Run: `cd frontend && npx tsc --noEmit`

Expected: PASS.

- [ ] **Step 3: Commit**

```bash
git add frontend/src/components/dashboard/StartSessionInput.tsx
git commit -m "feat: add launch view preference toggle to StartSessionInput"
```

---

### Task 9: Rename "AI Assistant" to "FlowPilot" in remaining UI labels

**Files:**
- Search and update any remaining references to "AI Assistant" in user-facing text

- [ ] **Step 1: Find all "AI Assistant" references**

Run:
```bash
cd frontend && grep -rn "AI Assistant" src/ --include="*.tsx" --include="*.ts"
```

For each hit, replace "AI Assistant" with "FlowPilot" in user-facing strings (page titles, headings, descriptions, placeholders). Do NOT rename file names, variable names, or comments in this task — only user-facing text.

Common locations to check:
- `PageMeta title` props
- Empty state headings
- Sidebar labels
- Breadcrumbs
- Any tooltip or aria-label text

- [ ] **Step 2: Verify it compiles**

Run: `cd frontend && npx tsc --noEmit`

Expected: PASS.

- [ ] **Step 3: Commit**

```bash
git add -A
git commit -m "chore: rename 'AI Assistant' to 'FlowPilot' in user-facing text"
```

---

### Task 10: Final build verification

- [ ] **Step 1: Run full TypeScript build**

Run: `cd frontend && npm run build`

This is stricter than `npx tsc --noEmit` — it enforces `noUnusedLocals` and `noUnusedParameters`. Fix any errors.

- [ ] **Step 2: Verify no stale AssistantChatPage references**

Run:
```bash
cd frontend && grep -rn "AssistantChatPage" src/ --include="*.tsx" --include="*.ts"
```

Expected: Zero results. If any remain, update them to reference either `FlowPilotPage` or `CockpitPage`.

- [ ] **Step 3: Commit any fixes**

```bash
git add -A
git commit -m "fix: resolve build errors from side-by-side refactor"
```
