* feat: Add TreeCanvasNode inline editor card component Replaces modal-based node editing with inline expand/collapse cards. Each card shows node type, title, and options in compact mode, then renders the full edit form inline on expand — no modal required. Local draft state with save/cancel prevents premature store writes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: Add TreeCanvas layout with visual branching and orchestration Replaces NodeList + TreePreviewPanel with a single full-width canvas. Decision nodes branch horizontally; action/solution nodes flow vertically. Inline type picker adds nodes without modal interruption. Handles pending link resolution, inbound reference cleanup on delete, and selection sync. CSS dot-grid background + connector lines for structure clarity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: Update forms for inline safety, add MetadataSidePanel, update layout - NodeFormDecision: option reorder via onUpdate (no premature store writes) - NodePicker: add allowCreate prop (default true) to hide Create New options during inline canvas editing, preventing side-effect node creation - MetadataSidePanel: 320px right slide-in overlay wrapping TreeMetadataForm, closes on backdrop click, close button, and Escape key - TreeEditorLayout: Flow mode now renders full-width TreeCanvas + MetadataSidePanel overlay; Code mode unchanged (Monaco + preview split) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: Wire toolbar metadata toggle and integrate canvas layout - Add isMetadataOpen state in TreeEditorPage - Add Metadata toolbar button (visible in Flow mode only) - Auto-close metadata panel when switching to Code mode - Pass isMetadataOpen/onCloseMetadata props through TreeEditorLayout - Update Flow mode toggle tooltip to reflect new canvas editing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add canvas UX fixes design doc (scroll, tooltips, answer stubs) Captures approved design for three post-implementation UX improvements to the tree canvas editor: card scroll fix, info tooltip replacement for hint text, and the new 'answer' node type for sketching decision branches before assigning types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add implementation plan for canvas UX fixes 12-task plan covering scroll fix, info tooltips, and answer stub node type. Each task has exact file paths, code, and build verification steps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: make canvas card expanded area scrollable with sticky header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add fullscreen toggle to Modal, enable in NodeEditorModal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add reusable InfoTip component for field-level help Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormDecision Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormAction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormResolution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add 'answer' to NodeType union for branch placeholder stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add AnswerStubCard component for unresolved branch placeholders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: guard NODE_TYPE_CONFIG lookup against 'answer' type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * feat: auto-create answer stubs on decision save, render AnswerStubCard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add answer type to all Record<NodeType> icon and color maps Fixes NodeList, ContinuationModal, NodePicker, and TreePreviewNode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: allow 'answer' type in tree drafts, block on publish Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: block publish if unresolved answer stub nodes exist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: serialize 'answer' stub nodes in markdown output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add defensive guard for answer nodes in session navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add delete button with confirmation to AnswerStubCard Adds an inline delete flow to answer stub placeholder cards: - Trash icon button (top-right, subtle) visible in idle state - Click reveals "Delete this stub?" confirmation with Delete/Cancel - Confirmed delete calls onDelete(nodeId) wired to handleDelete in TreeCanvas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent category Cancel overflow and add Tab/Enter to create options - TreeMetadataForm: add min-w-0 + shrink-0 to keep Cancel button in-panel - NodeFormDecision: Tab or Enter on the last non-empty option input adds a new option and auto-focuses it; empty last input lets Tab pass through normally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: re-sync draft from store when canvas card is opened When a decision node is saved with new options, stub next_node_id values are written back to the store. But the local draft was initialized once at mount and never refreshed, so reopening the card gave a stale draft with empty next_node_ids — causing duplicate stubs on every subsequent save. Fix: reset draft from the live node whenever isExpanded transitions to true. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix+feat: blank options, stub card dismiss, collapsible subtrees - TreeCanvas: strip blank-label options on save so they don't generate stubs; also filter them from the unlinked-option add-button list - AnswerStubCard: collapse type-picker when clicking outside the card - TreeCanvasNode: add subtree collapse toggle button (ChevronsDownUp icon) visible in compact mode when the node has children - TreeCanvas: track collapsedNodeIds; hide subtree behind a clickable "N nodes hidden" pill when collapsed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: stop connector fork line from overlapping child cards Replace the two-element approach (separate fork line + child lanes div with mismatched maxWidth values) with a single relative-positioned container. The fork line is absolutely positioned and its left/right are calculated from the number of children so it spans exactly from the center of the first lane to the center of the last lane. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace Show Drafts checkbox with Drafts tab in Flow Library - Remove the out-of-place checkbox; add 'Drafts' as a tab alongside All | Troubleshooting | Projects | Maintenance - Drafts tab sets showDrafts=true + typeFilter='all' so the API filter still works correctly via include_drafts - Move SortDropdown to the right side next to ViewToggle, so both secondary controls are grouped together Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
30 KiB
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 <p> 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 <div> 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):
<div
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'
)}
onClick={!isExpanded ? handleCardClick : undefined}
>
Change it to:
<div
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'
)}
onClick={!isExpanded ? handleCardClick : undefined}
>
Step 2: Make the expanded editing area scrollable
Find the expanded editing area div (around line 324):
{isExpanded && (
<div className="border-t border-border px-3 pb-3 pt-3">
Change it to:
{isExpanded && (
<div className="border-t border-border px-3 pb-3 pt-3 max-h-[70vh] overflow-y-auto">
Step 3: Build and verify no TypeScript errors
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
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 <noreply@anthropic.com>"
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:
<span
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
title="YOUR TOOLTIP TEXT HERE"
>
i
</span>
Step 1: Find the root node hint paragraph inside the Question field
Around line 89–93:
{isRootNode && (
<p className="mt-0.5 text-xs text-muted-foreground">
What's the main question to diagnose the issue?
</p>
)}
Remove this <p> block entirely. The input's placeholder already conveys the intent.
Step 2: Find the options hint paragraphs
Around lines 136–143:
{isRootNode ? (
<p className="mt-0.5 text-xs text-muted-foreground">
Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path.
</p>
) : (
<p className="mt-0.5 text-xs text-muted-foreground">
Each option can branch to a different next step.
</p>
)}
Replace both <p> tags with a ⓘ tooltip on the Options label. Change the label section (around line 133) from:
<label className="block text-sm font-medium text-foreground">
{isRootNode ? 'Answer Options (Branches)' : 'Options'} <span className="text-red-400">*</span>
</label>
{isRootNode ? (
<p className="mt-0.5 text-xs text-muted-foreground">
Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path.
</p>
) : (
<p className="mt-0.5 text-xs text-muted-foreground">
Each option can branch to a different next step.
</p>
)}
To:
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
{isRootNode ? 'Answer Options (Branches)' : 'Options'} <span className="text-red-400">*</span>
<span
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
title={isRootNode
? "Add as many options as needed (A, B, C, D...). Each option leads to a different troubleshooting path."
: "Each option can branch to a different next step."}
>
i
</span>
</label>
Step 3: Build to check for TS errors
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
Expected: Clean build.
Step 4: Commit
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 <noreply@anthropic.com>"
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:
<p className="mb-1 text-xs text-muted-foreground">
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
</p>
Change the Description label + remove the hint:
// Before
<label className="block text-sm font-medium text-foreground">
Description
</label>
<p className="mb-1 text-xs text-muted-foreground">
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
</p>
// After
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
Description
<span
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
title="Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`"
>
i
</span>
</label>
Step 2: Find the commands hint paragraph
Around lines 124–126:
<p className="mb-2 text-xs text-muted-foreground">
PowerShell or CLI commands to execute
</p>
Change the Commands label + remove the hint:
// Before
<label className="block text-sm font-medium text-foreground">
Commands
</label>
<p className="mb-2 text-xs text-muted-foreground">
PowerShell or CLI commands to execute
</p>
// After
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
Commands
<span
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
title="PowerShell or CLI commands to execute"
>
i
</span>
</label>
Step 3: Build to check for TS errors
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
Expected: Clean build.
Step 4: Commit
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 <noreply@anthropic.com>"
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:
<p className="mb-1 text-xs text-muted-foreground">
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
</p>
Change the Description label + remove the hint:
// Before
<label className="block text-sm font-medium text-foreground">
Description
</label>
<p className="mb-1 text-xs text-muted-foreground">
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
</p>
// After
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
Description
<span
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
title="Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`"
>
i
</span>
</label>
Step 2: Find the resolution steps hint paragraph
Around lines 118–120:
<p className="mb-2 text-xs text-muted-foreground">
Step-by-step instructions for resolving the issue
</p>
Change the Resolution Steps label + remove the hint:
// Before
<label className="block text-sm font-medium text-foreground">
Resolution Steps
</label>
<p className="mb-2 text-xs text-muted-foreground">
Step-by-step instructions for resolving the issue
</p>
// After
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
Resolution Steps
<span
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
title="Step-by-step instructions for resolving the issue"
>
i
</span>
</label>
Step 3: Build to check for TS errors
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
Expected: Clean build.
Step 4: Commit
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 <noreply@anthropic.com>"
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:
export type NodeType = 'decision' | 'action' | 'solution'
Change to:
export type NodeType = 'decision' | 'action' | 'solution' | 'answer'
Step 2: Check for TS exhaustiveness errors
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
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 <noreply@anthropic.com>"
Task 6: Create the AnswerStubCard component
Files:
- Create:
frontend/src/components/tree-editor/AnswerStubCard.tsx
Step 1: Create the file with the following content
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 (
<div
className={cn(
'min-w-[180px] max-w-[280px] rounded-xl border-2 border-dashed border-border bg-card/50',
'transition-all duration-150',
!picking && 'cursor-pointer hover:border-primary/40 hover:bg-accent/30'
)}
onClick={() => !picking && setPicking(true)}
>
{/* Label */}
<div className="px-3 pt-2.5 pb-1 text-sm font-heading font-medium text-foreground text-center">
{label}
</div>
{/* Prompt / type picker */}
{!picking ? (
<div className="pb-2.5 text-center text-[10px] text-muted-foreground font-label">
+ Choose Type
</div>
) : (
<div className="flex items-center justify-center gap-1.5 pb-2.5 px-2">
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onSelectType(node.id, 'decision')
}}
className={cn(
'flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-label',
'border border-blue-500/30 bg-blue-500/10 text-blue-400 hover:bg-blue-500/20'
)}
>
<HelpCircle className="h-2.5 w-2.5" />
Decision
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onSelectType(node.id, 'action')
}}
className={cn(
'flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-label',
'border border-yellow-500/30 bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20'
)}
>
<Zap className="h-2.5 w-2.5" />
Action
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onSelectType(node.id, 'solution')
}}
className={cn(
'flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-label',
'border border-green-500/30 bg-green-500/10 text-green-400 hover:bg-green-500/20'
)}
>
<CheckCircle className="h-2.5 w-2.5" />
Solution
</button>
</div>
)}
</div>
)
}
export default AnswerStubCard
Step 2: Build to check for TS errors in the new file only
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
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 <noreply@anthropic.com>"
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:
const config = NODE_TYPE_CONFIG[node.type]
const TypeIcon = config.icon
Change to:
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
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
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 <noreply@anthropic.com>"
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:
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:
renderItem={(option, index) => {
const optionLabelError = validationErrors.find(
e => e.nodeId === node.id && e.field === `options[${index}].label`
)
const letter = indexToLetter(index)
return (
<div className="flex items-center gap-2">
{/* Letter badge */}
<span className={cn(
'flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold',
isRootNode
? 'bg-blue-500/20 text-blue-400'
: 'bg-accent text-muted-foreground'
)}>
{letter}
</span>
<input
type="text"
value={option.label}
onChange={(e) => handleUpdateOption(index, { label: e.target.value })}
placeholder={isRootNode
? `Branch ${letter}: e.g., "Network Issues"...`
: `Option ${letter} label`}
className={cn(
'block flex-1 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 && (
<p className="mt-1 text-xs text-red-400">{optionLabelError.message}</p>
)}
</div>
)
}}
Note: The surrounding <div className="rounded-md border border-border bg-accent/50 p-3"> 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:
const optionNextError = validationErrors.find(
e => e.nodeId === node.id && e.field === `options[${index}].next_node_id`
)
Step 5: Build
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
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 <noreply@anthropic.com>"
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:
import { AnswerStubCard } from './AnswerStubCard'
Step 2: Add a handleSelectAnswerType callback to the TreeCanvas component
After the handleDuplicate callback (around line 278), add:
// ── 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:
const handleSave = useCallback(
(nodeId: string, updates: Partial<TreeStructure>) => {
updateNode(nodeId, updates)
// Resolve pending link for new nodes
const link = pendingLinks.get(nodeId)
Change to:
const handleSave = useCallback(
(nodeId: string, updates: Partial<TreeStructure>) => {
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:
[
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 <TreeCanvasNode> is rendered (around line 468). Add a conditional before it:
{/* The node card — answer stubs get their own component */}
{node.type === 'answer' ? (
<AnswerStubCard
node={node}
fromOption={optionLabel}
onSelectType={handleSelectAnswerType}
/>
) : (
<TreeCanvasNode
node={node}
depth={0}
fromOption={optionLabel}
isExpanded={isExpanded}
isNew={isNew}
onToggleExpand={() => 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
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -30
Expected: Clean build.
Step 7: Commit
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 <noreply@anthropic.com>"
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):
else:
errors.append({
"field": f"{path}.type",
"message": f"Unknown node type: {node_type}"
})
Change to:
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):
# 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:
# 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):
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
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:
pytest --override-ini="addopts=" -q 2>&1 | tail -20
Expected: All tests pass.
Step 5: Commit
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 <noreply@anthropic.com>"
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):
/** 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:
// 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
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
npm run build 2>&1 | tail -20
Expected: Clean build.
Step 4: Commit
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 <noreply@anthropic.com>"
Final Verification
Task 12: Full build and manual test checklist
Step 1: Run the full frontend build
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
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)
- Open a troubleshooting tree in the canvas editor
- Click a decision node → card expands
- Resize the browser to a short viewport — form should scroll, sticky header (save/cancel) stays visible
- Hover over the
ibadge next to field labels — tooltip text appears - Type answer labels in the Options section (e.g. "Server", "Desktop") → click ✓ to save
- Two dashed stub cards appear below the decision node labeled "Server" and "Desktop"
- Click "Server" stub → three type buttons appear (Decision / Action / Solution)
- Click "Decision" → stub converts to a full Decision card in expanded editing mode
- Save draft → no backend error (answer nodes allowed in drafts)
- Leave an unresolved stub and click Publish → blocked with: "Resolve all answer placeholders before publishing."
npm run buildpasses with no TypeScript errors
Step 4: Complete the development branch
Use superpowers:finishing-a-development-branch to present merge/PR options.