# 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 */} ) } 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

Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`

``` 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

Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`

``` 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 (
{letter}
handleUpdateOption(index, { label: e.target.value })} placeholder={isRootNode ? `Branch ${letter}: e.g., "Network Issues"...` : `Option ${letter} label`} className={cn( 'block w-full rounded-md border px-3 py-2 text-sm', 'bg-background text-foreground placeholder:text-muted-foreground', 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary', optionLabelError ? 'border-red-400' : 'border-border' )} /> {optionLabelError && (

{optionLabelError.message}

)}
) }} ``` **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 |