Files
resolutionflow/docs/plans/archive/2026-02-18-canvas-ux-fixes-impl.md
chihlasm 932927b9df chore: archive old plan docs + add survey foundation files
Move completed plan docs to docs/plans/archive/. Add survey migration 046
and reference HTML/plan files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:03:38 -05:00

30 KiB
Raw Permalink Blame History

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 8993:

{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:

{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 9193:

<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 124126:

<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 8688:

<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 118120:

<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 TreeCanvasNodeTreeCanvas 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 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:

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 9296):

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 5356):

    # 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)

  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.