diff --git a/docs/plans/2026-02-25-tree-fork-ui.md b/docs/plans/2026-02-25-tree-fork-ui.md new file mode 100644 index 00000000..42eb3f5f --- /dev/null +++ b/docs/plans/2026-02-25-tree-fork-ui.md @@ -0,0 +1,493 @@ +# Tree Fork UI Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add an explicit `ForkModal` with a "Reason for Forking" field to replace the silent fork flow, and show a "Fork" chip badge on forked tree cards in the library and My Trees views. + +**Architecture:** The backend is fully complete (POST `/trees/:id/fork` accepts `{ name, fork_reason }`). The frontend `treesApi.fork()` already accepts these params. We need: (1) `ForkInfo` types added to `tree.ts`, (2) a new `ForkModal` component, (3) updated fork handlers in `TreeLibraryPage` and `MyTreesPage` to open the modal instead of forking silently, (4) a "Fork" chip in all three card views (grid, list, table). + +**Tech Stack:** React 19, TypeScript, Tailwind CSS, Lucide React, `treesApi.fork(id, { name, fork_reason })` already wired. + +--- + +## Context for the Implementer + +- `treesApi.fork(id, data?)` is at `frontend/src/api/trees.ts:42` — already accepts `{ fork_reason?, name? }` +- `onForkTree` prop exists on all three card views and currently passes only `treeId: string` +- `TreeLibraryPage` has `handleForkTree(treeId: string)` at line ~247 that calls `treesApi.fork(treeId)` silently +- `MyTreesPage` does NOT currently have a fork handler — the "Fork" UI there is an informational message (line ~215), not a button wired to `onForkTree` +- `TreeListItem` (used by all three views) does NOT yet have `fork_depth` or `parent_tree_id` — must add these +- `MyTreesPage` already uses `tree.parent_tree_id` at line ~283 for a "Forked from" display block — this field must be on the type for that to compile cleanly after our changes +- All three card views are in `frontend/src/components/library/` +- Design system: `bg-violet-400/15 text-violet-400` for the Fork chip; `bg-gradient-brand` for the Fork submit button; modal structure uses `bg-card border-border rounded-xl` + +--- + +### Task 1: Add `ForkInfo` type and fork fields to `TreeListItem` and `Tree` + +**Files:** +- Modify: `frontend/src/types/tree.ts:142-190` + +This is a pure type change — no runtime behavior changes. + +**Step 1: Add `ForkInfo` interface and fork fields** + +In `frontend/src/types/tree.ts`, after line 141 (the `ProceduralTreeStructure` closing brace), add `ForkInfo` then update `Tree` and `TreeListItem`: + +```typescript +export interface ForkInfo { + parent_tree_id: string + parent_tree_name: string | null + fork_depth: number + fork_reason: string | null + has_parent_updates: boolean +} +``` + +Add to `Tree` interface (after `usage_count: number`): +```typescript + fork_info?: ForkInfo | null + parent_tree_id?: string | null + fork_depth?: number +``` + +Add to `TreeListItem` interface (after `visibility` field): +```typescript + fork_depth?: number + parent_tree_id?: string | null +``` + +**Step 2: Verify TypeScript compiles cleanly** + +```bash +cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20 +``` + +Expected: Clean build, no errors. + +**Step 3: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly +git add frontend/src/types/tree.ts +git commit -m "feat: add ForkInfo type and fork fields to Tree/TreeListItem + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 2: Create `ForkModal` component + +**Files:** +- Create: `frontend/src/components/library/ForkModal.tsx` + +**Step 1: Create the component file** + +Create `frontend/src/components/library/ForkModal.tsx` with this exact content: + +```tsx +import { useState } from 'react' +import { GitBranch, X } from 'lucide-react' +import { treesApi } from '@/api/trees' +import { toast } from '@/lib/toast' +import { cn } from '@/lib/utils' +import { useNavigate } from 'react-router-dom' + +interface ForkModalProps { + treeId: string + treeName: string + onClose: () => void +} + +export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) { + const navigate = useNavigate() + const [name, setName] = useState(`Copy of ${treeName}`) + const [forkReason, setForkReason] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim()) return + setIsSubmitting(true) + setError(null) + try { + await treesApi.fork(treeId, { + name: name.trim(), + fork_reason: forkReason.trim() || undefined, + }) + toast.success('Flow forked successfully') + onClose() + navigate('/my-trees') + } catch (err) { + console.error('Failed to fork flow:', err) + setError('Failed to fork flow. Please try again.') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ {/* Header */} +
+
+ +

Fork Flow

+
+ +
+ + {/* Body */} +
+
+ + setName(e.target.value)} + required + autoFocus + className={cn( + 'w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground', + 'placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20' + )} + /> +
+ +
+ +