# Editor-Embedded Flow Assist - Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace the standalone `/ai/chat` page with context-aware AI side panels embedded in the Troubleshooting and Procedural editors, supporting targeted actions, ghost node suggestions, and config-driven model routing.

**Architecture:** Backend extends existing AI chat service with action-type dispatch, delta responses, and model tier routing. Frontend adds a shared `EditorAIPanel` component, `ContextMenu` component, and `useEditorAI` hook that integrate into both editors. Ghost nodes use a `_suggestion` flag in tree/step structures with zundo pause/resume for clean undo history.

**Tech Stack:** FastAPI, SQLAlchemy, Alembic, React 19, Zustand (zundo), Tailwind CSS, Anthropic/Google AI SDK

**Design doc:** `docs/plans/2026-03-06-editor-embedded-flow-assist-design.md`

---

## Phase 1: Bug Fix + Backend Foundation

### Task 1: Fix Orphan Validation False Positive

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

**Step 1: Locate and fix the bug**

In `frontend/src/store/treeEditorStore.ts`, line 858, change:

```typescript
// BEFORE (line 858)
if (id !== 'root' && !referencedIds.has(id)) {

// AFTER
if (id !== state.treeStructure?.id && !referencedIds.has(id)) {
```

The root node's ID is not always `'root'` — AI-generated trees use descriptive IDs like `"verify-account-exists"`. The root is never referenced by `next_node_id` so it gets flagged as orphaned.

**Step 2: Verify the fix**

Run: `cd frontend && npm run build`
Expected: Build succeeds with no type errors.

**Step 3: Commit**

```bash
git add frontend/src/store/treeEditorStore.ts
git commit -m "fix: use actual root node ID in orphan validation check"
```

---

### Task 2: Add Model Tier Configuration

**Files:**
- Modify: `backend/app/core/config.py:77-85`

**Step 1: Write the failing test**

Create test in `backend/tests/test_config_model_tiers.py`:

```python
"""Tests for AI model tier configuration."""
from app.core.config import settings


def test_ai_model_tiers_exist():
    """Model tier config has fast and standard entries."""
    assert "fast" in settings.AI_MODEL_TIERS
    assert "standard" in settings.AI_MODEL_TIERS


def test_action_model_map_covers_all_actions():
    """Every action type maps to a valid tier."""
    valid_tiers = set(settings.AI_MODEL_TIERS.keys())
    for action, tier in settings.ACTION_MODEL_MAP.items():
        assert tier in valid_tiers, f"Action '{action}' maps to unknown tier '{tier}'"


def test_get_model_for_action():
    """get_model_for_action resolves tier to model name."""
    model = settings.get_model_for_action("generate_full")
    assert isinstance(model, str)
    assert len(model) > 0


def test_get_model_for_action_unknown_falls_back():
    """Unknown action types fall back to standard tier."""
    model = settings.get_model_for_action("nonexistent_action")
    assert model == settings.AI_MODEL_TIERS["standard"]
```

**Step 2: Run test to verify it fails**

Run: `cd backend && python -m pytest tests/test_config_model_tiers.py -v`
Expected: FAIL — `AI_MODEL_TIERS` attribute does not exist.

**Step 3: Add model tier config to Settings class**

In `backend/app/core/config.py`, after the existing AI config lines (~line 85), add:

```python
    # Model tier routing
    AI_MODEL_TIERS: dict[str, str] = {
        "fast": "claude-haiku-4-5-20251001",
        "standard": "claude-sonnet-4-6-20250514",
    }

    ACTION_MODEL_MAP: dict[str, str] = {
        "generate_full": "standard",
        "generate_branch": "standard",
        "modify_node": "fast",
        "add_steps": "standard",
        "quick_action": "fast",
        "open_chat": "standard",
        "variable_inference": "fast",
    }

    def get_model_for_action(self, action_type: str) -> str:
        """Resolve an action type to a concrete model name."""
        tier = self.ACTION_MODEL_MAP.get(action_type, "standard")
        return self.AI_MODEL_TIERS.get(tier, self.AI_MODEL_TIERS["standard"])
```

**Step 4: Run test to verify it passes**

Run: `cd backend && python -m pytest tests/test_config_model_tiers.py -v`
Expected: All 4 tests PASS.

**Step 5: Commit**

```bash
git add backend/app/core/config.py backend/tests/test_config_model_tiers.py
git commit -m "feat: add config-driven AI model tier routing"
```

---

### Task 3: Extend AI Chat Session Model

**Files:**
- Modify: `backend/app/models/ai_chat_session.py`
- Create: `backend/alembic/versions/051_extend_ai_chat_session.py`

**Step 1: Add columns to AIChatSession model**

In `backend/app/models/ai_chat_session.py`, add after the existing column definitions:

```python
    # Editor-embedded session: links to a specific tree/flow
    tree_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        ForeignKey("trees.id", ondelete="CASCADE"),
        nullable=True,
        index=True,
    )
    archived_at: Mapped[Optional[datetime]] = mapped_column(
        DateTime(timezone=True),
        nullable=True,
    )
```

Add the relationship (if `Tree` model import is needed, add it):

```python
    tree: Mapped[Optional["Tree"]] = relationship("Tree", foreign_keys=[tree_id])
```

**Step 2: Create the Alembic migration**

Run: `cd backend && alembic revision -m "extend ai chat session with tree_id and archived_at" --rev-id=051`

Then edit the generated file `backend/alembic/versions/051_extend_ai_chat_session.py`:

```python
"""extend ai chat session with tree_id and archived_at

Revision ID: 051
"""
from alembic import op
import sqlalchemy as sa

revision = "051"
down_revision = "050"  # Verify this matches the actual previous migration
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.add_column(
        "ai_chat_sessions",
        sa.Column("tree_id", sa.UUID(), nullable=True),
    )
    op.add_column(
        "ai_chat_sessions",
        sa.Column("archived_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.create_index("ix_ai_chat_sessions_tree_id", "ai_chat_sessions", ["tree_id"])
    op.create_foreign_key(
        "fk_ai_chat_sessions_tree_id",
        "ai_chat_sessions",
        "trees",
        ["tree_id"],
        ["id"],
        ondelete="CASCADE",
    )


def downgrade() -> None:
    op.drop_constraint("fk_ai_chat_sessions_tree_id", "ai_chat_sessions", type_="foreignkey")
    op.drop_index("ix_ai_chat_sessions_tree_id", table_name="ai_chat_sessions")
    op.drop_column("ai_chat_sessions", "archived_at")
    op.drop_column("ai_chat_sessions", "tree_id")
```

**Step 3: Run migration**

Run: `cd backend && alembic upgrade head`
Expected: Migration applies successfully.

**Step 4: Verify with psql**

Run: `docker exec -it patherly_postgres psql -U postgres -d patherly -c "\d ai_chat_sessions" | grep -E "tree_id|archived_at"`
Expected: Both columns visible.

**Step 5: Commit**

```bash
git add backend/app/models/ai_chat_session.py backend/alembic/versions/051_extend_ai_chat_session.py
git commit -m "feat: extend AI chat session with tree_id and archived_at"
```

---

### Task 4: Create AI Suggestion Model + Migration

**Files:**
- Create: `backend/app/models/ai_suggestion.py`
- Create: `backend/alembic/versions/052_add_ai_suggestion_table.py`
- Modify: `backend/alembic/env.py` (import new model)

**Step 1: Create the model**

Create `backend/app/models/ai_suggestion.py`:

```python
"""AI Suggestion model for tracking AI-applied changes to flows."""
import uuid
from datetime import datetime, timezone

from sqlalchemy import DateTime, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.core.database import Base


class AISuggestion(Base):
    __tablename__ = "ai_suggestions"

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
    )
    tree_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("trees.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
    )
    user_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
    )
    session_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("ai_chat_sessions.id", ondelete="SET NULL"),
        nullable=True,
    )
    action_type: Mapped[str] = mapped_column(
        String(50), nullable=False
    )
    target_node_id: Mapped[str | None] = mapped_column(
        String(255), nullable=True
    )
    changes_json: Mapped[dict] = mapped_column(
        JSONB, nullable=False, default=dict
    )
    status: Mapped[str] = mapped_column(
        String(20), nullable=False, default="pending"
    )  # pending, accepted, dismissed
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        default=lambda: datetime.now(timezone.utc),
        nullable=False,
    )
    resolved_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )

    # Relationships
    tree: Mapped["Tree"] = relationship("Tree", foreign_keys=[tree_id])
    user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
    session: Mapped["AIChatSession"] = relationship(
        "AIChatSession", foreign_keys=[session_id]
    )
```

**Step 2: Import in alembic/env.py**

In `backend/alembic/env.py`, add with the other model imports:

```python
from app.models.ai_suggestion import AISuggestion  # noqa: F401
```

**Step 3: Create migration manually**

Create `backend/alembic/versions/052_add_ai_suggestion_table.py`:

```python
"""add ai suggestion table

Revision ID: 052
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB

revision = "052"
down_revision = "051"
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.create_table(
        "ai_suggestions",
        sa.Column("id", UUID(as_uuid=True), primary_key=True),
        sa.Column("tree_id", UUID(as_uuid=True), sa.ForeignKey("trees.id", ondelete="CASCADE"), nullable=False),
        sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
        sa.Column("session_id", UUID(as_uuid=True), sa.ForeignKey("ai_chat_sessions.id", ondelete="SET NULL"), nullable=True),
        sa.Column("action_type", sa.String(50), nullable=False),
        sa.Column("target_node_id", sa.String(255), nullable=True),
        sa.Column("changes_json", JSONB, nullable=False, server_default="{}"),
        sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
        sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
        sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.create_index("ix_ai_suggestions_tree_id", "ai_suggestions", ["tree_id"])
    op.create_index("ix_ai_suggestions_user_id", "ai_suggestions", ["user_id"])


def downgrade() -> None:
    op.drop_index("ix_ai_suggestions_user_id", table_name="ai_suggestions")
    op.drop_index("ix_ai_suggestions_tree_id", table_name="ai_suggestions")
    op.drop_table("ai_suggestions")
```

**Step 4: Run migration**

Run: `cd backend && alembic upgrade head`
Expected: Table created successfully.

**Step 5: Verify**

Run: `docker exec -it patherly_postgres psql -U postgres -d patherly -c "\d ai_suggestions"`
Expected: Table schema displayed with all columns.

**Step 6: Commit**

```bash
git add backend/app/models/ai_suggestion.py backend/alembic/versions/052_add_ai_suggestion_table.py backend/alembic/env.py
git commit -m "feat: add AI suggestion audit trail table"
```

---

### Task 5: Add Action Type to AI Chat Schemas + Endpoints

**Files:**
- Modify: `backend/app/schemas/ai_chat.py`
- Modify: `backend/app/api/endpoints/ai_chat.py`

**Step 1: Write the failing test**

Add to `backend/tests/test_ai_chat.py` (or create new file `backend/tests/test_ai_chat_action_types.py`):

```python
"""Tests for action-type routing in AI chat endpoints."""
import pytest
from unittest.mock import AsyncMock, PropertyMock, patch


@pytest.fixture
def _enable_ai(monkeypatch):
    """Enable AI for tests without API key."""
    from app.core.config import Settings
    monkeypatch.setattr(Settings, "ai_enabled", PropertyMock(return_value=True))


@pytest.mark.asyncio
async def test_send_message_with_action_type(client, auth_headers, _enable_ai):
    """Messages can include an action_type parameter."""
    # Start a session first
    with patch("app.api.endpoints.ai_chat.ai_chat_service") as mock_svc:
        mock_svc.start_chat_session = AsyncMock(return_value={
            "session_id": "test-123",
            "greeting": "Hello",
            "current_phase": "scoping",
        })
        start_resp = await client.post(
            "/api/v1/ai/chat/sessions",
            json={"flow_type": "troubleshooting"},
            headers=auth_headers,
        )
        assert start_resp.status_code == 200

        # Send message with action_type
        mock_svc.send_message = AsyncMock(return_value={
            "content": "Here's a branch",
            "current_phase": "enrichment",
            "working_tree": None,
            "tree_metadata": None,
        })
        resp = await client.post(
            f"/api/v1/ai/chat/sessions/test-123/messages",
            json={
                "content": "Generate a branch from this node",
                "action_type": "generate_branch",
                "focal_node_id": "check-connectivity",
            },
            headers=auth_headers,
        )
        assert resp.status_code == 200
```

**Step 2: Run test to verify it fails**

Run: `cd backend && python -m pytest tests/test_ai_chat_action_types.py -v`
Expected: FAIL — `action_type` not accepted in schema.

**Step 3: Update schemas**

In `backend/app/schemas/ai_chat.py`, update `AIChatMessageRequest`:

```python
from typing import Literal, Optional

VALID_ACTION_TYPES = Literal[
    "generate_full",
    "generate_branch",
    "modify_node",
    "add_steps",
    "quick_action",
    "open_chat",
    "variable_inference",
]


class AIChatMessageRequest(BaseModel):
    content: str = Field(..., min_length=1, max_length=5000)
    action_type: Optional[VALID_ACTION_TYPES] = Field(
        default="open_chat",
        description="Type of AI action to perform, determines model tier and prompt",
    )
    focal_node_id: Optional[str] = Field(
        default=None,
        description="ID of the node/step being acted on (for targeted actions)",
    )
```

Also update `AIChatStartRequest` to accept optional `tree_id`:

```python
class AIChatStartRequest(BaseModel):
    flow_type: Literal["troubleshooting", "procedural"]
    tree_id: Optional[str] = Field(
        default=None,
        description="ID of existing tree to attach this session to (editor-embedded mode)",
    )
```

**Step 4: Update endpoint to pass action_type through**

In `backend/app/api/endpoints/ai_chat.py`, update the `send_message` endpoint to pass `action_type` and `focal_node_id` to the service:

```python
# In the send_message endpoint, after extracting request data:
result = await ai_chat_service.send_message(
    session_id=session_id,
    user_message=data.content,
    user_id=str(current_user.id),
    db=db,
    action_type=data.action_type,
    focal_node_id=data.focal_node_id,
)
```

In the `start_session` endpoint, pass `tree_id` if provided:

```python
# When creating session, pass tree_id
result = await ai_chat_service.start_chat_session(
    user_id=str(current_user.id),
    flow_type=data.flow_type,
    db=db,
    account_id=str(current_user.account_id) if current_user.account_id else None,
    tree_id=data.tree_id,
)
```

**Step 5: Update ai_chat_service.send_message signature**

In `backend/app/core/ai_chat_service.py`, update `send_message` to accept the new params (add `action_type: str = "open_chat"` and `focal_node_id: str | None = None` to the signature). For now, just accept them — prompt dispatch will come in a later task.

Also update `start_chat_session` to accept and store `tree_id`:

```python
async def start_chat_session(
    self,
    user_id: str,
    flow_type: str,
    db: AsyncSession,
    account_id: str | None = None,
    tree_id: str | None = None,
) -> dict:
    # ... existing code ...
    session = AIChatSession(
        # ... existing fields ...
        tree_id=uuid.UUID(tree_id) if tree_id else None,
    )
```

**Step 6: Run tests**

Run: `cd backend && python -m pytest tests/test_ai_chat_action_types.py -v`
Expected: PASS.

Run: `cd backend && python -m pytest tests/test_ai_chat.py -v`
Expected: Existing tests still PASS.

**Step 7: Commit**

```bash
git add backend/app/schemas/ai_chat.py backend/app/api/endpoints/ai_chat.py backend/app/core/ai_chat_service.py backend/tests/test_ai_chat_action_types.py
git commit -m "feat: add action_type and focal_node_id to AI chat message API"
```

---

### Task 6: Add Model Tier Routing to AI Chat Service

**Files:**
- Modify: `backend/app/core/ai_chat_service.py`

**Step 1: Write the failing test**

Add to `backend/tests/test_ai_chat_action_types.py`:

```python
def test_model_routing_uses_action_type():
    """Service selects model based on action_type."""
    from app.core.config import settings

    # Fast actions should use fast model
    fast_model = settings.get_model_for_action("quick_action")
    assert fast_model == settings.AI_MODEL_TIERS["fast"]

    # Standard actions should use standard model
    standard_model = settings.get_model_for_action("generate_branch")
    assert standard_model == settings.AI_MODEL_TIERS["standard"]
```

**Step 2: Run test — should pass (config already done in Task 2)**

Run: `cd backend && python -m pytest tests/test_ai_chat_action_types.py::test_model_routing_uses_action_type -v`
Expected: PASS (config was added in Task 2).

**Step 3: Update AI chat service to use model routing**

In `backend/app/core/ai_chat_service.py`, find where the AI model is selected (look for `settings.AI_MODEL` or `settings.AI_MODEL_ANTHROPIC` or `settings.AI_MODEL_GEMINI`). Replace the hardcoded model selection with:

```python
# Instead of:
# model = settings.AI_MODEL_ANTHROPIC  (or similar)

# Use:
model = settings.get_model_for_action(action_type)
```

This applies to both `send_message` and `generate_final_tree` methods. The `generate_final_tree` method should use `action_type="generate_full"`.

**Important:** The service may use either Anthropic or Gemini provider. The model tier config currently has Anthropic model names. If using Gemini provider, fall back to `settings.AI_MODEL_GEMINI`. Add a provider check:

```python
def _get_model(self, action_type: str) -> str:
    """Get the model name for this action, respecting provider."""
    if settings.AI_PROVIDER == "gemini":
        return settings.AI_MODEL_GEMINI
    return settings.get_model_for_action(action_type)
```

**Step 4: Run full AI chat tests**

Run: `cd backend && python -m pytest tests/test_ai_chat.py tests/test_ai_chat_action_types.py -v`
Expected: All PASS.

**Step 5: Commit**

```bash
git add backend/app/core/ai_chat_service.py backend/tests/test_ai_chat_action_types.py
git commit -m "feat: route AI model selection through action-type config"
```

---

## Phase 2: Core Frontend Infrastructure

### Task 7: Create Frontend Types for Editor AI

**Files:**
- Create: `frontend/src/types/editor-ai.ts`
- Modify: `frontend/src/types/index.ts`

**Step 1: Create the types file**

Create `frontend/src/types/editor-ai.ts`:

```typescript
export type AIActionType =
  | 'generate_full'
  | 'generate_branch'
  | 'modify_node'
  | 'add_steps'
  | 'quick_action'
  | 'open_chat'
  | 'variable_inference'

export interface AIDelta {
  action: 'add' | 'modify' | 'delete'
  target_node_id: string
  nodes: Record<string, unknown>[]
  explanation: string
}

export interface AISuggestion {
  id: string
  action_type: AIActionType
  target_node_id: string | null
  changes_json: {
    before?: Record<string, unknown>
    after?: Record<string, unknown>
    delta?: AIDelta
  }
  status: 'pending' | 'accepted' | 'dismissed'
  created_at: string
  resolved_at: string | null
}

export interface EditorAIChatMessage {
  role: 'user' | 'assistant'
  content: string
  timestamp: string
  action_type?: AIActionType
  delta?: AIDelta
  knowledge?: KnowledgeCitation[]
}

export interface KnowledgeCitation {
  title: string
  url: string
  excerpt: string
}

export interface EditorAISessionResponse {
  session_id: string
  status: 'active' | 'completed' | 'archived'
  flow_type: 'troubleshooting' | 'procedural'
  conversation_history: EditorAIChatMessage[]
  tree_id: string | null
  message_count: number
}

export interface ContextMenuAction {
  id: string
  label: string
  icon: string  // Lucide icon name
  action_type: AIActionType
  description?: string
}

export interface ContextMenuPosition {
  x: number
  y: number
}

/** Ghost node/step marker — mixed into TreeStructure or ProceduralStep */
export interface SuggestionMarker {
  _suggestion?: true
  _suggestion_id?: string  // links to AISuggestion.id
}
```

**Step 2: Export from types/index.ts**

Add to `frontend/src/types/index.ts`:

```typescript
export type {
  AIActionType,
  AIDelta,
  AISuggestion,
  EditorAIChatMessage,
  KnowledgeCitation,
  EditorAISessionResponse,
  ContextMenuAction,
  ContextMenuPosition,
  SuggestionMarker,
} from './editor-ai'
```

**Step 3: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 4: Commit**

```bash
git add frontend/src/types/editor-ai.ts frontend/src/types/index.ts
git commit -m "feat: add TypeScript types for editor-embedded AI"
```

---

### Task 8: Create ContextMenu Component

**Files:**
- Create: `frontend/src/components/common/ContextMenu.tsx`

**Step 1: Create the component**

Create `frontend/src/components/common/ContextMenu.tsx`:

```tsx
import { useEffect, useRef, useCallback } from 'react'
import { cn } from '@/lib/utils'
import type { ContextMenuPosition } from '@/types'

interface ContextMenuItem {
  id: string
  label: string
  icon?: React.ReactNode
  onClick: () => void
  variant?: 'default' | 'danger'
  separator?: boolean
}

interface ContextMenuProps {
  position: ContextMenuPosition
  items: ContextMenuItem[]
  onClose: () => void
}

export function ContextMenu({ position, items, onClose }: ContextMenuProps) {
  const menuRef = useRef<HTMLDivElement>(null)

  const handleClickOutside = useCallback(
    (e: MouseEvent) => {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
        onClose()
      }
    },
    [onClose]
  )

  const handleKeyDown = useCallback(
    (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose()
    },
    [onClose]
  )

  useEffect(() => {
    document.addEventListener('mousedown', handleClickOutside)
    document.addEventListener('keydown', handleKeyDown)
    return () => {
      document.removeEventListener('mousedown', handleClickOutside)
      document.removeEventListener('keydown', handleKeyDown)
    }
  }, [handleClickOutside, handleKeyDown])

  // Adjust position to stay within viewport
  const adjustedStyle = (() => {
    const style: React.CSSProperties = {
      position: 'fixed',
      zIndex: 100,
      left: position.x,
      top: position.y,
    }
    if (menuRef.current) {
      const rect = menuRef.current.getBoundingClientRect()
      if (position.x + rect.width > window.innerWidth) {
        style.left = position.x - rect.width
      }
      if (position.y + rect.height > window.innerHeight) {
        style.top = position.y - rect.height
      }
    }
    return style
  })()

  return (
    <div
      ref={menuRef}
      style={adjustedStyle}
      className="min-w-[200px] rounded-xl border border-border bg-card p-1 shadow-lg backdrop-blur-md"
    >
      {items.map((item) => (
        <div key={item.id}>
          {item.separator && (
            <div className="my-1 border-t border-border" />
          )}
          <button
            onClick={() => {
              item.onClick()
              onClose()
            }}
            className={cn(
              'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors',
              item.variant === 'danger'
                ? 'text-rose-400 hover:bg-rose-500/10'
                : 'text-foreground hover:bg-[rgba(255,255,255,0.06)]'
            )}
          >
            {item.icon && (
              <span className="flex h-4 w-4 items-center justify-center text-muted-foreground">
                {item.icon}
              </span>
            )}
            {item.label}
          </button>
        </div>
      ))}
    </div>
  )
}
```

**Step 2: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 3: Commit**

```bash
git add frontend/src/components/common/ContextMenu.tsx
git commit -m "feat: add shared ContextMenu component"
```

---

### Task 9: Create EditorAIPanel Component (Shell)

**Files:**
- Create: `frontend/src/components/editor-ai/EditorAIPanel.tsx`
- Create: `frontend/src/components/editor-ai/ChatTab.tsx`
- Create: `frontend/src/components/editor-ai/SuggestionsTab.tsx`
- Create: `frontend/src/components/editor-ai/NodeSummary.tsx`

**Step 1: Create NodeSummary component**

Create `frontend/src/components/editor-ai/NodeSummary.tsx`:

```tsx
import { HelpCircle, Zap, CheckCircle, FileText, Layout } from 'lucide-react'
import type { TreeStructure } from '@/types'

interface NodeSummaryProps {
  node?: TreeStructure | null
  flowName?: string
  flowType?: 'troubleshooting' | 'procedural' | 'maintenance'
  nodeCount?: number
}

const NODE_ICONS = {
  decision: HelpCircle,
  action: Zap,
  solution: CheckCircle,
}

const NODE_COLORS = {
  decision: 'text-blue-400',
  action: 'text-yellow-400',
  solution: 'text-green-400',
}

export function NodeSummary({ node, flowName, flowType, nodeCount }: NodeSummaryProps) {
  if (!node) {
    // Flow summary when no node selected
    return (
      <div className="border-b border-border px-3 py-2.5">
        <div className="flex items-center gap-2">
          <Layout className="h-3.5 w-3.5 text-muted-foreground" />
          <span className="text-xs font-medium text-foreground truncate">
            {flowName || 'Untitled Flow'}
          </span>
        </div>
        <div className="mt-1 flex items-center gap-3 text-[0.625rem] text-muted-foreground font-label uppercase tracking-wider">
          <span>{flowType || 'flow'}</span>
          {nodeCount !== undefined && <span>{nodeCount} nodes</span>}
        </div>
      </div>
    )
  }

  const Icon = NODE_ICONS[node.type as keyof typeof NODE_ICONS] || FileText
  const colorClass = NODE_COLORS[node.type as keyof typeof NODE_COLORS] || 'text-muted-foreground'

  return (
    <div className="border-b border-border px-3 py-2.5">
      <div className="flex items-center gap-2">
        <Icon className={`h-3.5 w-3.5 ${colorClass}`} />
        <span className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground">
          {node.type}
        </span>
      </div>
      <p className="mt-1 text-sm font-medium text-foreground truncate">
        {node.question || node.title || node.id}
      </p>
      {node.description && (
        <p className="mt-0.5 text-xs text-muted-foreground truncate">
          {node.description}
        </p>
      )}
    </div>
  )
}
```

**Step 2: Create ChatTab component**

Create `frontend/src/components/editor-ai/ChatTab.tsx`:

```tsx
import { useRef, useEffect } from 'react'
import { Send, Sparkles } from 'lucide-react'
import type { EditorAIChatMessage } from '@/types'

interface ChatTabProps {
  messages: EditorAIChatMessage[]
  input: string
  onInputChange: (value: string) => void
  onSend: () => void
  isLoading: boolean
}

export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: ChatTabProps) {
  const messagesEndRef = useRef<HTMLDivElement>(null)

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

  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      if (input.trim() && !isLoading) onSend()
    }
  }

  return (
    <div className="flex flex-1 flex-col overflow-hidden">
      {/* Messages */}
      <div className="flex-1 overflow-y-auto px-3 py-3 space-y-3">
        {messages.length === 0 && (
          <div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm">
            <Sparkles className="h-8 w-8 mb-2 opacity-40" />
            <p>Ask me to help build your flow</p>
          </div>
        )}
        {messages.map((msg, i) => (
          <div
            key={i}
            className={`text-sm rounded-lg px-3 py-2 ${
              msg.role === 'user'
                ? 'ml-6 bg-primary/15 text-foreground'
                : 'mr-2 bg-[rgba(255,255,255,0.04)] border border-border text-foreground'
            }`}
          >
            <p className="whitespace-pre-wrap">{msg.content}</p>
          </div>
        ))}
        {isLoading && (
          <div className="mr-2 bg-[rgba(255,255,255,0.04)] border border-border rounded-lg px-3 py-2">
            <div className="flex gap-1">
              <div className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce" />
              <div className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce [animation-delay:0.15s]" />
              <div className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce [animation-delay:0.3s]" />
            </div>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      {/* Input */}
      <div className="shrink-0 border-t border-border p-3">
        <div className="flex items-end gap-2">
          <textarea
            value={input}
            onChange={(e) => onInputChange(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="Ask AI to help..."
            rows={1}
            className="flex-1 resize-none rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none"
          />
          <button
            onClick={onSend}
            disabled={!input.trim() || isLoading}
            className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-brand text-[#101114] transition-all hover:opacity-90 active:scale-[0.97] disabled:opacity-40"
          >
            <Send className="h-4 w-4" />
          </button>
        </div>
      </div>
    </div>
  )
}
```

**Step 3: Create SuggestionsTab component**

Create `frontend/src/components/editor-ai/SuggestionsTab.tsx`:

```tsx
import { Check, X, Clock } from 'lucide-react'
import type { AISuggestion } from '@/types'

interface SuggestionsTabProps {
  suggestions: AISuggestion[]
}

const STATUS_CONFIG = {
  accepted: { icon: Check, color: 'text-emerald-400', label: 'Accepted' },
  dismissed: { icon: X, color: 'text-rose-400', label: 'Dismissed' },
  pending: { icon: Clock, color: 'text-amber-400', label: 'Pending' },
}

export function SuggestionsTab({ suggestions }: SuggestionsTabProps) {
  if (suggestions.length === 0) {
    return (
      <div className="flex flex-1 items-center justify-center text-sm text-muted-foreground p-6">
        No AI suggestions yet
      </div>
    )
  }

  return (
    <div className="flex-1 overflow-y-auto px-3 py-3 space-y-2">
      {suggestions.map((s) => {
        const config = STATUS_CONFIG[s.status]
        const StatusIcon = config.icon
        return (
          <div
            key={s.id}
            className="rounded-lg border border-border bg-[rgba(255,255,255,0.02)] px-3 py-2"
          >
            <div className="flex items-center justify-between">
              <span className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground">
                {s.action_type.replace(/_/g, ' ')}
              </span>
              <span className={`flex items-center gap-1 text-xs ${config.color}`}>
                <StatusIcon className="h-3 w-3" />
                {config.label}
              </span>
            </div>
            {s.target_node_id && (
              <p className="mt-1 text-xs text-muted-foreground truncate">
                Node: {s.target_node_id}
              </p>
            )}
            <p className="mt-0.5 text-[0.625rem] text-[#5a6170]">
              {new Date(s.created_at).toLocaleDateString()}
            </p>
          </div>
        )
      })}
    </div>
  )
}
```

**Step 4: Create the main EditorAIPanel component**

Create `frontend/src/components/editor-ai/EditorAIPanel.tsx`:

```tsx
import { useState } from 'react'
import { X, Sparkles } from 'lucide-react'
import { cn } from '@/lib/utils'
import { NodeSummary } from './NodeSummary'
import { ChatTab } from './ChatTab'
import { SuggestionsTab } from './SuggestionsTab'
import type { TreeStructure, EditorAIChatMessage, AISuggestion } from '@/types'

interface EditorAIPanelProps {
  isOpen: boolean
  onClose: () => void
  // Context
  focalNode?: TreeStructure | null
  flowName?: string
  flowType?: 'troubleshooting' | 'procedural' | 'maintenance'
  nodeCount?: number
  // Chat state
  messages: EditorAIChatMessage[]
  input: string
  onInputChange: (value: string) => void
  onSend: () => void
  isLoading: boolean
  // Suggestions
  suggestions: AISuggestion[]
}

type Tab = 'chat' | 'suggestions'

export function EditorAIPanel({
  isOpen,
  onClose,
  focalNode,
  flowName,
  flowType,
  nodeCount,
  messages,
  input,
  onInputChange,
  onSend,
  isLoading,
  suggestions,
}: EditorAIPanelProps) {
  const [activeTab, setActiveTab] = useState<Tab>('chat')

  if (!isOpen) return null

  const pendingCount = suggestions.filter((s) => s.status === 'pending').length

  return (
    <div className="flex h-full w-[320px] shrink-0 flex-col border-l border-border bg-[rgba(24,26,31,0.55)] backdrop-blur-[var(--glass-blur)]">
      {/* Header */}
      <div className="flex items-center justify-between border-b border-border px-3 py-2.5 shrink-0">
        <div className="flex items-center gap-2">
          <Sparkles className="h-4 w-4 text-primary" />
          <span className="text-sm font-medium text-foreground">AI Assist</span>
        </div>
        <button
          onClick={onClose}
          className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:bg-[rgba(255,255,255,0.06)] hover:text-foreground transition-colors"
        >
          <X className="h-3.5 w-3.5" />
        </button>
      </div>

      {/* Node/Flow Summary */}
      <NodeSummary
        node={focalNode}
        flowName={flowName}
        flowType={flowType}
        nodeCount={nodeCount}
      />

      {/* Tabs */}
      <div className="flex border-b border-border shrink-0">
        <button
          onClick={() => setActiveTab('chat')}
          className={cn(
            'flex-1 px-3 py-2 text-xs font-medium transition-colors',
            activeTab === 'chat'
              ? 'border-b-2 border-primary text-foreground'
              : 'text-muted-foreground hover:text-foreground'
          )}
        >
          Chat
        </button>
        <button
          onClick={() => setActiveTab('suggestions')}
          className={cn(
            'flex-1 px-3 py-2 text-xs font-medium transition-colors relative',
            activeTab === 'suggestions'
              ? 'border-b-2 border-primary text-foreground'
              : 'text-muted-foreground hover:text-foreground'
          )}
        >
          Suggestions
          {pendingCount > 0 && (
            <span className="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary/20 px-1 text-[0.5625rem] text-primary">
              {pendingCount}
            </span>
          )}
        </button>
      </div>

      {/* Tab Content */}
      {activeTab === 'chat' ? (
        <ChatTab
          messages={messages}
          input={input}
          onInputChange={onInputChange}
          onSend={onSend}
          isLoading={isLoading}
        />
      ) : (
        <SuggestionsTab suggestions={suggestions} />
      )}
    </div>
  )
}
```

**Step 5: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 6: Commit**

```bash
git add frontend/src/components/editor-ai/
git commit -m "feat: add EditorAIPanel component with Chat and Suggestions tabs"
```

---

### Task 10: Create useEditorAI Hook

**Files:**
- Create: `frontend/src/hooks/useEditorAI.ts`
- Create: `frontend/src/api/editorAI.ts`

**Step 1: Create the API client**

Create `frontend/src/api/editorAI.ts`:

```typescript
import { apiClient } from './client'
import type { AIActionType } from '@/types'

export interface SendMessageParams {
  sessionId: string
  content: string
  actionType?: AIActionType
  focalNodeId?: string | null
}

export const editorAIApi = {
  startSession: async (flowType: 'troubleshooting' | 'procedural', treeId?: string) => {
    const { data } = await apiClient.post('/ai/chat/sessions', {
      flow_type: flowType,
      tree_id: treeId,
    })
    return data
  },

  sendMessage: async ({ sessionId, content, actionType, focalNodeId }: SendMessageParams) => {
    const { data } = await apiClient.post(`/ai/chat/sessions/${sessionId}/messages`, {
      content,
      action_type: actionType || 'open_chat',
      focal_node_id: focalNodeId,
    })
    return data
  },

  getSession: async (sessionId: string) => {
    const { data } = await apiClient.get(`/ai/chat/sessions/${sessionId}`)
    return data
  },

  generateFull: async (sessionId: string) => {
    const { data } = await apiClient.post(`/ai/chat/sessions/${sessionId}/generate`)
    return data
  },

  abandonSession: async (sessionId: string) => {
    await apiClient.delete(`/ai/chat/sessions/${sessionId}`)
  },
}
```

**Step 2: Create the hook**

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

```typescript
import { useState, useCallback, useRef } from 'react'
import { editorAIApi } from '@/api/editorAI'
import type {
  AIActionType,
  EditorAIChatMessage,
  AISuggestion,
  ContextMenuPosition,
} from '@/types'

interface UseEditorAIOptions {
  flowType: 'troubleshooting' | 'procedural'
  treeId?: string | null
}

export function useEditorAI({ flowType, treeId }: UseEditorAIOptions) {
  // Panel state
  const [isOpen, setIsOpen] = useState(false)
  const [focalNodeId, setFocalNodeId] = useState<string | null>(null)

  // Context menu
  const [contextMenu, setContextMenu] = useState<{
    position: ContextMenuPosition
    nodeId: string
  } | null>(null)

  // Chat state
  const [sessionId, setSessionId] = useState<string | null>(null)
  const [messages, setMessages] = useState<EditorAIChatMessage[]>([])
  const [input, setInput] = useState('')
  const [isLoading, setIsLoading] = useState(false)

  // Suggestions
  const [suggestions, setSuggestions] = useState<AISuggestion[]>([])

  const pendingActionRef = useRef<AIActionType>('open_chat')

  const ensureSession = useCallback(async () => {
    if (sessionId) return sessionId
    try {
      const result = await editorAIApi.startSession(flowType, treeId || undefined)
      setSessionId(result.session_id)
      if (result.greeting) {
        setMessages((prev) => [
          ...prev,
          {
            role: 'assistant' as const,
            content: result.greeting,
            timestamp: new Date().toISOString(),
          },
        ])
      }
      return result.session_id
    } catch {
      return null
    }
  }, [sessionId, flowType, treeId])

  const openPanel = useCallback(
    (nodeId?: string, actionType?: AIActionType) => {
      setIsOpen(true)
      if (nodeId) setFocalNodeId(nodeId)
      if (actionType) pendingActionRef.current = actionType
    },
    []
  )

  const closePanel = useCallback(() => {
    setIsOpen(false)
    setContextMenu(null)
  }, [])

  const openContextMenu = useCallback(
    (e: React.MouseEvent, nodeId: string) => {
      e.preventDefault()
      e.stopPropagation()
      setContextMenu({
        position: { x: e.clientX, y: e.clientY },
        nodeId,
      })
    },
    []
  )

  const closeContextMenu = useCallback(() => {
    setContextMenu(null)
  }, [])

  const sendMessage = useCallback(async () => {
    if (!input.trim() || isLoading) return

    const userMessage: EditorAIChatMessage = {
      role: 'user',
      content: input,
      timestamp: new Date().toISOString(),
      action_type: pendingActionRef.current,
    }
    setMessages((prev) => [...prev, userMessage])
    setInput('')
    setIsLoading(true)

    try {
      const sid = await ensureSession()
      if (!sid) return

      const result = await editorAIApi.sendMessage({
        sessionId: sid,
        content: input,
        actionType: pendingActionRef.current,
        focalNodeId: focalNodeId,
      })

      const assistantMessage: EditorAIChatMessage = {
        role: 'assistant',
        content: result.content,
        timestamp: new Date().toISOString(),
      }
      setMessages((prev) => [...prev, assistantMessage])

      // TODO: Parse delta from response and create ghost nodes
    } catch {
      setMessages((prev) => [
        ...prev,
        {
          role: 'assistant',
          content: 'Sorry, something went wrong. Please try again.',
          timestamp: new Date().toISOString(),
        },
      ])
    } finally {
      setIsLoading(false)
      pendingActionRef.current = 'open_chat'
    }
  }, [input, isLoading, ensureSession, focalNodeId])

  const triggerAction = useCallback(
    (nodeId: string, actionType: AIActionType, prompt: string) => {
      setFocalNodeId(nodeId)
      pendingActionRef.current = actionType
      setInput(prompt)
      setIsOpen(true)
      // Auto-send after a tick so panel is visible
      setTimeout(() => {
        sendMessage()
      }, 100)
    },
    [sendMessage]
  )

  return {
    // Panel
    isOpen,
    openPanel,
    closePanel,
    focalNodeId,
    setFocalNodeId,

    // Context menu
    contextMenu,
    openContextMenu,
    closeContextMenu,

    // Chat
    messages,
    input,
    setInput,
    sendMessage,
    isLoading,

    // Suggestions
    suggestions,
    setSuggestions,

    // Actions
    triggerAction,
  }
}
```

**Step 3: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 4: Commit**

```bash
git add frontend/src/hooks/useEditorAI.ts frontend/src/api/editorAI.ts
git commit -m "feat: add useEditorAI hook and editorAI API client"
```

---

## Phase 3: Tree Editor Integration

### Task 11: Add AI Panel to Tree Editor Layout

**Files:**
- Modify: `frontend/src/pages/TreeEditorPage.tsx`

**Step 1: Read the current file to understand its full structure**

Read `frontend/src/pages/TreeEditorPage.tsx` before modifying.

**Step 2: Add imports and hook**

At the top of `TreeEditorPage.tsx`, add:

```typescript
import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel'
import { ContextMenu } from '@/components/common/ContextMenu'
import { useEditorAI } from '@/hooks/useEditorAI'
import { Sparkles } from 'lucide-react'  // may already be imported
```

Inside the component, after existing hooks:

```typescript
const editorAI = useEditorAI({
  flowType: 'troubleshooting',
  treeId: id,
})
```

**Step 3: Add AI Assist toolbar button**

In the toolbar section (around line 706, after the Export button), add:

```tsx
<button
  onClick={() => editorAI.isOpen ? editorAI.closePanel() : editorAI.openPanel()}
  className={cn(
    'flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors',
    editorAI.isOpen
      ? 'bg-primary/10 text-primary'
      : 'text-muted-foreground hover:bg-[rgba(255,255,255,0.06)] hover:text-foreground'
  )}
  title="Toggle AI Assist"
>
  <Sparkles className="h-4 w-4" />
  <span className="hidden lg:inline">AI Assist</span>
</button>
```

**Step 4: Handle single-panel rule**

When AI panel opens, close the node editor. When AI panel closes, reopen node editor if a node was selected:

```typescript
// Track the node that was being edited when AI panel opened
const previousEditingNodeRef = useRef<string | null>(null)

// When AI panel opens, remember editing node and close editor
useEffect(() => {
  if (editorAI.isOpen && editingNodeId) {
    previousEditingNodeRef.current = editingNodeId
    setEditingNodeId(null)
  }
}, [editorAI.isOpen])

// When AI panel closes, restore previous editing node
const handleAIPanelClose = () => {
  editorAI.closePanel()
  if (previousEditingNodeRef.current) {
    setEditingNodeId(previousEditingNodeRef.current)
    previousEditingNodeRef.current = null
  }
}
```

**Step 5: Add panel to layout**

After the existing editor layout area (around line 782), add the AI panel alongside:

```tsx
<div className="flex flex-1 overflow-hidden">
  {/* Existing editor layout takes flex-1 */}
  <div className="flex-1 overflow-hidden">
    <TreeEditorLayout
      isMobile={isMobile}
      isMetadataOpen={isMetadataOpen}
      onCloseMetadata={() => setIsMetadataOpen(false)}
      editingNodeId={editorAI.isOpen ? null : editingNodeId}
      onNodeSelect={handleNodeSelect}
      onSelectAnswerType={handleSelectAnswerType}
      onNodeContextMenu={editorAI.openContextMenu}
    />
  </div>

  {/* AI Panel */}
  <EditorAIPanel
    isOpen={editorAI.isOpen}
    onClose={handleAIPanelClose}
    focalNode={editorAI.focalNodeId ? findNode(editorAI.focalNodeId, treeStructure) : null}
    flowName={name}
    flowType="troubleshooting"
    nodeCount={treeStructure ? getAllNodeIds(treeStructure).length : 0}
    messages={editorAI.messages}
    input={editorAI.input}
    onInputChange={editorAI.setInput}
    onSend={editorAI.sendMessage}
    isLoading={editorAI.isLoading}
    suggestions={editorAI.suggestions}
  />
</div>
```

**Note:** The `onNodeContextMenu` prop will need to be threaded through `TreeEditorLayout` → `TreeCanvas` → `TreeCanvasNode`. This happens in Task 12.

**Step 6: Add context menu rendering**

At the bottom of the component, before the closing fragment:

```tsx
{editorAI.contextMenu && (
  <ContextMenu
    position={editorAI.contextMenu.position}
    items={[
      {
        id: 'generate-branch',
        label: 'Generate Branch',
        icon: <Sparkles className="h-4 w-4" />,
        onClick: () => editorAI.triggerAction(
          editorAI.contextMenu!.nodeId,
          'generate_branch',
          `Generate a branch from node "${editorAI.contextMenu!.nodeId}"`
        ),
      },
      {
        id: 'explain',
        label: 'Explain Node',
        icon: <HelpCircle className="h-4 w-4" />,
        onClick: () => editorAI.triggerAction(
          editorAI.contextMenu!.nodeId,
          'quick_action',
          `Explain what node "${editorAI.contextMenu!.nodeId}" does`
        ),
      },
      { id: 'sep1', label: '', onClick: () => {}, separator: true },
      {
        id: 'delete',
        label: 'Delete Node',
        variant: 'danger' as const,
        onClick: () => {
          // Use existing delete logic
          deleteNode(editorAI.contextMenu!.nodeId)
        },
      },
    ]}
    onClose={editorAI.closeContextMenu}
  />
)}
```

**Step 7: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds (some props like `onNodeContextMenu` may need stubbing if TreeEditorLayout doesn't accept it yet — add it as an optional prop first).

**Step 8: Commit**

```bash
git add frontend/src/pages/TreeEditorPage.tsx
git commit -m "feat: integrate AI panel and context menu in tree editor"
```

---

### Task 12: Thread Context Menu Through Tree Canvas

**Files:**
- Modify: `frontend/src/components/tree-editor/TreeEditorLayout.tsx` (or wherever TreeCanvas is rendered)
- Modify: `frontend/src/components/tree-editor/TreeCanvas.tsx`
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`

**Step 1: Read each file to understand prop threading**

Read all three files before modifying.

**Step 2: Add onNodeContextMenu prop through the chain**

In `TreeCanvasNode.tsx`, add `onContextMenu` handler to the card's root div:

```tsx
// In TreeCanvasNode props interface, add:
onContextMenu?: (e: React.MouseEvent, nodeId: string) => void

// On the card container div, add:
onContextMenu={(e) => onContextMenu?.(e, node.id)}
```

Thread this prop up through `TreeCanvas.tsx` and `TreeEditorLayout.tsx` from the `TreeEditorPage`.

**Step 3: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 4: Commit**

```bash
git add frontend/src/components/tree-editor/
git commit -m "feat: thread context menu handler through tree canvas components"
```

---

### Task 13: Add Ghost Node Rendering to TreeCanvasNode

**Files:**
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`

**Step 1: Read the current TreeCanvasNode styling**

Read `frontend/src/components/tree-editor/TreeCanvasNode.tsx`.

**Step 2: Add ghost node styling**

In `TreeCanvasNode.tsx`, detect the `_suggestion` flag and apply ghost styling:

```tsx
// After determining node type config (around line 90):
const isGhost = !!(node as Record<string, unknown>)._suggestion

// On the card container, modify className to conditionally add ghost styles:
className={cn(
  'group relative rounded-xl border bg-card p-3 transition-all',
  config.borderClass,
  isSelected && 'ring-2 ring-primary/40',
  hasErrors && 'ring-2 ring-rose-500/40',
  hasWarnings && !hasErrors && 'ring-2 ring-amber-500/40',
  // Ghost node styling
  isGhost && 'border-dashed border-primary/40 opacity-60',
)}
```

**Step 3: Add accept/dismiss overlay for ghost nodes**

Inside the card, at the bottom, add:

```tsx
{isGhost && (
  <div className="mt-2 flex gap-2 border-t border-border/50 pt-2">
    <button
      onClick={(e) => {
        e.stopPropagation()
        onAcceptSuggestion?.(node.id)
      }}
      className="flex-1 rounded-md bg-emerald-500/20 px-2 py-1 text-xs text-emerald-400 hover:bg-emerald-500/30 transition-colors"
    >
      Accept
    </button>
    <button
      onClick={(e) => {
        e.stopPropagation()
        onDismissSuggestion?.(node.id)
      }}
      className="flex-1 rounded-md bg-rose-500/20 px-2 py-1 text-xs text-rose-400 hover:bg-rose-500/30 transition-colors"
    >
      Dismiss
    </button>
  </div>
)}
```

Add `onAcceptSuggestion` and `onDismissSuggestion` to the component's props interface and thread them from the parent.

**Step 4: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 5: Commit**

```bash
git add frontend/src/components/tree-editor/TreeCanvasNode.tsx
git commit -m "feat: add ghost node rendering with accept/dismiss controls"
```

---

## Phase 4: Procedural Editor Integration

### Task 14: Add AI Panel to Procedural Editor Layout

**Files:**
- Modify: `frontend/src/pages/ProceduralEditorPage.tsx`

**Step 1: Read the current file**

Read `frontend/src/pages/ProceduralEditorPage.tsx` to understand its layout.

**Step 2: Add imports and hook**

```typescript
import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel'
import { ContextMenu } from '@/components/common/ContextMenu'
import { useEditorAI } from '@/hooks/useEditorAI'
import { Sparkles } from 'lucide-react'
```

```typescript
const editorAI = useEditorAI({
  flowType: isMaintenance ? 'procedural' : 'procedural',  // maintenance uses procedural
  treeId: treeId,
})
```

**Step 3: Wrap the step list area in a flex container**

The procedural editor has no existing side panel. Wrap the main content area in a flex container so the AI panel can sit alongside:

```tsx
<div className="flex flex-1 overflow-hidden">
  <div className="flex-1 overflow-y-auto">
    {/* Existing step list, intake form, etc. */}
  </div>

  <EditorAIPanel
    isOpen={editorAI.isOpen}
    onClose={editorAI.closePanel}
    focalNode={null}  // TODO: adapt for steps
    flowName={name}
    flowType={isMaintenance ? 'maintenance' : 'procedural'}
    nodeCount={steps.length}
    messages={editorAI.messages}
    input={editorAI.input}
    onInputChange={editorAI.setInput}
    onSend={editorAI.sendMessage}
    isLoading={editorAI.isLoading}
    suggestions={editorAI.suggestions}
  />
</div>
```

**Step 4: Add AI Assist toolbar button**

Add the same toolbar toggle button as the tree editor (in the procedural editor's toolbar area).

**Step 5: Add context menu for steps**

Add `onContextMenu` handler to step items in `StepList` that opens the context menu with procedural-specific actions:

```tsx
{editorAI.contextMenu && (
  <ContextMenu
    position={editorAI.contextMenu.position}
    items={[
      {
        id: 'generate-steps',
        label: 'Generate Steps After',
        icon: <Sparkles className="h-4 w-4" />,
        onClick: () => editorAI.triggerAction(
          editorAI.contextMenu!.nodeId,
          'add_steps',
          `Generate steps after step "${editorAI.contextMenu!.nodeId}"`
        ),
      },
      {
        id: 'expand-step',
        label: 'Expand Step',
        icon: <Layers className="h-4 w-4" />,
        onClick: () => editorAI.triggerAction(
          editorAI.contextMenu!.nodeId,
          'quick_action',
          `Expand step "${editorAI.contextMenu!.nodeId}" into detailed substeps`
        ),
      },
    ]}
    onClose={editorAI.closeContextMenu}
  />
)}
```

**Step 6: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 7: Commit**

```bash
git add frontend/src/pages/ProceduralEditorPage.tsx
git commit -m "feat: integrate AI panel and context menu in procedural editor"
```

---

### Task 15: Add Ghost Step Rendering to StepList

**Files:**
- Modify: `frontend/src/components/procedural-editor/StepList.tsx`

**Step 1: Read the current StepList component**

Read `frontend/src/components/procedural-editor/StepList.tsx`.

**Step 2: Add ghost step styling**

For each step item in the list, detect `_suggestion` and apply ghost styling:

```tsx
const isGhost = !!(step as Record<string, unknown>)._suggestion

// On the step container:
className={cn(
  'rounded-xl border bg-card p-3',
  // Ghost step styling
  isGhost && 'border-l-2 border-dashed border-primary/40 opacity-60',
)}
```

**Step 3: Add accept/dismiss controls for ghost steps**

```tsx
{isGhost && (
  <div className="mt-2 flex gap-2">
    <button
      onClick={() => onAcceptSuggestion?.(step.id)}
      className="flex-1 rounded-md bg-emerald-500/20 px-2 py-1 text-xs text-emerald-400 hover:bg-emerald-500/30"
    >
      Accept
    </button>
    <button
      onClick={() => onDismissSuggestion?.(step.id)}
      className="flex-1 rounded-md bg-rose-500/20 px-2 py-1 text-xs text-rose-400 hover:bg-rose-500/30"
    >
      Dismiss
    </button>
  </div>
)}
```

**Step 4: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 5: Commit**

```bash
git add frontend/src/components/procedural-editor/StepList.tsx
git commit -m "feat: add ghost step rendering with accept/dismiss in step list"
```

---

## Phase 5: AI-Assisted Create + Prompt Dialog

### Task 16: Create AI Prompt Dialog Modal

**Files:**
- Create: `frontend/src/components/editor-ai/AIPromptDialog.tsx`

**Step 1: Create the component**

Create `frontend/src/components/editor-ai/AIPromptDialog.tsx`:

```tsx
import { useState } from 'react'
import { Sparkles, Loader2, AlertCircle } from 'lucide-react'

interface AIPromptDialogProps {
  isOpen: boolean
  onClose: () => void
  onGenerate: (prompt: string) => Promise<void>
  flowType: 'troubleshooting' | 'procedural' | 'maintenance'
}

const FLOW_TYPE_LABELS = {
  troubleshooting: 'Troubleshooting Flow',
  procedural: 'Project Flow',
  maintenance: 'Maintenance Flow',
}

export function AIPromptDialog({
  isOpen,
  onClose,
  onGenerate,
  flowType,
}: AIPromptDialogProps) {
  const [prompt, setPrompt] = useState('')
  const [isGenerating, setIsGenerating] = useState(false)
  const [error, setError] = useState<string | null>(null)

  if (!isOpen) return null

  const handleGenerate = async () => {
    if (!prompt.trim()) return
    setIsGenerating(true)
    setError(null)
    try {
      await onGenerate(prompt)
      setPrompt('')
      onClose()
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Generation failed. Please try again.')
    } finally {
      setIsGenerating(false)
    }
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center">
      {/* Backdrop */}
      <div
        className="absolute inset-0 bg-black/60 backdrop-blur-sm"
        onClick={() => !isGenerating && onClose()}
      />

      {/* Dialog */}
      <div className="relative w-full max-w-lg rounded-2xl border border-border bg-card p-6 shadow-xl">
        <div className="flex items-center gap-2 mb-4">
          <Sparkles className="h-5 w-5 text-primary" />
          <h2 className="text-lg font-heading font-semibold text-foreground">
            AI-Assisted {FLOW_TYPE_LABELS[flowType]}
          </h2>
        </div>

        <p className="text-sm text-muted-foreground mb-4">
          Describe what you want to build and AI will generate a starting structure for you.
        </p>

        <textarea
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder={`Example: "A flow for troubleshooting VPN connectivity issues when users can't connect to the corporate network"`}
          rows={4}
          disabled={isGenerating}
          className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none disabled:opacity-50"
          autoFocus
        />

        {error && (
          <div className="mt-3 flex items-start gap-2 rounded-lg bg-rose-500/10 border border-rose-500/20 px-3 py-2">
            <AlertCircle className="h-4 w-4 text-rose-400 mt-0.5 shrink-0" />
            <p className="text-sm text-rose-400">{error}</p>
          </div>
        )}

        <div className="mt-4 flex justify-end gap-3">
          <button
            onClick={onClose}
            disabled={isGenerating}
            className="rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2 text-sm text-foreground hover:border-[rgba(255,255,255,0.12)] transition-colors disabled:opacity-50"
          >
            Cancel
          </button>
          <button
            onClick={handleGenerate}
            disabled={!prompt.trim() || isGenerating}
            className="flex items-center gap-2 rounded-[10px] bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-50"
          >
            {isGenerating ? (
              <>
                <Loader2 className="h-4 w-4 animate-spin" />
                Generating...
              </>
            ) : (
              <>
                <Sparkles className="h-4 w-4" />
                Generate
              </>
            )}
          </button>
        </div>
      </div>
    </div>
  )
}
```

**Step 2: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 3: Commit**

```bash
git add frontend/src/components/editor-ai/AIPromptDialog.tsx
git commit -m "feat: add AI prompt dialog for AI-assisted flow creation"
```

---

### Task 17: Update CreateFlowDropdown

**Files:**
- Modify: `frontend/src/components/common/CreateFlowDropdown.tsx`

**Step 1: Read the current file**

Read `frontend/src/components/common/CreateFlowDropdown.tsx`.

**Step 2: Replace `/ai/chat` navigation with prompt dialog**

Remove the `navigate('/ai/chat')` and `navigate('/ai/chat?type=procedural')` calls. Instead:

1. Add state for the prompt dialog:
```tsx
const [aiPromptOpen, setAiPromptOpen] = useState(false)
const [aiPromptFlowType, setAiPromptFlowType] = useState<'troubleshooting' | 'procedural' | 'maintenance'>('troubleshooting')
```

2. Replace AI builder menu items to open the dialog:
```tsx
// Instead of navigate('/ai/chat')
onClick={() => {
  setAiPromptFlowType('troubleshooting')
  setAiPromptOpen(true)
}}
```

3. Add the `AIPromptDialog` at the end of the component:
```tsx
<AIPromptDialog
  isOpen={aiPromptOpen}
  onClose={() => setAiPromptOpen(false)}
  flowType={aiPromptFlowType}
  onGenerate={async (prompt) => {
    // 1. Start session with generate_full action
    // 2. Send the prompt
    // 3. Generate the tree
    // 4. Import to editor
    // 5. Navigate to editor with AI panel open flag
    const session = await editorAIApi.startSession(aiPromptFlowType)
    await editorAIApi.sendMessage({
      sessionId: session.session_id,
      content: prompt,
      actionType: 'generate_full',
    })
    const result = await editorAIApi.generateFull(session.session_id)
    // Import the generated tree
    const imported = await apiClient.post(`/ai/chat/sessions/${session.session_id}/import`, {})
    // Navigate to editor with AI panel open
    const path = aiPromptFlowType === 'troubleshooting'
      ? `/trees/${imported.data.tree_id}/edit`
      : `/flows/${imported.data.tree_id}/edit`
    navigate(path, { state: { aiPanelOpen: true, sessionId: session.session_id } })
  }}
/>
```

**Step 3: Verify build**

Run: `cd frontend && npm run build`
Expected: Build succeeds.

**Step 4: Commit**

```bash
git add frontend/src/components/common/CreateFlowDropdown.tsx
git commit -m "feat: replace /ai/chat navigation with AI prompt dialog in create dropdown"
```

---

## Phase 6: Remove Old Standalone Flow Assist

### Task 18: Remove Old AI Chat Components

**Files:**
- Delete: `frontend/src/pages/AIChatBuilderPage.tsx`
- Delete: `frontend/src/store/aiChatStore.ts`
- Delete: `frontend/src/components/ai-chat/` (entire directory)
- Modify: `frontend/src/router.tsx` (remove `/ai/chat` route)
- Delete: Any `AIFlowBuilderModal` component (search for it)

**Step 1: Search for all references to removed files**

Search for imports of `AIChatBuilderPage`, `aiChatStore`, `ai-chat/` components, and `AIFlowBuilderModal` across the frontend.

**Step 2: Remove the route**

In `frontend/src/router.tsx`, remove:
- The lazy import: `const AIChatBuilderPage = lazy(() => import('@/pages/AIChatBuilderPage'))`
- The route entry: `{ path: 'ai/chat', element: ... }`

**Step 3: Remove any sidebar/nav references to `/ai/chat`**

Search for `/ai/chat` in navigation components and remove links.

**Step 4: Delete the files**

```bash
rm frontend/src/pages/AIChatBuilderPage.tsx
rm frontend/src/store/aiChatStore.ts
rm -rf frontend/src/components/ai-chat/
```

Also search for and remove `AIFlowBuilderModal` if it exists:
```bash
# Search: grep -r "AIFlowBuilderModal" frontend/src/
# Delete the file if found
```

**Step 5: Fix any remaining import errors**

Run: `cd frontend && npm run build`
Fix any broken imports referencing deleted files.

**Step 6: Verify build clean**

Run: `cd frontend && npm run build`
Expected: Build succeeds with no errors.

**Step 7: Commit**

```bash
git add -A
git commit -m "refactor: remove standalone Flow Assist page and old AI chat components"
```

---

## Phase 7: Backend Action-Type Prompt Dispatch

### Task 19: Add Delta Response Parsing to AI Chat Service

**Files:**
- Modify: `backend/app/core/ai_chat_service.py`

**Step 1: Write the failing test**

Create `backend/tests/test_ai_delta_response.py`:

```python
"""Tests for AI delta response parsing."""
from app.core.ai_chat_service import AIChatService


def test_parse_delta_from_response():
    """Service extracts [DELTA] markers from AI responses."""
    service = AIChatService()
    response = '''Here's a new branch for that node.

[DELTA]
{"action": "add", "target_node_id": "check-dns", "nodes": [{"id": "verify-dns-server", "type": "decision", "question": "Is the DNS server responding?"}], "explanation": "Added DNS verification branch"}
[/DELTA]

Let me know if you'd like to adjust this.'''

    parsed = service._parse_delta(response)
    assert parsed is not None
    assert parsed["action"] == "add"
    assert parsed["target_node_id"] == "check-dns"
    assert len(parsed["nodes"]) == 1


def test_parse_delta_none_when_absent():
    """Returns None when no delta marker present."""
    service = AIChatService()
    response = "Sure, I can explain that node. It checks connectivity."
    parsed = service._parse_delta(response)
    assert parsed is None
```

**Step 2: Run test to verify it fails**

Run: `cd backend && python -m pytest tests/test_ai_delta_response.py -v`
Expected: FAIL — `_parse_delta` does not exist.

**Step 3: Add _parse_delta method**

In `backend/app/core/ai_chat_service.py`, add:

```python
import re
import json

def _parse_delta(self, response: str) -> dict | None:
    """Extract [DELTA]...[/DELTA] JSON from AI response."""
    match = re.search(r'\[DELTA\](.*?)\[/DELTA\]', response, re.DOTALL)
    if not match:
        return None
    raw = self._strip_markdown_fences(match.group(1).strip())
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        return None
```

**Step 4: Run test to verify it passes**

Run: `cd backend && python -m pytest tests/test_ai_delta_response.py -v`
Expected: All PASS.

**Step 5: Commit**

```bash
git add backend/app/core/ai_chat_service.py backend/tests/test_ai_delta_response.py
git commit -m "feat: add delta response parsing to AI chat service"
```

---

### Task 20: Add Action-Type Prompt Dispatch

**Files:**
- Modify: `backend/app/core/ai_chat_service.py`

**Step 1: Write the failing test**

Add to `backend/tests/test_ai_delta_response.py`:

```python
def test_build_action_prompt_generate_branch():
    """Generate branch action includes focal node context."""
    service = AIChatService()
    tree = {
        "id": "root",
        "type": "decision",
        "question": "Is the server up?",
        "children": [],
        "options": [],
    }
    prompt = service._build_action_prompt(
        action_type="generate_branch",
        focal_node_id="root",
        tree_structure=tree,
        flow_type="troubleshooting",
    )
    assert "root" in prompt
    assert "generate" in prompt.lower() or "branch" in prompt.lower()


def test_build_action_prompt_open_chat():
    """Open chat action is general conversation."""
    service = AIChatService()
    prompt = service._build_action_prompt(
        action_type="open_chat",
        focal_node_id=None,
        tree_structure={"id": "root", "type": "decision"},
        flow_type="troubleshooting",
    )
    assert isinstance(prompt, str)
    assert len(prompt) > 0
```

**Step 2: Run test to verify it fails**

Run: `cd backend && python -m pytest tests/test_ai_delta_response.py -v`
Expected: FAIL — `_build_action_prompt` does not exist.

**Step 3: Add the method**

In `backend/app/core/ai_chat_service.py`, add:

```python
def _build_action_prompt(
    self,
    action_type: str,
    focal_node_id: str | None,
    tree_structure: dict,
    flow_type: str,
) -> str:
    """Build action-specific system prompt supplement."""
    import json

    tree_json = json.dumps(tree_structure, indent=2)

    focal_context = ""
    if focal_node_id:
        focal_node = self._find_node_by_id(tree_structure, focal_node_id)
        if focal_node:
            focal_context = f"\n\nFOCAL NODE (the node being acted on):\n{json.dumps(focal_node, indent=2)}"

    prompts = {
        "generate_branch": (
            f"Generate a complete branch of child nodes for the focal node. "
            f"Return the new nodes wrapped in [DELTA]...[/DELTA] markers as JSON with "
            f"action='add', target_node_id='{focal_node_id}', and nodes array."
            f"{focal_context}"
        ),
        "modify_node": (
            f"Modify the focal node based on the user's instruction. "
            f"Return the updated node in [DELTA]...[/DELTA] markers with action='modify'."
            f"{focal_context}"
        ),
        "add_steps": (
            f"Generate new procedural steps to insert after the focal step. "
            f"Return them in [DELTA]...[/DELTA] markers with action='add'."
            f"{focal_context}"
        ),
        "quick_action": (
            f"Respond to the user's quick action request about the focal node. "
            f"If the action modifies the node, return changes in [DELTA]...[/DELTA] markers. "
            f"If it's informational (e.g. explain), just respond in text."
            f"{focal_context}"
        ),
        "open_chat": (
            "Have a helpful conversation about the flow. If the user asks for changes, "
            "return them in [DELTA]...[/DELTA] markers. Otherwise respond in text."
        ),
        "generate_full": (
            "Generate a complete flow structure based on the user's description."
        ),
        "variable_inference": (
            "Analyze the procedural steps for implicit variables. Look for references to "
            "specific servers, clients, credentials, or other values that should be captured "
            "in an intake form. Return suggestions as JSON."
        ),
    }

    action_prompt = prompts.get(action_type, prompts["open_chat"])

    return (
        f"CURRENT FLOW STRUCTURE ({flow_type}):\n{tree_json}\n\n"
        f"ACTION: {action_type}\n{action_prompt}"
    )

def _find_node_by_id(self, tree: dict, node_id: str) -> dict | None:
    """Find a node by ID in a tree structure (recursive)."""
    if tree.get("id") == node_id:
        return tree
    for child in tree.get("children", []):
        found = self._find_node_by_id(child, node_id)
        if found:
            return found
    # For procedural steps
    for step in tree.get("steps", []):
        if step.get("id") == node_id:
            return step
    return None
```

**Step 4: Run tests**

Run: `cd backend && python -m pytest tests/test_ai_delta_response.py -v`
Expected: All PASS.

**Step 5: Integrate into send_message**

In the `send_message` method, after building the system prompt, append the action prompt:

```python
# In send_message, after building the base system prompt:
if action_type and action_type != "open_chat":
    action_supplement = self._build_action_prompt(
        action_type=action_type,
        focal_node_id=focal_node_id,
        tree_structure=session.working_tree or {},
        flow_type=session.flow_type,
    )
    # Append to system prompt or add as a user context message
```

**Step 6: Run full test suite**

Run: `cd backend && python -m pytest tests/test_ai_chat.py tests/test_ai_delta_response.py -v`
Expected: All PASS.

**Step 7: Commit**

```bash
git add backend/app/core/ai_chat_service.py backend/tests/test_ai_delta_response.py
git commit -m "feat: add action-type prompt dispatch with delta response format"
```

---

## Phase 8: Suggestion Endpoints + Archive

### Task 21: Add AI Suggestion CRUD Endpoints

**Files:**
- Create: `backend/app/schemas/ai_suggestion.py`
- Create: `backend/app/api/endpoints/ai_suggestions.py`
- Modify: `backend/app/api/router.py`

**Step 1: Create schemas**

Create `backend/app/schemas/ai_suggestion.py`:

```python
"""Schemas for AI suggestion audit trail."""
from datetime import datetime
from typing import Optional
from uuid import UUID

from pydantic import BaseModel, Field


class AISuggestionCreate(BaseModel):
    tree_id: UUID
    session_id: Optional[UUID] = None
    action_type: str
    target_node_id: Optional[str] = None
    changes_json: dict = Field(default_factory=dict)


class AISuggestionResponse(BaseModel):
    id: UUID
    tree_id: UUID
    user_id: UUID
    session_id: Optional[UUID]
    action_type: str
    target_node_id: Optional[str]
    changes_json: dict
    status: str
    created_at: datetime
    resolved_at: Optional[datetime]

    model_config = {"from_attributes": True}


class AISuggestionResolve(BaseModel):
    status: str = Field(..., pattern="^(accepted|dismissed)$")
```

**Step 2: Create endpoints**

Create `backend/app/api/endpoints/ai_suggestions.py`:

```python
"""AI Suggestion audit trail endpoints."""
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime, timezone

from app.api.deps import get_current_active_user, get_db
from app.models.user import User
from app.models.ai_suggestion import AISuggestion
from app.schemas.ai_suggestion import (
    AISuggestionCreate,
    AISuggestionResponse,
    AISuggestionResolve,
)

router = APIRouter(prefix="/ai/suggestions", tags=["ai-suggestions"])


@router.get("/tree/{tree_id}", response_model=list[AISuggestionResponse])
async def list_suggestions(
    tree_id: UUID,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """List all suggestions for a flow, filtered to current user."""
    result = await db.execute(
        select(AISuggestion)
        .where(AISuggestion.tree_id == tree_id, AISuggestion.user_id == current_user.id)
        .order_by(AISuggestion.created_at.desc())
    )
    return result.scalars().all()


@router.post("", response_model=AISuggestionResponse, status_code=201)
async def create_suggestion(
    data: AISuggestionCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """Record a new AI suggestion."""
    suggestion = AISuggestion(
        tree_id=data.tree_id,
        user_id=current_user.id,
        session_id=data.session_id,
        action_type=data.action_type,
        target_node_id=data.target_node_id,
        changes_json=data.changes_json,
    )
    db.add(suggestion)
    await db.commit()
    await db.refresh(suggestion)
    return suggestion


@router.patch("/{suggestion_id}", response_model=AISuggestionResponse)
async def resolve_suggestion(
    suggestion_id: UUID,
    data: AISuggestionResolve,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """Accept or dismiss a suggestion."""
    result = await db.execute(
        select(AISuggestion).where(
            AISuggestion.id == suggestion_id,
            AISuggestion.user_id == current_user.id,
        )
    )
    suggestion = result.scalar_one_or_none()
    if not suggestion:
        raise HTTPException(status_code=404, detail="Suggestion not found")

    suggestion.status = data.status
    suggestion.resolved_at = datetime.now(timezone.utc)
    await db.commit()
    await db.refresh(suggestion)
    return suggestion
```

**Step 3: Register in router**

In `backend/app/api/router.py`, add:

```python
from app.api.endpoints.ai_suggestions import router as ai_suggestions_router

api_router.include_router(ai_suggestions_router)
```

**Step 4: Write tests**

Create `backend/tests/test_ai_suggestions.py`:

```python
"""Tests for AI suggestion endpoints."""
import pytest


@pytest.mark.asyncio
async def test_create_and_list_suggestions(client, auth_headers, test_tree):
    """Can create and list suggestions for a tree."""
    tree_id = test_tree["id"]

    # Create suggestion
    resp = await client.post(
        "/api/v1/ai/suggestions",
        json={
            "tree_id": tree_id,
            "action_type": "generate_branch",
            "target_node_id": "some-node",
            "changes_json": {"before": {}, "after": {"id": "new-node"}},
        },
        headers=auth_headers,
    )
    assert resp.status_code == 201
    suggestion_id = resp.json()["id"]
    assert resp.json()["status"] == "pending"

    # List suggestions
    resp = await client.get(
        f"/api/v1/ai/suggestions/tree/{tree_id}",
        headers=auth_headers,
    )
    assert resp.status_code == 200
    assert len(resp.json()) >= 1

    # Resolve suggestion
    resp = await client.patch(
        f"/api/v1/ai/suggestions/{suggestion_id}",
        json={"status": "accepted"},
        headers=auth_headers,
    )
    assert resp.status_code == 200
    assert resp.json()["status"] == "accepted"
    assert resp.json()["resolved_at"] is not None
```

**Step 5: Run tests**

Run: `cd backend && python -m pytest tests/test_ai_suggestions.py -v`
Expected: All PASS.

**Step 6: Commit**

```bash
git add backend/app/schemas/ai_suggestion.py backend/app/api/endpoints/ai_suggestions.py backend/app/api/router.py backend/tests/test_ai_suggestions.py
git commit -m "feat: add AI suggestion audit trail endpoints"
```

---

### Task 22: Add Session Auto-Archive APScheduler Task

**Files:**
- Modify: `backend/app/main.py` (or wherever APScheduler is configured)

**Step 1: Find existing APScheduler setup**

Search for `APScheduler` or `add_job` in `backend/app/main.py` or `backend/app/core/`.

**Step 2: Add the archive job**

Add a new scheduled job function:

```python
async def archive_stale_ai_sessions():
    """Archive AI chat sessions with no activity for 30 days."""
    from app.core.database import async_session_maker
    from app.models.ai_chat_session import AIChatSession
    from sqlalchemy import update
    from datetime import datetime, timezone, timedelta

    cutoff = datetime.now(timezone.utc) - timedelta(days=30)
    async with async_session_maker() as db:
        await db.execute(
            update(AIChatSession)
            .where(
                AIChatSession.updated_at < cutoff,
                AIChatSession.archived_at.is_(None),
                AIChatSession.status != "abandoned",
            )
            .values(archived_at=datetime.now(timezone.utc))
        )
        await db.commit()
```

Register it in the scheduler (follow existing pattern):

```python
scheduler.add_job(
    archive_stale_ai_sessions,
    'cron',
    hour=3,  # Run daily at 3 AM
    id='archive_stale_ai_sessions',
    replace_existing=True,
)
```

**Step 3: Verify the app starts**

Run: `cd backend && uvicorn app.main:app --reload`
Expected: App starts without errors, job registered.

**Step 4: Commit**

```bash
git add backend/app/main.py
git commit -m "feat: add APScheduler task to auto-archive stale AI chat sessions"
```

---

## Phase 9: Final Integration + Build Verification

### Task 23: Frontend Build Verification

**Step 1: Run full frontend build**

Run: `cd frontend && npm run build`
Expected: Build succeeds with zero errors.

**Step 2: Fix any remaining issues**

Address any TypeScript errors, unused imports, or missing type exports.

**Step 3: Commit fixes if needed**

```bash
git add -A
git commit -m "fix: resolve build issues from editor AI integration"
```

---

### Task 24: Backend Test Suite Verification

**Step 1: Run full backend test suite**

Run: `cd backend && python -m pytest --override-ini="addopts=" -v`
Expected: All tests PASS. No regressions.

**Step 2: Fix any failures**

Address any test failures from the changes.

**Step 3: Commit fixes if needed**

```bash
git add -A
git commit -m "fix: resolve test regressions from editor AI integration"
```

---

### Task 25: Update Documentation

**Files:**
- Modify: `CURRENT-STATE.md`
- Modify: `CLAUDE.md` (if new patterns/lessons discovered)

**Step 1: Update CURRENT-STATE.md**

Move "AI chat session conclusion" items and add:
- **In Progress:** Editor-Embedded Flow Assist (AI panel in tree + procedural editors)
- **Recently Completed:** (update as appropriate)

**Step 2: Update CLAUDE.md if needed**

Add any new lessons learned (e.g., ghost node pattern, zundo pause/resume, action-type routing).

**Step 3: Commit**

```bash
git add CURRENT-STATE.md CLAUDE.md
git commit -m "docs: update project status for editor-embedded Flow Assist"
```

---

## Summary

| Phase | Tasks | Description |
|---|---|---|
| **1** | 1-6 | Bug fix + backend foundation (config, models, migrations, schemas) |
| **2** | 7-10 | Frontend infrastructure (types, ContextMenu, EditorAIPanel, useEditorAI hook) |
| **3** | 11-13 | Tree editor integration (panel layout, context menu threading, ghost nodes) |
| **4** | 14-15 | Procedural editor integration (panel layout, ghost steps) |
| **5** | 16-17 | AI-assisted create (prompt dialog, CreateFlowDropdown update) |
| **6** | 18 | Remove old standalone Flow Assist |
| **7** | 19-20 | Backend action-type prompt dispatch + delta response |
| **8** | 21-22 | Suggestion endpoints + auto-archive |
| **9** | 23-25 | Build verification + docs |

**Deferred to future work:**
- Knowledge integration (Phase 1: Microsoft Learn MCP)
- Intake variable inference (tiers 2-3: natural language + cross-step)
- Ghost node grouping for 5+ suggestions ("Accept Branch" controls)
- Before/after diff view for modified-selected-node in chat
