Files
resolutionflow/docs/plans/2026-02-18-canvas-ux-fixes-impl.md
chihlasm 94de29b5f2 feat: canvas UX fixes — scroll, fullscreen, InfoTip tooltips, answer stub system (#80)
* 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>
2026-02-18 12:52:08 -05:00

1004 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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):
```tsx
<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:
```tsx
<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):
```tsx
{isExpanded && (
<div className="border-t border-border px-3 pb-3 pt-3">
```
Change it to:
```tsx
{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**
```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 <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:
```tsx
<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 8993:
```tsx
{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 136143:
```tsx
{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:
```tsx
<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:
```tsx
<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**
```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 <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 9193:
```tsx
<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:
```tsx
// 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 124126:
```tsx
<p className="mb-2 text-xs text-muted-foreground">
PowerShell or CLI commands to execute
</p>
```
Change the Commands label + remove the hint:
```tsx
// 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**
```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 <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 8688:
```tsx
<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:
```tsx
// 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 118120:
```tsx
<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:
```tsx
// 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**
```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 <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:
```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 <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**
```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 (
<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**
```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 <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:
```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 <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:
```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 3039) 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 156209). 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 (
<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:
```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 <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:
```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<TreeStructure>) => {
updateNode(nodeId, updates)
// Resolve pending link for new nodes
const link = pendingLinks.get(nodeId)
```
Change to:
```tsx
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:
```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 `<TreeCanvasNode>` is rendered (around line 468). Add a conditional before it:
```tsx
{/* 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**
```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 <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 9296):
```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 5356):
```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 <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):
```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 <noreply@anthropic.com>"
```
---
## 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.