# Flow Editor UX Fixes Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Fix three UX problems in the flow editor — unreachable card content, noisy hint text, and forced child-type selection while naming answer options.
**Architecture:** Five phases in order: scrollability + fullscreen modal, reusable InfoTip component + tooltip replacements, answer stub type system (frontend types → new component → canvas wiring → NodeList guard), backend draft/publish validation, then markdown serializer and runtime navigation guard. Each phase builds on the previous and must produce a clean `npm run build` before the next begins.
**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (`treeEditorStore`), FastAPI (`tree_validation.py`), `frontend/src/lib/treeMarkdownSync.ts`
**Working directory:** `/home/michaelchihlas/dev/patherly` (main branch — this plan targets the main codebase, not the worktree, since the canvas code was already merged or will be)
> **Note on worktree vs main:** If the `feature/tree-editor-canvas` branch has not yet been merged to main, run all frontend tasks in `.worktrees/tree-editor-canvas/frontend/` and all backend tasks in `.worktrees/tree-editor-canvas/backend/`. If it has been merged, use the repo root. Check with `git branch --show-current` at the start.
---
## Phase 1: Scrollability + Fullscreen Editor
### Task 1.1: Fix canvas inline card scroll (TreeCanvasNode)
**Files:**
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`
**Step 1: Make the card header sticky when expanded**
Open the file. Find the card header `
` (around line 165) — it's the one with class `flex items-center gap-2 px-3 py-2.5`. It currently has a `cn()` call like this:
```tsx
className={cn(
'flex items-center gap-2 px-3 py-2.5',
!isExpanded && 'cursor-pointer hover:bg-accent/50 rounded-t-xl',
!isExpanded && 'rounded-xl'
)}
```
Add a sticky class when expanded:
```tsx
className={cn(
'flex items-center gap-2 px-3 py-2.5',
!isExpanded && 'cursor-pointer hover:bg-accent/50 rounded-t-xl',
!isExpanded && 'rounded-xl',
isExpanded && 'sticky top-0 z-10 bg-card rounded-t-xl'
)}
```
**Step 2: Make the expanded editing area scrollable**
Find the expanded content `
` (around line 324) — it's the one that appears under `{isExpanded && (`:
```tsx
```
Change it to:
```tsx
```
**Step 3: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: `✓ built in Xs` with zero errors.
**Step 4: Commit**
```bash
git add frontend/src/components/tree-editor/TreeCanvasNode.tsx
git commit -m "fix: make canvas card expanded area scrollable with sticky header
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 1.2: Add fullscreen toggle to Modal component
**Files:**
- Modify: `frontend/src/components/common/Modal.tsx`
- Modify: `frontend/src/components/tree-editor/NodeEditorModal.tsx`
**Step 1: Update Modal.tsx**
The current `Modal.tsx` is ~103 lines. The `ModalProps` interface (lines 5–13) and the component signature (line 15) need a new `allowFullScreen` optional prop.
Replace the entire `Modal.tsx` content with the following (it's short enough to replace in full to be safe):
```tsx
import { useState, useEffect, useCallback, type ReactNode } from 'react'
import { X, Maximize2, Minimize2 } from 'lucide-react'
import { cn } from '@/lib/utils'
interface ModalProps {
isOpen: boolean
onClose: () => void
title: string
children: ReactNode
/** Optional footer content that stays fixed at bottom (doesn't scroll) */
footer?: ReactNode
size?: 'sm' | 'md' | 'lg' | 'xl'
/** If true, a fullscreen toggle button appears in the modal header */
allowFullScreen?: boolean
}
export function Modal({ isOpen, onClose, title, children, footer, size = 'md', allowFullScreen = false }: ModalProps) {
const [isFullScreen, setIsFullScreen] = useState(() => {
if (!allowFullScreen) return false
try {
return localStorage.getItem('rf-editor-fullscreen') === 'true'
} catch {
return false
}
})
const toggleFullScreen = () => {
const next = !isFullScreen
setIsFullScreen(next)
try {
localStorage.setItem('rf-editor-fullscreen', String(next))
} catch {}
}
// Close on Escape key
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
}
},
[onClose]
)
useEffect(() => {
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
document.body.style.overflow = 'hidden'
}
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.body.style.overflow = ''
}
}, [isOpen, handleKeyDown])
if (!isOpen) return null
const sizeClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-full sm:max-w-lg',
xl: 'max-w-full sm:max-w-4xl',
}
return (
{/* Backdrop */}
{/* Modal Content */}
{/* Header - Fixed at top */}
{title}
{allowFullScreen && (
)}
{/* Body - Scrollable */}
{children}
{/* Footer - Fixed at bottom */}
{footer && (
{footer}
)}
)
}
export default Modal
```
**Step 2: Pass `allowFullScreen` to NodeEditorModal**
In `frontend/src/components/tree-editor/NodeEditorModal.tsx`, find line 86:
```tsx
```
Change to:
```tsx
```
**Step 3: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build, zero errors.
**Step 4: Commit**
```bash
git add frontend/src/components/common/Modal.tsx frontend/src/components/tree-editor/NodeEditorModal.tsx
git commit -m "feat: add fullscreen toggle to Modal, enable in NodeEditorModal
Co-Authored-By: Claude Sonnet 4.6 "
```
---
## Phase 2: Info-On-Demand Tooltips
### Task 2.1: Create the InfoTip component
**Files:**
- Create: `frontend/src/components/common/InfoTip.tsx`
**Step 1: Create the file**
```tsx
interface InfoTipProps {
text: string
}
export function InfoTip({ text }: InfoTipProps) {
return (
i
)
}
```
**Step 2: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build.
**Step 3: Commit**
```bash
git add frontend/src/components/common/InfoTip.tsx
git commit -m "feat: add reusable InfoTip component for field-level help
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 2.2: Replace hint text in NodeFormDecision
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
**Step 1: Add the InfoTip import**
After the existing imports at the top of the file, add:
```tsx
import { InfoTip } from '@/components/common/InfoTip'
```
**Step 2: Remove the root node question hint paragraph**
Around line 89–93 there is:
```tsx
{isRootNode && (
What's the main question to diagnose the issue?
)}
```
Delete this entire block. The input placeholder `"e.g., What type of issue are you experiencing?"` already conveys the intent.
**Step 3: Replace the options hint paragraphs with an InfoTip on the label**
Around lines 133–144, the Options section label and hints look like:
```tsx
{isRootNode ? (
Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path.
) : (
Each option can branch to a different next step.
)}
```
Replace with:
```tsx
```
**Step 4: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build.
**Step 5: Commit**
```bash
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
git commit -m "fix: replace hint paragraphs with InfoTip tooltips in NodeFormDecision
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 2.3: Replace hint text in NodeFormAction
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormAction.tsx`
**Step 1: Add the InfoTip import**
Add at the top after existing imports:
```tsx
import { InfoTip } from '@/components/common/InfoTip'
```
**Step 2: Replace the Description hint paragraph**
Around lines 91–93:
```tsx
```
And the Description label above it (around line 77–79):
```tsx
```
Replace both with (combine label + infotip, remove paragraph):
```tsx
```
**Step 3: Replace the Commands hint paragraph**
Around lines 124–126:
```tsx
PowerShell or CLI commands to execute
```
And the Commands label above it:
```tsx
```
Replace with:
```tsx
```
**Step 4: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build.
**Step 5: Commit**
```bash
git add frontend/src/components/tree-editor/NodeFormAction.tsx
git commit -m "fix: replace hint paragraphs with InfoTip tooltips in NodeFormAction
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 2.4: Replace hint text in NodeFormResolution
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormResolution.tsx`
**Step 1: Add the InfoTip import**
```tsx
import { InfoTip } from '@/components/common/InfoTip'
```
**Step 2: Replace the Description hint paragraph**
Around lines 86–88 (same markdown hint as NodeFormAction). Replace:
```tsx
```
With:
```tsx
```
**Step 3: Replace the Resolution Steps hint paragraph**
Around lines 118–120:
```tsx
Step-by-step instructions for resolving the issue
```
Replace with:
```tsx
```
**Step 4: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build.
**Step 5: Commit**
```bash
git add frontend/src/components/tree-editor/NodeFormResolution.tsx
git commit -m "fix: replace hint paragraphs with InfoTip tooltips in NodeFormResolution
Co-Authored-By: Claude Sonnet 4.6 "
```
---
## Phase 3: Answer Stub Placeholder System
### Task 3.1: Add `'answer'` to the NodeType union
**Files:**
- Modify: `frontend/src/types/tree.ts:4`
**Step 1: Edit the NodeType line**
Find line 4:
```typescript
export type NodeType = 'decision' | 'action' | 'solution'
```
Change to:
```typescript
export type NodeType = 'decision' | 'action' | 'solution' | 'answer'
```
**Step 2: Run build — note the expected error**
```bash
cd frontend && npm run build 2>&1 | grep "error TS" | head -10
```
Expected: You will see TypeScript errors in `TreeCanvasNode.tsx` (and possibly `NodeList.tsx`) because their `Record` maps don't include `'answer'`. This is expected and will be fixed in Tasks 3.3 and 3.6.
**Step 3: Commit the type change now** (before fixing downstream errors)
```bash
git add frontend/src/types/tree.ts
git commit -m "feat: add 'answer' to NodeType union for branch placeholder stubs
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 3.2: Create the AnswerStubCard component
**Files:**
- Create: `frontend/src/components/tree-editor/AnswerStubCard.tsx`
**Step 1: Create the file**
```tsx
import { useState } from 'react'
import { HelpCircle, Zap, CheckCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { TreeStructure } from '@/types'
interface AnswerStubCardProps {
node: TreeStructure // type === 'answer'
fromOption?: string
onSelectType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void
}
export function AnswerStubCard({ node, fromOption, onSelectType }: AnswerStubCardProps) {
const [picking, setPicking] = useState(false)
const label = fromOption || node.title || 'Answer'
return (
!picking && setPicking(true)}
>
{/* Label */}
{label}
{/* Prompt / type picker */}
{!picking ? (
+ Choose Type
) : (
)}
)
}
export default AnswerStubCard
```
**Step 2: Build to confirm no errors in this new file**
```bash
cd frontend && npm run build 2>&1 | grep "AnswerStubCard"
```
Expected: No errors mentioning AnswerStubCard (the earlier `TreeCanvasNode.tsx` errors from Task 3.1 are still present, that's fine).
**Step 3: Commit**
```bash
git add frontend/src/components/tree-editor/AnswerStubCard.tsx
git commit -m "feat: add AnswerStubCard component for unresolved branch placeholders
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 3.3: Guard TreeCanvasNode against `'answer'` type
**Files:**
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`
**Step 1: Fix the NODE_TYPE_CONFIG lookup**
Find (around line 135):
```tsx
const config = NODE_TYPE_CONFIG[node.type]
const TypeIcon = config.icon
```
Replace with:
```tsx
const config = node.type in NODE_TYPE_CONFIG
? NODE_TYPE_CONFIG[node.type as keyof typeof NODE_TYPE_CONFIG]
: NODE_TYPE_CONFIG.decision // fallback for 'answer' (rendered by AnswerStubCard)
const TypeIcon = config.icon
```
**Step 2: Build — confirm the TS error from Task 3.1 is now gone**
```bash
cd frontend && npm run build 2>&1 | grep "TreeCanvasNode"
```
Expected: No errors mentioning TreeCanvasNode.
**Step 3: Confirm full clean build (NodeList errors may still exist)**
```bash
cd frontend && npm run build 2>&1 | grep "error TS" | head -5
```
Note: NodeList errors will be fixed in Task 3.6. Only TreeCanvasNode should be clean now.
**Step 4: Commit**
```bash
git add frontend/src/components/tree-editor/TreeCanvasNode.tsx
git commit -m "fix: guard NODE_TYPE_CONFIG lookup against 'answer' type
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 3.4: Redesign NodeFormDecision — label-only options (remove NodePicker)
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
The old form had a NodePicker per option that forced users to pick a child node type during the same editing session as writing the question. The new form is label-only — stubs are created automatically on save.
**Step 1: Remove the NodePicker import**
Find and delete:
```tsx
import { NodePicker } from './NodePicker'
```
**Step 2: Replace the DynamicArrayField renderItem**
Find the existing `renderItem` prop inside ``. The current version renders a box with a letter badge + label input + NodePicker. Replace the entire `renderItem` callback with:
```tsx
renderItem={(option, index) => {
const optionLabelError = validationErrors.find(
e => e.nodeId === node.id && e.field === `options[${index}].label`
)
const letter = indexToLetter(index)
return (
)
}}
```
**Step 3: Remove the optionNextError validation lookup** (it referenced `options[N].next_node_id`, no longer needed since there's no NodePicker)
Inside the old renderItem, there was:
```tsx
const optionNextError = validationErrors.find(
e => e.nodeId === node.id && e.field === `options[${index}].next_node_id`
)
```
This is now gone (it was inside the old renderItem you just replaced). Verify there are no remaining references.
**Step 4: Build**
```bash
cd frontend && npm run build 2>&1 | grep -E "NodeFormDecision|NodePicker" | head -10
```
Expected: No errors. If you see "Cannot find module './NodePicker'" that means the import wasn't fully removed — double-check Step 1.
**Step 5: Commit**
```bash
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
git commit -m "feat: redesign NodeFormDecision to label-only options, remove NodePicker
Users now type answer labels only. Stub nodes are created automatically
by TreeCanvas when the decision node is saved.
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 3.5: Wire up auto-creation and AnswerStubCard rendering in TreeCanvas
**Files:**
- Modify: `frontend/src/components/tree-editor/TreeCanvas.tsx`
**Step 1: Add the AnswerStubCard import**
After the existing `TreeCanvasNode` import, add:
```tsx
import { AnswerStubCard } from './AnswerStubCard'
```
**Step 2: Add `handleSelectAnswerType` callback**
After the `handleDuplicate` callback (around line 278), add:
```tsx
// ── Convert answer stub to a real node type ──
const handleSelectAnswerType = useCallback(
(nodeId: string, type: 'decision' | 'action' | 'solution') => {
updateNode(nodeId, { type })
setExpandedNodeId(nodeId)
selectNode(nodeId)
},
[updateNode, selectNode]
)
```
**Step 3: Update `handleSave` to auto-create stubs for unlinked options**
Find `handleSave` (around line 202). It currently starts:
```tsx
const handleSave = useCallback(
(nodeId: string, updates: Partial) => {
updateNode(nodeId, updates)
// Resolve pending link for new nodes
const link = pendingLinks.get(nodeId)
```
After `updateNode(nodeId, updates)` and before the pending link resolution, insert:
```tsx
// For decision nodes: create answer stubs for any option without a next_node_id
if (updates.options) {
const options = updates.options
const stubsToCreate: Array<{ opt: typeof options[number]; stubId: string }> = []
options.forEach((opt) => {
if (!opt.next_node_id && opt.label.trim()) {
const stubId = addNode(nodeId, 'answer')
updateNode(stubId, { title: opt.label })
stubsToCreate.push({ opt, stubId })
}
})
if (stubsToCreate.length > 0) {
const updatedOptions = options.map((o) => {
const stub = stubsToCreate.find((s) => s.opt.id === o.id)
return stub ? { ...o, next_node_id: stub.stubId } : o
})
updateNode(nodeId, { options: updatedOptions })
}
}
```
> **Why this shape:** We build the list of stubs first, then do a single `updateNode` with the fully updated options array, to avoid multiple sequential calls stomping on each other.
**Step 4: Add `handleSelectAnswerType` to the `renderNode` dependency array**
Find the `useCallback` dependency array at the end of `renderNode` (around line 580–594). Add `handleSelectAnswerType` to the array.
**Step 5: Render AnswerStubCard for answer-type nodes in `renderNode`**
In `renderNode`, find where `` is rendered (it's the card component, around line 468). Wrap it with a conditional:
Replace:
```tsx
{/* The node card itself */}
```
With:
```tsx
{/* The node card — answer stubs get their own component */}
{node.type === 'answer' ? (
) : (
handleToggleExpand(node.id)}
onSave={handleSave}
onCancelNew={handleCancelNew}
onDelete={handleDelete}
onDuplicate={handleDuplicate}
onDragStart={handleDragStart}
onDragOver={(e) => handleDragOver(e, parentId, index)}
onDrop={(e) => handleDrop(e, parentId, index)}
/>
)}
```
**Step 6: Build**
```bash
cd frontend && npm run build 2>&1 | tail -15
```
Expected: Clean build, zero errors (NodeList may still have errors — check next task).
**Step 7: Commit**
```bash
git add frontend/src/components/tree-editor/TreeCanvas.tsx
git commit -m "feat: auto-create answer stubs on decision save, render AnswerStubCard
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 3.6: Guard NodeList against `'answer'` type
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeList.tsx`
The `nodeTypeIcons` and `nodeTypeColors` objects (lines 91–101) use `Record` which now requires an `'answer'` entry.
**Step 1: Add `'answer'` entries to both records**
Find:
```tsx
const nodeTypeIcons: Record = {
decision: ,
action: ,
solution:
}
const nodeTypeColors: Record = {
decision: 'bg-blue-500/20 text-blue-400',
action: 'bg-yellow-500/20 text-yellow-400',
solution: 'bg-green-500/20 text-green-400'
}
```
Replace with:
```tsx
const nodeTypeIcons: Record = {
decision: ,
action: ,
solution: ,
answer:
}
const nodeTypeColors: Record = {
decision: 'bg-blue-500/20 text-blue-400',
action: 'bg-yellow-500/20 text-yellow-400',
solution: 'bg-green-500/20 text-green-400',
answer: 'bg-muted text-muted-foreground border border-dashed border-border'
}
```
**Step 2: Build — confirm full clean build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: `✓ built in Xs` — zero TypeScript errors.
**Step 3: Commit**
```bash
git add frontend/src/components/tree-editor/NodeList.tsx
git commit -m "fix: add answer type to NodeList icon and color maps
Co-Authored-By: Claude Sonnet 4.6 "
```
---
## Phase 4: Backend + Frontend Validation
### Task 4.1: Backend — allow `'answer'` in drafts, block on publish
**Files:**
- Modify: `backend/app/core/tree_validation.py`
**Step 1: Add the `'answer'` elif in `_validate_node`**
Find the `_validate_node` function. Inside it, find the `else` branch at the end (around lines 92–96):
```python
else:
errors.append({
"field": f"{path}.type",
"message": f"Unknown node type: {node_type}"
})
```
Insert a new `elif` before the `else`:
```python
elif node_type == "answer":
# Answer nodes are draft-only placeholders — no structural validation needed
pass
else:
errors.append({
"field": f"{path}.type",
"message": f"Unknown node type: {node_type}"
})
```
**Step 2: Add the `_has_answer_nodes` helper**
After the `_validate_children` function (ends around line 115), add a new function:
```python
def _has_answer_nodes(node: dict[str, Any]) -> bool:
"""Recursively check if any node in the tree has type 'answer'."""
if node.get("type") == "answer":
return True
for child in node.get("children", []):
if _has_answer_nodes(child):
return True
return False
```
**Step 3: Add publish-time check in `validate_tree_structure`**
Find `validate_tree_structure`. After the `_validate_children` call and before `return len(errors) == 0, errors` (around line 53–56):
```python
# Validate all child nodes recursively
if "children" in tree_structure:
_validate_children(tree_structure["children"], "root.children", errors)
return len(errors) == 0, errors
```
Change to:
```python
# Validate all child nodes recursively
if "children" in tree_structure:
_validate_children(tree_structure["children"], "root.children", errors)
# Block publish if any answer placeholder nodes remain
if _has_answer_nodes(tree_structure):
errors.append({
"field": "tree_structure",
"message": "Answer placeholders must be resolved to a node type before publishing."
})
return len(errors) == 0, errors
```
**Step 4: Run backend tests**
```bash
cd backend
pytest --override-ini="addopts=" -q 2>&1 | tail -15
```
Expected: All existing tests pass. No new failures.
**Step 5: Commit**
```bash
git add backend/app/core/tree_validation.py
git commit -m "feat: allow 'answer' type in tree drafts, block on publish
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 4.2: Frontend publish guard
**Files:**
- Modify: `frontend/src/pages/TreeEditorPage.tsx`
**Step 1: Add utility function before the component**
Find the component declaration (`export function TreeEditorPage` or similar). Immediately before it, add:
```typescript
/** Recursively check if any node in the tree has type 'answer' */
function hasAnswerNodes(node: TreeStructure): boolean {
if (node.type === 'answer') return true
return (node.children || []).some(hasAnswerNodes)
}
```
Ensure `TreeStructure` is imported from `@/types` — check the existing imports at the top of the file (it should already be there).
**Step 2: Add the guard in `handlePublish`**
Find `handlePublish` (around line 269). It starts with a code-mode markdown validation check. After the tree name check (around line 293, after `if (!currentState.name.trim()) {...}`) and before `const errors = validate()`, insert:
```typescript
// Block publish if any answer placeholder nodes remain
const currentStructure = useTreeEditorStore.getState().treeStructure
if (currentStructure && hasAnswerNodes(currentStructure)) {
toast.error('Resolve all answer placeholders before publishing. Click each dashed stub card to assign a type.')
setSaving(false)
return
}
```
**Step 3: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build.
**Step 4: Commit**
```bash
git add frontend/src/pages/TreeEditorPage.tsx
git commit -m "feat: block publish if unresolved answer stub nodes exist
Co-Authored-By: Claude Sonnet 4.6 "
```
---
## Phase 5: Markdown Serializer + Runtime Guard
### Task 5.1: Handle `'answer'` in the markdown serializer
**Files:**
- Modify: `frontend/src/lib/treeMarkdownSync.ts`
**Step 1: Locate the `serializeNode` function**
In `treeMarkdownSync.ts`, find `serializeNode`. It has a chain of `if (node.type === 'decision') ... else if (node.type === 'action') ... else if (node.type === 'solution')`. After the final `else if` (around line 75–81), add an `else if` for `'answer'`:
```typescript
} else if (node.type === 'answer') {
// Answer placeholder — render as a clearly marked stub
body.push(`## [ANSWER PLACEHOLDER] ${node.title || 'Untitled'}`, '')
body.push('> This is an unresolved answer stub. Convert it to a Decision, Action, or Solution before publishing.')
}
```
**Step 2: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build.
**Step 3: Commit**
```bash
git add frontend/src/lib/treeMarkdownSync.ts
git commit -m "feat: serialize 'answer' stub nodes in markdown output
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 5.2: Add runtime defensive guard in TreeNavigationPage
**Files:**
- Modify: `frontend/src/pages/TreeNavigationPage.tsx`
**Step 1: Find the "Current Node" rendering block**
Around line 758–760 there is a comment `{/* Current Node */}` followed by a `
`. Inside this `
`, node type is dispatched via conditionals:
```tsx
{currentNode && currentNode.type === 'decision' && (
...
)}
```
Before any of these existing conditionals (before the `decision` block), add a guard for `'answer'` nodes:
```tsx
{currentNode && currentNode.type === 'answer' && (
This tree contains an unresolved placeholder node. Please contact the tree author to complete it before use.
)}
```
**Step 2: Build**
```bash
cd frontend && npm run build 2>&1 | tail -10
```
Expected: Clean build.
**Step 3: Commit**
```bash
git add frontend/src/pages/TreeNavigationPage.tsx
git commit -m "fix: add defensive guard for answer nodes in session navigation
Co-Authored-By: Claude Sonnet 4.6 "
```
---
## Phase 6: Final Verification
### Task 6.1: Full build and backend test suite
**Step 1: Frontend build**
```bash
cd frontend && npm run build 2>&1 | tail -5
```
Expected: `✓ built in Xs` — zero errors.
**Step 2: Backend tests**
```bash
cd backend && pytest --override-ini="addopts=" -q 2>&1 | tail -10
```
Expected: All existing tests pass.
---
### Task 6.2: Manual test checklist
Confirm all of the following in the browser:
1. **Canvas scroll** — Open a decision node in the canvas editor → resize browser to a short viewport → form content scrolls → sticky header (save/cancel) stays visible at top
2. **Modal scroll** — Open a node via the modal editor (`NodeEditorModal`) → content scrolls, header and footer are fixed
3. **Fullscreen toggle** — Click the expand icon in the modal header → modal fills viewport with margin → click again → returns to normal size smoothly → refresh → preference is remembered
4. **Other modals unaffected** — Open any other modal (step library, share session, etc.) → no fullscreen button appears
5. **InfoTip tooltips** — Hover over `ⓘ` badges on NodeFormDecision / NodeFormAction / NodeFormResolution labels → tooltip text appears → no always-visible hint paragraphs remain
6. **Answer stubs — creation** — Create or edit a decision node → type a question → type answer labels ("Server", "Desktop") → save → two dashed stub cards appear below the decision
7. **Answer stubs — conversion** — Click a dashed stub → three type buttons appear (Decision / Action / Solution) → click one → stub converts to a real node card in expanded editing mode
8. **Draft save with stubs** — Save draft with unresolved stubs → no backend error
9. **Publish blocked** — Leave an unresolved stub → click Publish → toast: "Resolve all answer placeholders before publishing."
10. **Publish succeeds after resolution** — Convert all stubs → Publish → succeeds
---
## Summary of All Files Changed
### New Files
| File | Description |
|------|-------------|
| `frontend/src/components/common/InfoTip.tsx` | Reusable info tooltip badge |
| `frontend/src/components/tree-editor/AnswerStubCard.tsx` | Dashed stub card with inline type picker |
### Modified Files
| File | Changes |
|------|---------|
| `frontend/src/components/tree-editor/TreeCanvasNode.tsx` | Sticky header + scrollable area + answer type guard |
| `frontend/src/components/common/Modal.tsx` | `allowFullScreen` prop + toggle button + localStorage |
| `frontend/src/components/tree-editor/NodeEditorModal.tsx` | Pass `allowFullScreen={true}` |
| `frontend/src/components/common/InfoTip.tsx` | (new) |
| `frontend/src/components/tree-editor/NodeFormDecision.tsx` | InfoTip tooltips + label-only options |
| `frontend/src/components/tree-editor/NodeFormAction.tsx` | InfoTip tooltips |
| `frontend/src/components/tree-editor/NodeFormResolution.tsx` | InfoTip tooltips |
| `frontend/src/types/tree.ts` | Add `'answer'` to NodeType union |
| `frontend/src/components/tree-editor/TreeCanvas.tsx` | Auto-create stubs + AnswerStubCard rendering |
| `frontend/src/components/tree-editor/NodeList.tsx` | Add answer type to icon/color maps |
| `frontend/src/pages/TreeEditorPage.tsx` | Publish guard |
| `frontend/src/pages/TreeNavigationPage.tsx` | Runtime guard for answer nodes |
| `frontend/src/lib/treeMarkdownSync.ts` | Serialize answer nodes |
| `backend/app/core/tree_validation.py` | Allow answer in drafts, block on publish |