diff --git a/backend/app/core/tree_validation.py b/backend/app/core/tree_validation.py index de562889..a0c8652d 100644 --- a/backend/app/core/tree_validation.py +++ b/backend/app/core/tree_validation.py @@ -53,6 +53,13 @@ def validate_tree_structure(tree_structure: dict[str, Any]) -> tuple[bool, list[ 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 @@ -89,6 +96,10 @@ def _validate_node(node: dict[str, Any], path: str, errors: list[dict[str, str]] "message": "Solution nodes must have a non-empty solution" }) + elif node_type == "answer": + # Answer nodes are draft-only placeholders — no structural validation needed + pass + else: errors.append({ "field": f"{path}.type", @@ -115,6 +126,16 @@ def _validate_children(children: list[dict[str, Any]], path: str, errors: list[d _validate_children(child["children"], f"{child_path}.children", errors) +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 + + # --- Procedural Tree Validation --- VALID_STEP_TYPES = {"procedure_step", "procedure_end", "section_header"} diff --git a/docs/plans/2026-02-18-canvas-ux-fixes-design.md b/docs/plans/2026-02-18-canvas-ux-fixes-design.md new file mode 100644 index 00000000..faa93698 --- /dev/null +++ b/docs/plans/2026-02-18-canvas-ux-fixes-design.md @@ -0,0 +1,258 @@ +# Canvas UX Fixes — Design Document + +**Date:** 2026-02-18 +**Branch:** `feature/tree-editor-canvas` +**Status:** Approved, pending implementation + +--- + +## Context + +The new TreeCanvas editor (Phase 1–4) was tested and three UX problems were identified: + +1. **Scroll**: Expanded card forms have no height limit — long forms are cut off and unreachable +2. **Busy forms**: Inline hint text (`

`) inside NodeForm components creates visual clutter +3. **Answer stubs**: When building a decision node, users must immediately pick a child node type — there's no way to sketch out answer options first and decide types later + +All three fixes apply exclusively to the canvas editor. No session, navigation, backend session-saving, or procedural flow code is affected. + +--- + +## Fix 1: Card Scroll + +### Problem + +`TreeCanvasNode.tsx` renders the expanded editing area as an unbounded `

`. On decision nodes with many options, or on any node when the browser viewport is short, the card overflows off-screen. There is no scrollbar — content is unreachable. Tab cycling doesn't scroll the canvas to bring hidden fields into view. + +### Design + +Apply `max-h-[70vh] overflow-y-auto` to the expanded editing `
` inside `TreeCanvasNode.tsx`. + +Make the save/cancel header row sticky (`sticky top-0 z-10 bg-card`) so the action buttons are always visible when the user scrolls the form content. + +**Files changed:** +- `frontend/src/components/tree-editor/TreeCanvasNode.tsx` + - Add `max-h-[70vh] overflow-y-auto` to the expanded area `
` (currently `border-t border-border px-3 pb-3 pt-3`) + - Add `sticky top-0 z-10 bg-card` to the card header `
` containing the save/cancel row when in expanded state + +**No other files affected.** + +--- + +## Fix 2: Info Tooltips + +### Problem + +`NodeFormDecision.tsx`, `NodeFormAction.tsx`, and `NodeFormResolution.tsx` each contain `

` hint paragraphs below field labels. These add vertical height and visual noise inside a card that's already compact. + +Examples of the current hint text: +- "Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, \`code\`" +- "PowerShell or CLI commands to execute" +- "Step-by-step instructions for resolving the issue" + +### Design + +Replace each hint `

` with a small `ⓘ` icon placed inline next to the field label. The icon shows a tooltip on hover containing the same text. + +**Tooltip implementation:** + +Use `title=""` on the icon element for a native browser tooltip. No third-party tooltip library needed — keeps the implementation minimal and consistent with the existing codebase pattern (the validation badge already uses `title={nodeErrors.map(...).join('\n')}`). + +```tsx +// Before + +

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

+ +// After + +``` + +**Files changed:** +- `frontend/src/components/tree-editor/NodeFormDecision.tsx` — remove help_text hint `

`, replace with `ⓘ` on `Help Text` label +- `frontend/src/components/tree-editor/NodeFormAction.tsx` — remove description markdown hint `

` and commands hint `

`, add `ⓘ` on those labels +- `frontend/src/components/tree-editor/NodeFormResolution.tsx` — remove description markdown hint `

` and steps hint `

`, add `ⓘ` on those labels + +--- + +## Fix 3: Answer Stubs (New `answer` Node Type) + +### Problem + +Decision nodes require the user to pick child node types at the same time they're creating the decision. This is backwards — you naturally know the answer options before you know what each one should do. The NodePicker in NodeFormDecision forces a concrete type selection (decision / action / solution) or leaves the option disconnected (`next_node_id: null`). + +Users want to type answer labels first, see those answers appear as placeholder cards in the canvas, and then click each placeholder to assign a type and fill in details. + +### Design + +Introduce `'answer'` as a new internal NodeType that represents a typed-but-unresolved branch placeholder. Answer nodes are: +- Created when a user types an answer label in the decision node form +- Shown in the canvas as a dashed-border stub card with the answer label +- Clickable to open a type picker (decision / action / solution) and convert to a real node +- **Not publishable** — blocked by backend and frontend validation on publish + +Answer nodes persist to draft saves so users don't lose their sketch when they navigate away. + +### Data Model + +**`frontend/src/types/tree.ts`** + +```typescript +// Before +export type NodeType = 'decision' | 'action' | 'solution' + +// After +export type NodeType = 'decision' | 'action' | 'solution' | 'answer' +``` + +`TreeStructure` interface: no new fields needed. Answer nodes use: +- `id`: auto-generated UUID (same as other nodes) +- `type`: `'answer'` +- `title`: the answer label text (e.g. "Server", "Desktop") +- No other fields required + +### NodeFormDecision Redesign + +Replace the current options UI (NodePicker per option → picks existing or creates new) with a two-zone layout: + +**Zone 1 — Answer Labels** +A simple list of text inputs, one per answer option. Each input edits `options[i].label`. Add/remove/reorder via `DynamicArrayField` (already available). + +No `next_node_id` selection here. When the user saves, for each option that has a label but no `next_node_id`, a new answer-type stub node is created and linked automatically. + +``` +Options (answer labels): +[ Server ] [×] +[ Desktop ] [×] +[ + Add Answer ] +``` + +**Zone 2 — (removed) NodePicker per option** +The per-option NodePicker is removed entirely. The canvas becomes the way to traverse to a child and set its type. + +### TreeCanvas Changes + +**Rendering answer nodes:** + +When a node has `type === 'answer'`, render an `AnswerStubCard` instead of a full `TreeCanvasNode`. The stub card: +- Dashed border: `border-2 border-dashed border-border` +- Colored left accent: none (neutral/muted) +- Shows the answer label (`node.title`) centered +- Shows a "+ Choose Type" label below the title +- On click: opens an inline type picker (3 buttons: Decision / Action / Solution) +- On type selection: calls `updateNode(node.id, { type: selectedType })` and immediately expands the node for editing + +**New component:** `frontend/src/components/tree-editor/AnswerStubCard.tsx` + +Props: +```typescript +interface AnswerStubCardProps { + node: TreeStructure // type === 'answer' + fromOption?: string // the answer label (same as node.title) + onSelectType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void +} +``` + +### Stub Creation Logic (TreeCanvas) + +When a decision node is saved (`onSave`), the canvas compares options before/after: + +For each option in the saved node: +- If `option.next_node_id` is null/undefined → create a new answer stub node with `title = option.label` and link `option.next_node_id` to its ID. +- If `option.next_node_id` already points to a node → leave it. + +This creation logic lives in `TreeCanvas.tsx`'s `handleNodeSave()` function, which already handles pending link resolution. + +### Backend Validation + +**`backend/app/core/tree_validation.py`** + +`_validate_node()` currently rejects unknown node types: +```python +if node_type not in ('decision', 'action', 'solution'): + errors.append(...) +``` + +Changes: +1. Allow `'answer'` type through without structural validation (answer nodes have no required fields beyond `id` and `type`). +2. Add a publish-time check in `can_publish_tree()` (or in `validate_tree_structure()` before publish): if any node has `type == 'answer'`, reject with a clear message: `"Answer placeholders must be resolved to a node type before publishing."` +3. The draft save endpoint (`PUT /trees/:id`) does not call `can_publish_tree()`, so draft saves continue to work with answer nodes present. + +### Frontend Publish Guard + +In `TreeEditorPage.tsx`, before calling the publish API, add a check: +```typescript +const hasAnswerNodes = findAllAnswerNodes(tree.tree_structure).length > 0 +if (hasAnswerNodes) { + // Show toast or inline error: "Resolve all answer placeholders before publishing." + return +} +``` + +`findAllAnswerNodes` is a simple recursive traversal (can be a small utility function in `TreeCanvas.tsx` or a new file `lib/treeUtils.ts`). + +### Visual Design (AnswerStubCard) + +``` +┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ + Server + [ ? Decision ] [ ⚡ Action ] [ ✓ Solution ] +└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +``` + +- Card: `min-w-[180px] max-w-[280px] rounded-xl border-2 border-dashed border-border bg-card/50` +- Title: `text-sm font-heading font-medium text-foreground text-center py-2 px-3` +- Type picker row (default state — not yet clicked): `text-xs text-muted-foreground text-center pb-2 cursor-pointer hover:text-foreground` + - Clicking the card reveals three compact buttons for type selection +- Type picker (expanded): three small buttons side-by-side in the card footer + +--- + +## Files Changed Summary + +| File | Change | +|------|--------| +| `frontend/src/components/tree-editor/TreeCanvasNode.tsx` | Fix 1: max-h + overflow-y + sticky header | +| `frontend/src/components/tree-editor/NodeFormDecision.tsx` | Fix 2: ⓘ tooltip on help_text; Fix 3: replace NodePicker with answer label list | +| `frontend/src/components/tree-editor/NodeFormAction.tsx` | Fix 2: ⓘ tooltips on description + commands fields | +| `frontend/src/components/tree-editor/NodeFormResolution.tsx` | Fix 2: ⓘ tooltips on description + steps fields | +| `frontend/src/components/tree-editor/TreeCanvas.tsx` | Fix 3: stub creation in handleNodeSave; AnswerStubCard rendering | +| `frontend/src/components/tree-editor/AnswerStubCard.tsx` | Fix 3: NEW — dashed stub card with inline type picker | +| `frontend/src/types/tree.ts` | Fix 3: add `'answer'` to NodeType union | +| `backend/app/core/tree_validation.py` | Fix 3: allow 'answer' in draft; block on publish | +| `frontend/src/pages/TreeEditorPage.tsx` | Fix 3: frontend publish guard | + +--- + +## Non-Goals + +- No changes to session navigation, procedural flows, or maintenance flows +- No changes to the Code mode editor +- No changes to `treeEditorStore.ts` store actions (addNode, updateNode, deleteNode are used as-is) +- No third-party tooltip library +- No new backend endpoints + +--- + +## Verification + +1. Open a troubleshooting tree in the canvas editor +2. Click a decision node → card expands, form is scrollable with sticky save/cancel header +3. Field labels show `ⓘ` icons; hovering reveals the hint text +4. Type answer labels in the Options section; click ✓ to save +5. Answer stub cards appear as dashed cards below the decision node +6. Click a stub card → type picker appears; select "Decision" → card converts and expands for editing +7. Draft save works with answer nodes present (no backend error) +8. Attempt to publish with unresolved answer nodes → blocked with a clear error message +9. `npm run build` passes with no TypeScript errors diff --git a/docs/plans/2026-02-18-canvas-ux-fixes-impl.md b/docs/plans/2026-02-18-canvas-ux-fixes-impl.md new file mode 100644 index 00000000..a070a5bd --- /dev/null +++ b/docs/plans/2026-02-18-canvas-ux-fixes-impl.md @@ -0,0 +1,1003 @@ +# Canvas UX Fixes Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Fix three UX problems in the TreeCanvas editor: card scroll, noisy hint text, and forced child-type selection when building decision nodes. + +**Architecture:** Three independent fixes applied to the canvas editor components only. Fix 1 is a pure CSS change. Fix 2 replaces `

` hint text with native `title` tooltips on ⓘ badges. Fix 3 introduces a new `'answer'` NodeType — a branch placeholder that the user converts to a real type by clicking it. + +**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (existing `treeEditorStore`), FastAPI backend (`tree_validation.py`) + +**Working directory:** `/home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas` + +--- + +## Fix 1: Card Scroll + +### Task 1: Make expanded card area scrollable with sticky header + +**Files:** +- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx:164-321` + +**Step 1: Open the file and locate the card header div (expanded state)** + +The card header is the `

` at line 165. When expanded it shows the action buttons (save/cancel/etc). We need this row to be sticky. + +Find this block (around line 165): +```tsx +
+``` + +Change it to: +```tsx +
+``` + +**Step 2: Make the expanded editing area scrollable** + +Find the expanded editing area div (around line 324): +```tsx +{isExpanded && ( +
+``` + +Change it to: +```tsx +{isExpanded && ( +
+``` + +**Step 3: Build and verify no TypeScript errors** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -20 +``` + +Expected: Build exits with code 0, no errors mentioning TreeCanvasNode. + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/TreeCanvasNode.tsx +git commit -m "fix: make canvas card expanded area scrollable with sticky header + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Fix 2: Info Tooltips + +### Task 2: Replace hint text in NodeFormDecision + +**Files:** +- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx` + +The ⓘ badge pattern to use throughout Fix 2: +```tsx + + i + +``` + +**Step 1: Find the root node hint paragraph inside the Question field** + +Around line 89–93: +```tsx +{isRootNode && ( +

+ What's the main question to diagnose the issue? +

+)} +``` + +Remove this `

` block entirely. The input's placeholder already conveys the intent. + +**Step 2: Find the options hint paragraphs** + +Around lines 136–143: +```tsx +{isRootNode ? ( +

+ Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path. +

+) : ( +

+ Each option can branch to a different next step. +

+)} +``` + +Replace both `

` tags with a ⓘ tooltip on the Options label. Change the label section (around line 133) from: +```tsx + +{isRootNode ? ( +

+ Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path. +

+) : ( +

+ Each option can branch to a different next step. +

+)} +``` + +To: +```tsx + +``` + +**Step 3: Build to check for TS errors** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -20 +``` + +Expected: Clean build. + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/NodeFormDecision.tsx +git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormDecision + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 3: Replace hint text in NodeFormAction + +**Files:** +- Modify: `frontend/src/components/tree-editor/NodeFormAction.tsx` + +**Step 1: Find the description hint paragraph** + +Around lines 91–93: +```tsx +

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

+``` + +Change the Description label + remove the hint: +```tsx +// Before + +

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

+ +// After + +``` + +**Step 2: Find the commands hint paragraph** + +Around lines 124–126: +```tsx +

+ PowerShell or CLI commands to execute +

+``` + +Change the Commands label + remove the hint: +```tsx +// Before + +

+ PowerShell or CLI commands to execute +

+ +// After + +``` + +**Step 3: Build to check for TS errors** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -20 +``` + +Expected: Clean build. + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/NodeFormAction.tsx +git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormAction + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 4: Replace hint text in NodeFormResolution + +**Files:** +- Modify: `frontend/src/components/tree-editor/NodeFormResolution.tsx` + +**Step 1: Find the description hint paragraph** + +Around lines 86–88: +```tsx +

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

+``` + +Change the Description label + remove the hint: +```tsx +// Before + +

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

+ +// After + +``` + +**Step 2: Find the resolution steps hint paragraph** + +Around lines 118–120: +```tsx +

+ Step-by-step instructions for resolving the issue +

+``` + +Change the Resolution Steps label + remove the hint: +```tsx +// Before + +

+ Step-by-step instructions for resolving the issue +

+ +// After + +``` + +**Step 3: Build to check for TS errors** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -20 +``` + +Expected: Clean build. + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/NodeFormResolution.tsx +git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormResolution + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Fix 3: Answer Stubs + +### Task 5: Add `'answer'` to the NodeType union + +**Files:** +- Modify: `frontend/src/types/tree.ts:4` + +**Step 1: Add `'answer'` to NodeType** + +Current line 4: +```typescript +export type NodeType = 'decision' | 'action' | 'solution' +``` + +Change to: +```typescript +export type NodeType = 'decision' | 'action' | 'solution' | 'answer' +``` + +**Step 2: Check for TS exhaustiveness errors** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | grep -E "error TS|Type.*answer" +``` + +The build will likely show an error in `TreeCanvasNode.tsx` because `NODE_TYPE_CONFIG` only has keys for `decision`, `action`, `solution` — and `config = NODE_TYPE_CONFIG[node.type]` will fail when `node.type === 'answer'`. We fix that in Task 7. For now note the exact error and proceed. + +**Step 3: Commit the type change** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/types/tree.ts +git commit -m "feat: add 'answer' to NodeType union for branch placeholder stubs + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 6: Create the AnswerStubCard component + +**Files:** +- Create: `frontend/src/components/tree-editor/AnswerStubCard.tsx` + +**Step 1: Create the file with the following content** + +```tsx +import { useState } from 'react' +import { HelpCircle, Zap, CheckCircle } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { TreeStructure } from '@/types' + +interface AnswerStubCardProps { + node: TreeStructure // type === 'answer' + fromOption?: string + onSelectType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void +} + +export function AnswerStubCard({ node, fromOption, onSelectType }: AnswerStubCardProps) { + const [picking, setPicking] = useState(false) + const label = fromOption || node.title || 'Answer' + + return ( +
!picking && setPicking(true)} + > + {/* Label */} +
+ {label} +
+ + {/* Prompt / type picker */} + {!picking ? ( +
+ + Choose Type +
+ ) : ( +
+ + + + + +
+ )} +
+ ) +} + +export default AnswerStubCard +``` + +**Step 2: Build to check for TS errors in the new file only** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | grep "AnswerStubCard" +``` + +Expected: No errors mentioning AnswerStubCard. + +**Step 3: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/AnswerStubCard.tsx +git commit -m "feat: add AnswerStubCard component for unresolved branch placeholders + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 7: Update TreeCanvasNode to handle `'answer'` type + +**Files:** +- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx` + +The `NODE_TYPE_CONFIG` object (line 47) only has entries for `decision`, `action`, `solution`. When `node.type === 'answer'`, calling `NODE_TYPE_CONFIG[node.type]` will cause a TypeScript error and runtime crash. + +The fix: guard `config` access so answer nodes get a safe fallback. However, answer nodes should **never** be rendered by `TreeCanvasNode` — `TreeCanvas` will render them as `AnswerStubCard` instead. We still need to fix the TypeScript error. + +**Step 1: Guard the config lookup** + +Find around line 135: +```tsx +const config = NODE_TYPE_CONFIG[node.type] +const TypeIcon = config.icon +``` + +Change to: +```tsx +const config = node.type in NODE_TYPE_CONFIG + ? NODE_TYPE_CONFIG[node.type as keyof typeof NODE_TYPE_CONFIG] + : NODE_TYPE_CONFIG.decision // fallback for 'answer' type (should be rendered by AnswerStubCard instead) +const TypeIcon = config.icon +``` + +**Step 2: Build to confirm the TS error from Task 5 is now resolved** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -20 +``` + +Expected: Clean build (zero errors). + +**Step 3: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/TreeCanvasNode.tsx +git commit -m "fix: guard NODE_TYPE_CONFIG lookup against 'answer' type + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 8: Redesign NodeFormDecision to use answer labels only (no NodePicker) + +**Files:** +- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx` + +This is the biggest change in the plan. We replace the per-option NodePicker with a simple label-only input. The `next_node_id` field on each option is **preserved** in the data model but no longer set via the form — it gets wired up automatically in TreeCanvas when the user saves (Task 9). + +**Step 1: Remove the NodePicker import** + +Current line 3: +```tsx +import { NodePicker } from './NodePicker' +``` + +Remove this line entirely. + +**Step 2: Simplify handleAddOption — set next_node_id to empty string (not required by user)** + +The current `handleAddOption` (line 30–39) is fine as-is — it creates options with `next_node_id: ''`. Leave it unchanged. + +**Step 3: Replace the options renderItem to show only the label input** + +Find the `DynamicArrayField` renderItem (lines 156–209). Replace the entire `renderItem` prop with a simpler version: + +```tsx +renderItem={(option, index) => { + const optionLabelError = validationErrors.find( + e => e.nodeId === node.id && e.field === `options[${index}].label` + ) + const letter = indexToLetter(index) + + return ( +
+ {/* Letter badge */} + + {letter} + + 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 && ( +

{optionLabelError.message}

+ )} +
+ ) +}} +``` + +Note: The surrounding `
` wrapper from the old renderItem should also be removed — the new renderItem renders a flat row. + +**Step 4: Remove the optionNextError validation lookup** (it's no longer displayed) + +Find and remove: +```tsx +const optionNextError = validationErrors.find( + e => e.nodeId === node.id && e.field === `options[${index}].next_node_id` +) +``` + +**Step 5: Build** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -20 +``` + +Expected: Clean build. If there's an unused import warning for `NodePicker` even after removal, double-check Step 1. + +**Step 6: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/NodeFormDecision.tsx +git commit -m "feat: redesign NodeFormDecision to use answer label list (no NodePicker) + +Users now type answer labels only. Stub nodes are created automatically +by TreeCanvas when the decision node is saved. + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 9: Wire up answer stub creation and AnswerStubCard rendering in TreeCanvas + +**Files:** +- Modify: `frontend/src/components/tree-editor/TreeCanvas.tsx` + +Two changes: (1) when a decision node is saved, create answer stubs for any option without a `next_node_id`; (2) render `AnswerStubCard` for nodes with `type === 'answer'`. + +**Step 1: Import AnswerStubCard and add handleSelectAnswerType** + +At the top of the file, add the import after the existing `TreeCanvasNode` import: +```tsx +import { AnswerStubCard } from './AnswerStubCard' +``` + +**Step 2: Add a `handleSelectAnswerType` callback to the TreeCanvas component** + +After the `handleDuplicate` callback (around line 278), add: + +```tsx +// ── Convert answer stub to a real node type ── +const handleSelectAnswerType = useCallback( + (nodeId: string, type: 'decision' | 'action' | 'solution') => { + updateNode(nodeId, { type }) + setExpandedNodeId(nodeId) + selectNode(nodeId) + }, + [updateNode, selectNode] +) +``` + +**Step 3: Update handleSave to create answer stubs for unlinked options** + +Find `handleSave` (around line 202). After the existing `updateNode(nodeId, updates)` call but before the pending link resolution, add answer stub creation logic: + +The current `handleSave` starts: +```tsx +const handleSave = useCallback( + (nodeId: string, updates: Partial) => { + updateNode(nodeId, updates) + + // Resolve pending link for new nodes + const link = pendingLinks.get(nodeId) +``` + +Change to: +```tsx +const handleSave = useCallback( + (nodeId: string, updates: Partial) => { + updateNode(nodeId, updates) + + // For decision nodes: create answer stubs for any option without a next_node_id + if (updates.type === 'decision' || updates.options) { + const options = updates.options || [] + options.forEach((opt) => { + if (!opt.next_node_id && opt.label.trim()) { + // Create a new answer stub node under this decision node + const stubId = addNode(nodeId, 'answer') + // Give it the label as its title so AnswerStubCard can display it + updateNode(stubId, { title: opt.label }) + // Link the option to the stub + const updatedOptions = options.map((o) => + o.id === opt.id ? { ...o, next_node_id: stubId } : o + ) + updateNode(nodeId, { options: updatedOptions }) + } + }) + } + + // Resolve pending link for new nodes + const link = pendingLinks.get(nodeId) +``` + +**Step 4: Add `handleSelectAnswerType` to the renderNode dependency array** + +Find the `useCallback` dependency array at the end of `renderNode` (around line 580). Add `handleSelectAnswerType` to it: + +```tsx +[ + expandedNodeId, + newNodeIds, + dragOverTarget, + handleToggleExpand, + handleSave, + handleCancelNew, + handleDelete, + handleDuplicate, + handleDragStart, + handleDragOver, + handleDrop, + pendingAddKey, + handleAddNodeSelect, + handleSelectAnswerType, // ← add this +] +``` + +**Step 5: Render AnswerStubCard for answer-type nodes inside renderNode** + +Find the section in `renderNode` where `` is rendered (around line 468). Add a conditional before it: + +```tsx +{/* The node card — answer stubs get their own component */} +{node.type === 'answer' ? ( + +) : ( + handleToggleExpand(node.id)} + onSave={handleSave} + onCancelNew={handleCancelNew} + onDelete={handleDelete} + onDuplicate={handleDuplicate} + onDragStart={handleDragStart} + onDragOver={(e) => handleDragOver(e, parentId, index)} + onDrop={(e) => handleDrop(e, parentId, index)} + /> +)} +``` + +**Step 6: Build** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -30 +``` + +Expected: Clean build. + +**Step 7: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/components/tree-editor/TreeCanvas.tsx +git commit -m "feat: auto-create answer stubs on decision save, render AnswerStubCard + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 10: Update backend to allow `'answer'` type in drafts and block on publish + +**Files:** +- Modify: `backend/app/core/tree_validation.py` + +**Step 1: Allow `'answer'` type in `_validate_node` without structural validation** + +Find the `else` branch at the end of `_validate_node` (around line 92–96): +```python +else: + errors.append({ + "field": f"{path}.type", + "message": f"Unknown node type: {node_type}" + }) +``` + +Change to: +```python +elif node_type == "answer": + # Answer nodes are draft-only placeholders — no structural validation needed + pass +else: + errors.append({ + "field": f"{path}.type", + "message": f"Unknown node type: {node_type}" + }) +``` + +**Step 2: Add publish-time answer node check in `validate_tree_structure`** + +After the root node is validated and before returning, add a recursive check for answer nodes. + +Find the end of `validate_tree_structure` (around line 53–56): +```python + # Validate all child nodes recursively + if "children" in tree_structure: + _validate_children(tree_structure["children"], "root.children", errors) + + return len(errors) == 0, errors +``` + +Change to: +```python + # Validate all child nodes recursively + if "children" in tree_structure: + _validate_children(tree_structure["children"], "root.children", errors) + + # Block publish if any answer placeholder nodes remain + if _has_answer_nodes(tree_structure): + errors.append({ + "field": "tree_structure", + "message": "Answer placeholders must be resolved to a node type before publishing." + }) + + return len(errors) == 0, errors +``` + +**Step 3: Add the `_has_answer_nodes` helper function** + +Add this function after `_validate_children` (around line 115): + +```python +def _has_answer_nodes(node: dict[str, Any]) -> bool: + """Recursively check if any node in the tree has type 'answer'.""" + if node.get("type") == "answer": + return True + for child in node.get("children", []): + if _has_answer_nodes(child): + return True + return False +``` + +**Step 4: Verify the backend tests still pass** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/backend +source venv/bin/activate 2>/dev/null || true +pytest tests/ -k "tree_valid" --override-ini="addopts=" -q 2>&1 | tail -20 +``` + +If no tests exist specifically for tree_validation, run the full suite: +```bash +pytest --override-ini="addopts=" -q 2>&1 | tail -20 +``` + +Expected: All tests pass. + +**Step 5: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add backend/app/core/tree_validation.py +git commit -m "feat: allow 'answer' type in tree drafts, block on publish + +Draft saves succeed with answer placeholder nodes. Publish is blocked +with a clear message if any answer nodes remain unresolved. + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 11: Add frontend publish guard for answer nodes + +**Files:** +- Modify: `frontend/src/pages/TreeEditorPage.tsx` + +**Step 1: Add a `hasAnswerNodes` utility** + +At the top of `TreeEditorPage.tsx`, after the imports, add a small utility function (before the component function): + +```typescript +/** Recursively check if any node in the tree has type 'answer' */ +function hasAnswerNodes(node: TreeStructure): boolean { + if (node.type === 'answer') return true + return (node.children || []).some(hasAnswerNodes) +} +``` + +You'll need to ensure `TreeStructure` is imported — it should already be imported via `@/types`. + +**Step 2: Add the guard in `handlePublish`** + +Find `handlePublish` (around line 269). After the name check (around line 293) and before `validate()`, add: + +```typescript +// Block publish if any answer placeholder nodes remain +const currentStructure = useTreeEditorStore.getState().treeStructure +if (currentStructure && hasAnswerNodes(currentStructure)) { + toast.error('Resolve all answer placeholders before publishing. Click each dashed stub card to assign a type.') + setSaving(false) + return +} +``` + +**Step 3: Build** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -20 +``` + +Expected: Clean build. + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas +git add frontend/src/pages/TreeEditorPage.tsx +git commit -m "feat: block publish if unresolved answer stub nodes exist + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Final Verification + +### Task 12: Full build and manual test checklist + +**Step 1: Run the full frontend build** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend +npm run build 2>&1 | tail -10 +``` + +Expected: `✓ built in Xs` with zero errors. + +**Step 2: Run backend tests** + +```bash +cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/backend +pytest --override-ini="addopts=" -q 2>&1 | tail -10 +``` + +Expected: All tests pass. + +**Step 3: Manual test checklist (confirm with developer)** + +1. Open a troubleshooting tree in the canvas editor +2. Click a decision node → card expands +3. Resize the browser to a short viewport — form should scroll, sticky header (save/cancel) stays visible +4. Hover over the `i` badge next to field labels — tooltip text appears +5. Type answer labels in the Options section (e.g. "Server", "Desktop") → click ✓ to save +6. Two dashed stub cards appear below the decision node labeled "Server" and "Desktop" +7. Click "Server" stub → three type buttons appear (Decision / Action / Solution) +8. Click "Decision" → stub converts to a full Decision card in expanded editing mode +9. Save draft → no backend error (answer nodes allowed in drafts) +10. Leave an unresolved stub and click Publish → blocked with: "Resolve all answer placeholders before publishing." +11. `npm run build` passes with no TypeScript errors + +**Step 4: Complete the development branch** + +Use `superpowers:finishing-a-development-branch` to present merge/PR options. diff --git a/frontend/src/components/common/InfoTip.tsx b/frontend/src/components/common/InfoTip.tsx new file mode 100644 index 00000000..ccaf448e --- /dev/null +++ b/frontend/src/components/common/InfoTip.tsx @@ -0,0 +1,14 @@ +interface InfoTipProps { + text: string +} + +export function InfoTip({ text }: InfoTipProps) { + return ( + + i + + ) +} diff --git a/frontend/src/components/common/Modal.tsx b/frontend/src/components/common/Modal.tsx index 73cf54ad..f798e4bb 100644 --- a/frontend/src/components/common/Modal.tsx +++ b/frontend/src/components/common/Modal.tsx @@ -1,5 +1,5 @@ -import { useEffect, useCallback, type ReactNode } from 'react' -import { X } from 'lucide-react' +import { useState, useEffect, useCallback, type ReactNode } from 'react' +import { X, Maximize2, Minimize2 } from 'lucide-react' import { cn } from '@/lib/utils' interface ModalProps { @@ -10,9 +10,28 @@ interface ModalProps { /** Optional footer content that stays fixed at bottom (doesn't scroll) */ footer?: ReactNode size?: 'sm' | 'md' | 'lg' | 'xl' + /** If true, a fullscreen toggle button appears in the modal header */ + allowFullScreen?: boolean } -export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }: ModalProps) { +export function Modal({ isOpen, onClose, title, children, footer, size = 'md', allowFullScreen = false }: ModalProps) { + const [isFullScreen, setIsFullScreen] = useState(() => { + if (!allowFullScreen) return false + try { + return localStorage.getItem('rf-editor-fullscreen') === 'true' + } catch { + return false + } + }) + + const toggleFullScreen = () => { + const next = !isFullScreen + setIsFullScreen(next) + try { + localStorage.setItem('rf-editor-fullscreen', String(next)) + } catch {} + } + // Close on Escape key const handleKeyDown = useCallback( (e: KeyboardEvent) => { @@ -61,9 +80,13 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
{/* Header - Fixed at top */} @@ -71,17 +94,32 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }: - )} - aria-label="Close modal" - > - - + +
{/* Body - Scrollable */} diff --git a/frontend/src/components/session/ContinuationModal.tsx b/frontend/src/components/session/ContinuationModal.tsx index b6b4a8f0..3e594890 100644 --- a/frontend/src/components/session/ContinuationModal.tsx +++ b/frontend/src/components/session/ContinuationModal.tsx @@ -21,13 +21,15 @@ interface ContinuationModalProps { const nodeTypeIcons: Record = { decision: , action: , - solution: + solution: , + answer: } const nodeTypeLabels: Record = { decision: 'Decision', action: 'Action', - solution: 'Solution' + solution: 'Solution', + answer: 'Answer' } export function ContinuationModal({ diff --git a/frontend/src/components/tree-editor/AnswerStubCard.tsx b/frontend/src/components/tree-editor/AnswerStubCard.tsx new file mode 100644 index 00000000..277e8aea --- /dev/null +++ b/frontend/src/components/tree-editor/AnswerStubCard.tsx @@ -0,0 +1,121 @@ +import { useState, useRef, useEffect } from 'react' +import { HelpCircle, Zap, CheckCircle, Trash2 } 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 + onDelete: (nodeId: string) => void +} + +export function AnswerStubCard({ node, fromOption, onSelectType, onDelete }: AnswerStubCardProps) { + const [picking, setPicking] = useState(false) + const [confirming, setConfirming] = useState(false) + const cardRef = useRef(null) + const label = fromOption || node.title || 'Answer' + + // Collapse picker when clicking outside the card + useEffect(() => { + if (!picking) return + const handleOutsideClick = (e: MouseEvent) => { + if (cardRef.current && !cardRef.current.contains(e.target as Node)) { + setPicking(false) + } + } + document.addEventListener('mousedown', handleOutsideClick) + return () => document.removeEventListener('mousedown', handleOutsideClick) + }, [picking]) + + return ( +
!picking && !confirming && setPicking(true)} + > + {/* Delete button — top-right corner */} + {!picking && !confirming && ( + + )} + + {/* Label */} +
+ {label} +
+ + {/* Confirm delete */} + {confirming ? ( +
+

Delete this stub?

+
+ + +
+
+ ) : !picking ? ( +
+ + Choose Type +
+ ) : ( +
+ + + +
+ )} +
+ ) +} + +export default AnswerStubCard diff --git a/frontend/src/components/tree-editor/MetadataSidePanel.tsx b/frontend/src/components/tree-editor/MetadataSidePanel.tsx new file mode 100644 index 00000000..25ebafb3 --- /dev/null +++ b/frontend/src/components/tree-editor/MetadataSidePanel.tsx @@ -0,0 +1,66 @@ +import { useEffect } from 'react' +import { X } from 'lucide-react' +import { TreeMetadataForm } from './TreeMetadataForm' + +interface MetadataSidePanelProps { + isOpen: boolean + onClose: () => void +} + +export function MetadataSidePanel({ isOpen, onClose }: MetadataSidePanelProps) { + // Close on Escape key + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose() + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [isOpen, onClose]) + + if (!isOpen) return null + + return ( + <> + {/* Backdrop — click to close */} +