# Canvas 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 TreeCanvas editor: card scroll, noisy hint text, and forced child-type selection when building decision nodes.
**Architecture:** Three independent fixes applied to the canvas editor components only. Fix 1 is a pure CSS change. Fix 2 replaces `
` hint text with native `title` tooltips on ⓘ badges. Fix 3 introduces a new `'answer'` NodeType — a branch placeholder that the user converts to a real type by clicking it.
**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (existing `treeEditorStore`), FastAPI backend (`tree_validation.py`)
**Working directory:** `/home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas`
---
## Fix 1: Card Scroll
### Task 1: Make expanded card area scrollable with sticky header
**Files:**
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx:164-321`
**Step 1: Open the file and locate the card header div (expanded state)**
The card header is the `
` at line 165. When expanded it shows the action buttons (save/cancel/etc). We need this row to be sticky.
Find this block (around line 165):
```tsx
```
Change it to:
```tsx
```
**Step 2: Make the expanded editing area scrollable**
Find the expanded editing area div (around line 324):
```tsx
{isExpanded && (
```
Change it to:
```tsx
{isExpanded && (
```
**Step 3: Build and verify no TypeScript errors**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
```
Expected: Build exits with code 0, no errors mentioning TreeCanvasNode.
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
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 "
```
---
## Fix 2: Info Tooltips
### Task 2: Replace hint text in NodeFormDecision
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
The ⓘ badge pattern to use throughout Fix 2:
```tsx
i
```
**Step 1: Find the root node hint paragraph inside the Question field**
Around line 89–93:
```tsx
{isRootNode && (
What's the main question to diagnose the issue?
)}
```
Remove this `
` block entirely. The input's placeholder already conveys the intent.
**Step 2: Find the options hint paragraphs**
Around lines 136–143:
```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 both `
` tags with a ⓘ tooltip on the Options label. Change the label section (around line 133) from:
```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.
)}
```
To:
```tsx
```
**Step 3: Build to check for TS errors**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
```
Expected: Clean build.
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormDecision
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 3: Replace hint text in NodeFormAction
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormAction.tsx`
**Step 1: Find the description hint paragraph**
Around lines 91–93:
```tsx
// After
```
**Step 2: Find the commands hint paragraph**
Around lines 124–126:
```tsx
PowerShell or CLI commands to execute
```
Change the Commands label + remove the hint:
```tsx
// Before
PowerShell or CLI commands to execute
// After
```
**Step 3: Build to check for TS errors**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
```
Expected: Clean build.
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
git add frontend/src/components/tree-editor/NodeFormAction.tsx
git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormAction
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 4: Replace hint text in NodeFormResolution
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormResolution.tsx`
**Step 1: Find the description hint paragraph**
Around lines 86–88:
```tsx
// After
```
**Step 2: Find the resolution steps hint paragraph**
Around lines 118–120:
```tsx
Step-by-step instructions for resolving the issue
```
Change the Resolution Steps label + remove the hint:
```tsx
// Before
Step-by-step instructions for resolving the issue
// After
```
**Step 3: Build to check for TS errors**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
```
Expected: Clean build.
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
git add frontend/src/components/tree-editor/NodeFormResolution.tsx
git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormResolution
Co-Authored-By: Claude Sonnet 4.6 "
```
---
## Fix 3: Answer Stubs
### Task 5: Add `'answer'` to the NodeType union
**Files:**
- Modify: `frontend/src/types/tree.ts:4`
**Step 1: Add `'answer'` to NodeType**
Current line 4:
```typescript
export type NodeType = 'decision' | 'action' | 'solution'
```
Change to:
```typescript
export type NodeType = 'decision' | 'action' | 'solution' | 'answer'
```
**Step 2: Check for TS exhaustiveness errors**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | grep -E "error TS|Type.*answer"
```
The build will likely show an error in `TreeCanvasNode.tsx` because `NODE_TYPE_CONFIG` only has keys for `decision`, `action`, `solution` — and `config = NODE_TYPE_CONFIG[node.type]` will fail when `node.type === 'answer'`. We fix that in Task 7. For now note the exact error and proceed.
**Step 3: Commit the type change**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
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 6: Create the AnswerStubCard component
**Files:**
- Create: `frontend/src/components/tree-editor/AnswerStubCard.tsx`
**Step 1: Create the file with the following content**
```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 check for TS errors in the new file only**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | grep "AnswerStubCard"
```
Expected: No errors mentioning AnswerStubCard.
**Step 3: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
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 7: Update TreeCanvasNode to handle `'answer'` type
**Files:**
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`
The `NODE_TYPE_CONFIG` object (line 47) only has entries for `decision`, `action`, `solution`. When `node.type === 'answer'`, calling `NODE_TYPE_CONFIG[node.type]` will cause a TypeScript error and runtime crash.
The fix: guard `config` access so answer nodes get a safe fallback. However, answer nodes should **never** be rendered by `TreeCanvasNode` — `TreeCanvas` will render them as `AnswerStubCard` instead. We still need to fix the TypeScript error.
**Step 1: Guard the config lookup**
Find around line 135:
```tsx
const config = NODE_TYPE_CONFIG[node.type]
const TypeIcon = config.icon
```
Change to:
```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' type (should be rendered by AnswerStubCard instead)
const TypeIcon = config.icon
```
**Step 2: Build to confirm the TS error from Task 5 is now resolved**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
```
Expected: Clean build (zero errors).
**Step 3: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
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 8: Redesign NodeFormDecision to use answer labels only (no NodePicker)
**Files:**
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
This is the biggest change in the plan. We replace the per-option NodePicker with a simple label-only input. The `next_node_id` field on each option is **preserved** in the data model but no longer set via the form — it gets wired up automatically in TreeCanvas when the user saves (Task 9).
**Step 1: Remove the NodePicker import**
Current line 3:
```tsx
import { NodePicker } from './NodePicker'
```
Remove this line entirely.
**Step 2: Simplify handleAddOption — set next_node_id to empty string (not required by user)**
The current `handleAddOption` (line 30–39) is fine as-is — it creates options with `next_node_id: ''`. Leave it unchanged.
**Step 3: Replace the options renderItem to show only the label input**
Find the `DynamicArrayField` renderItem (lines 156–209). Replace the entire `renderItem` prop with a simpler version:
```tsx
renderItem={(option, index) => {
const optionLabelError = validationErrors.find(
e => e.nodeId === node.id && e.field === `options[${index}].label`
)
const letter = indexToLetter(index)
return (
` wrapper from the old renderItem should also be removed — the new renderItem renders a flat row.
**Step 4: Remove the optionNextError validation lookup** (it's no longer displayed)
Find and remove:
```tsx
const optionNextError = validationErrors.find(
e => e.nodeId === node.id && e.field === `options[${index}].next_node_id`
)
```
**Step 5: Build**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
```
Expected: Clean build. If there's an unused import warning for `NodePicker` even after removal, double-check Step 1.
**Step 6: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
git commit -m "feat: redesign NodeFormDecision to use answer label list (no 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 9: Wire up answer stub creation and AnswerStubCard rendering in TreeCanvas
**Files:**
- Modify: `frontend/src/components/tree-editor/TreeCanvas.tsx`
Two changes: (1) when a decision node is saved, create answer stubs for any option without a `next_node_id`; (2) render `AnswerStubCard` for nodes with `type === 'answer'`.
**Step 1: Import AnswerStubCard and add handleSelectAnswerType**
At the top of the file, add the import after the existing `TreeCanvasNode` import:
```tsx
import { AnswerStubCard } from './AnswerStubCard'
```
**Step 2: Add a `handleSelectAnswerType` callback to the TreeCanvas component**
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 create answer stubs for unlinked options**
Find `handleSave` (around line 202). After the existing `updateNode(nodeId, updates)` call but before the pending link resolution, add answer stub creation logic:
The current `handleSave` starts:
```tsx
const handleSave = useCallback(
(nodeId: string, updates: Partial) => {
updateNode(nodeId, updates)
// Resolve pending link for new nodes
const link = pendingLinks.get(nodeId)
```
Change to:
```tsx
const handleSave = useCallback(
(nodeId: string, updates: Partial) => {
updateNode(nodeId, updates)
// For decision nodes: create answer stubs for any option without a next_node_id
if (updates.type === 'decision' || updates.options) {
const options = updates.options || []
options.forEach((opt) => {
if (!opt.next_node_id && opt.label.trim()) {
// Create a new answer stub node under this decision node
const stubId = addNode(nodeId, 'answer')
// Give it the label as its title so AnswerStubCard can display it
updateNode(stubId, { title: opt.label })
// Link the option to the stub
const updatedOptions = options.map((o) =>
o.id === opt.id ? { ...o, next_node_id: stubId } : o
)
updateNode(nodeId, { options: updatedOptions })
}
})
}
// Resolve pending link for new nodes
const link = pendingLinks.get(nodeId)
```
**Step 4: Add `handleSelectAnswerType` to the renderNode dependency array**
Find the `useCallback` dependency array at the end of `renderNode` (around line 580). Add `handleSelectAnswerType` to it:
```tsx
[
expandedNodeId,
newNodeIds,
dragOverTarget,
handleToggleExpand,
handleSave,
handleCancelNew,
handleDelete,
handleDuplicate,
handleDragStart,
handleDragOver,
handleDrop,
pendingAddKey,
handleAddNodeSelect,
handleSelectAnswerType, // ← add this
]
```
**Step 5: Render AnswerStubCard for answer-type nodes inside renderNode**
Find the section in `renderNode` where `` is rendered (around line 468). Add a conditional before it:
```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 /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -30
```
Expected: Clean build.
**Step 7: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
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 10: Update backend to allow `'answer'` type in drafts and block on publish
**Files:**
- Modify: `backend/app/core/tree_validation.py`
**Step 1: Allow `'answer'` type in `_validate_node` without structural validation**
Find the `else` branch at the end of `_validate_node` (around line 92–96):
```python
else:
errors.append({
"field": f"{path}.type",
"message": f"Unknown node type: {node_type}"
})
```
Change to:
```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 publish-time answer node check in `validate_tree_structure`**
After the root node is validated and before returning, add a recursive check for answer nodes.
Find the end of `validate_tree_structure` (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 3: Add the `_has_answer_nodes` helper function**
Add this function after `_validate_children` (around line 115):
```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 4: Verify the backend tests still pass**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/backend
source venv/bin/activate 2>/dev/null || true
pytest tests/ -k "tree_valid" --override-ini="addopts=" -q 2>&1 | tail -20
```
If no tests exist specifically for tree_validation, run the full suite:
```bash
pytest --override-ini="addopts=" -q 2>&1 | tail -20
```
Expected: All tests pass.
**Step 5: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
git add backend/app/core/tree_validation.py
git commit -m "feat: allow 'answer' type in tree drafts, block on publish
Draft saves succeed with answer placeholder nodes. Publish is blocked
with a clear message if any answer nodes remain unresolved.
Co-Authored-By: Claude Sonnet 4.6 "
```
---
### Task 11: Add frontend publish guard for answer nodes
**Files:**
- Modify: `frontend/src/pages/TreeEditorPage.tsx`
**Step 1: Add a `hasAnswerNodes` utility**
At the top of `TreeEditorPage.tsx`, after the imports, add a small utility function (before the component function):
```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)
}
```
You'll need to ensure `TreeStructure` is imported — it should already be imported via `@/types`.
**Step 2: Add the guard in `handlePublish`**
Find `handlePublish` (around line 269). After the name check (around line 293) and before `validate()`, add:
```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 /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
```
Expected: Clean build.
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
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 "
```
---
## Final Verification
### Task 12: Full build and manual test checklist
**Step 1: Run the full frontend build**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -10
```
Expected: `✓ built in Xs` with zero errors.
**Step 2: Run backend tests**
```bash
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/backend
pytest --override-ini="addopts=" -q 2>&1 | tail -10
```
Expected: All tests pass.
**Step 3: Manual test checklist (confirm with developer)**
1. Open a troubleshooting tree in the canvas editor
2. Click a decision node → card expands
3. Resize the browser to a short viewport — form should scroll, sticky header (save/cancel) stays visible
4. Hover over the `i` badge next to field labels — tooltip text appears
5. Type answer labels in the Options section (e.g. "Server", "Desktop") → click ✓ to save
6. Two dashed stub cards appear below the decision node labeled "Server" and "Desktop"
7. Click "Server" stub → three type buttons appear (Decision / Action / Solution)
8. Click "Decision" → stub converts to a full Decision card in expanded editing mode
9. Save draft → no backend error (answer nodes allowed in drafts)
10. Leave an unresolved stub and click Publish → blocked with: "Resolve all answer placeholders before publishing."
11. `npm run build` passes with no TypeScript errors
**Step 4: Complete the development branch**
Use `superpowers:finishing-a-development-branch` to present merge/PR options.