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>
16 KiB
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 atfrontend/src/api/trees.ts:42— already accepts{ fork_reason?, name? }onForkTreeprop exists on all three card views and currently passes onlytreeId: stringTreeLibraryPagehashandleForkTree(treeId: string)at line ~247 that callstreesApi.fork(treeId)silentlyMyTreesPagedoes NOT currently have a fork handler — the "Fork" UI there is an informational message (line ~215), not a button wired toonForkTreeTreeListItem(used by all three views) does NOT yet havefork_depthorparent_tree_id— must add theseMyTreesPagealready usestree.parent_tree_idat 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-400for the Fork chip;bg-gradient-brandfor the Fork submit button; modal structure usesbg-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:
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):
fork_info?: ForkInfo | null
parent_tree_id?: string | null
fork_depth?: number
Add to TreeListItem interface (after visibility field):
fork_depth?: number
parent_tree_id?: string | null
Step 2: Verify TypeScript compiles cleanly
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
Expected: Clean build, no errors.
Step 3: Commit
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 <noreply@anthropic.com>"
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:
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<string | null>(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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
<div className="w-full max-w-md rounded-xl border border-border bg-card shadow-xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-5 py-4">
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold text-foreground">Fork Flow</h2>
</div>
<button
onClick={onClose}
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
</div>
{/* Body */}
<form onSubmit={handleSubmit} className="space-y-4 px-5 py-4">
<div>
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
Name <span className="text-red-400">*</span>
</label>
<input
type="text"
value={name}
onChange={(e) => 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'
)}
/>
</div>
<div>
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
Reason for Forking{' '}
<span className="text-muted-foreground/60">(optional)</span>
</label>
<textarea
value={forkReason}
onChange={(e) => setForkReason(e.target.value)}
rows={3}
placeholder="e.g. customizing for a specific client…"
className={cn(
'w-full resize-none 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'
)}
/>
</div>
{error && (
<p className="text-xs text-red-400">{error}</p>
)}
{/* Footer */}
<div className="flex justify-end gap-2 pt-1">
<button
type="button"
onClick={onClose}
className="rounded-lg border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting || !name.trim()}
className="bg-gradient-brand flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90 disabled:opacity-40"
>
{isSubmitting ? 'Forking…' : 'Fork Flow'}
</button>
</div>
</form>
</div>
</div>
)
}
Step 2: Verify TypeScript compiles cleanly
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
Expected: Clean build, no errors.
Step 3: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/library/ForkModal.tsx
git commit -m "feat: add ForkModal component with name and reason fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Task 3: Update TreeLibraryPage to open ForkModal
Files:
- Modify:
frontend/src/pages/TreeLibraryPage.tsx
The current handleForkTree at ~line 247 calls treesApi.fork(treeId) silently. Replace it with state that opens ForkModal.
Step 1: Add import for ForkModal and TreeListItem
At the top of TreeLibraryPage.tsx, the file already imports TreeListItem from @/types. Add ForkModal to the library component imports. Find the line that imports from @/components/library/... and add:
import { ForkModal } from '@/components/library/ForkModal'
Step 2: Replace fork state
Find (around line 76):
// Fork state
const [isForkingTree, setIsForkingTree] = useState(false)
Replace with:
// Fork modal state
const [forkTarget, setForkTarget] = useState<TreeListItem | null>(null)
Step 3: Replace handleForkTree
Find (around line 247):
const handleForkTree = async (treeId: string) => {
if (isForkingTree) return
setIsForkingTree(true)
try {
await treesApi.fork(treeId)
toast.success('Flow forked successfully')
navigate('/my-trees')
} catch (err) {
console.error('Failed to fork flow:', err)
toast.error('Failed to fork flow')
} finally {
setIsForkingTree(false)
}
}
Replace with:
const handleForkTree = (treeId: string) => {
const tree = trees.find((t) => t.id === treeId)
if (tree) setForkTarget(tree)
}
Note: trees is the existing state variable holding the fetched tree list. If the variable is named differently in context, use the correct name.
Step 4: Add ForkModal to JSX
Find the closing </div> of the page's root element (near the end of the return statement, after all the other modals like FolderEditModal, ConfirmDialog). Add before the root closing tag:
{forkTarget && (
<ForkModal
treeId={forkTarget.id}
treeName={forkTarget.name}
onClose={() => setForkTarget(null)}
/>
)}
Step 5: Verify TypeScript compiles cleanly
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
Expected: Clean build, no errors. If there are unused import errors for treesApi (if it was only used by the old handleForkTree), check whether treesApi is still used elsewhere on the page; if not, remove it from imports.
Step 6: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/pages/TreeLibraryPage.tsx
git commit -m "feat: open ForkModal on fork action in TreeLibraryPage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Task 4: Update MyTreesPage to open ForkModal
Files:
- Modify:
frontend/src/pages/MyTreesPage.tsx
MyTreesPage does NOT currently pass onForkTree to any view components — the page is a custom hand-rolled list, not using the three card views. The fork action is not wired here. However, tree.parent_tree_id is already rendered (line ~283), so we just need to add a ForkModal trigger for any fork buttons that may be present.
Step 1: Read the MyTreesPage fork section carefully
Read lines 200–300 of frontend/src/pages/MyTreesPage.tsx to understand the exact current fork UI and whether there's a fork button.
sed -n '200,300p' /home/michaelchihlas/dev/patherly/frontend/src/pages/MyTreesPage.tsx
Step 2: Add import for ForkModal
Add to the imports:
import { ForkModal } from '@/components/library/ForkModal'
Step 3: Add fork modal state
Find the state declarations section. Add:
const [forkTarget, setForkTarget] = useState<TreeListItem | null>(null)
Step 4: Add a "Fork" button to each tree row (if not already present)
In the tree list rendering, find the action buttons area for each tree (look for the edit/delete buttons). Add a Fork button next to them:
<button
type="button"
onClick={() => setForkTarget(tree)}
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
title="Fork flow"
>
<GitBranch className="h-4 w-4" />
</button>
Note: GitBranch is already imported in MyTreesPage (line 3).
Step 5: Add ForkModal to JSX
Find the end of the return statement. Before the root closing tag, add:
{forkTarget && (
<ForkModal
treeId={forkTarget.id}
treeName={forkTarget.name}
onClose={() => setForkTarget(null)}
/>
)}
Step 6: Verify TypeScript compiles cleanly
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
Expected: Clean build, no errors.
Step 7: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/pages/MyTreesPage.tsx
git commit -m "feat: add ForkModal to MyTreesPage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Task 5: Add "Fork" chip badge to all three card views
Files:
- Modify:
frontend/src/components/library/TreeGridView.tsx - Modify:
frontend/src/components/library/TreeListView.tsx - Modify:
frontend/src/components/library/TreeTableView.tsx
Show a small violet chip when tree.fork_depth > 0 (or tree.parent_tree_id is set). Place it near the tree name or alongside other metadata chips.
Step 1: Add Fork chip to TreeGridView
Read frontend/src/components/library/TreeGridView.tsx lines 60–100 to find where tree name and category badge are rendered.
In the name/header area of each card (near where tree.category_info chip is rendered), add:
{(tree.fork_depth ?? 0) > 0 && (
<span className="shrink-0 rounded-full bg-violet-400/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-violet-400">
Fork
</span>
)}
Place this chip alongside or just after the tree name <span>, or next to the category badge — wherever fits the card layout (read the file to confirm exact placement).
Step 2: Add Fork chip to TreeListView
Read frontend/src/components/library/TreeListView.tsx lines 60–130 to find the name + metadata row.
Add the same chip in the same relative position:
{(tree.fork_depth ?? 0) > 0 && (
<span className="shrink-0 rounded-full bg-violet-400/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-violet-400">
Fork
</span>
)}
Step 3: Add Fork chip to TreeTableView
Read frontend/src/components/library/TreeTableView.tsx lines 80–150 to find the name column cell.
Add the same chip inline after the tree name in the name column:
{(tree.fork_depth ?? 0) > 0 && (
<span className="shrink-0 rounded-full bg-violet-400/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-violet-400">
Fork
</span>
)}
Step 4: Verify TypeScript compiles cleanly
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
Expected: Clean build, no errors.
Step 5: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/library/TreeGridView.tsx \
frontend/src/components/library/TreeListView.tsx \
frontend/src/components/library/TreeTableView.tsx
git commit -m "feat: show Fork chip badge on forked tree cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Manual Verification Checklist
After all tasks are complete:
-
Fork flow (Library): Go to Flow Library → click GitBranch icon on any published flow →
ForkModalopens with name pre-filled as "Copy of " → enter a reason → click "Fork Flow" → toast appears → redirected to My Trees. -
Fork flow (My Trees): Go to My Trees → find a flow → click Fork button → same modal + behavior.
-
Fork badge: Fork a flow → go to My Trees → forked flow shows violet "Fork" chip in card header.
-
Badge in Library views: In Flow Library, switch to grid/list/table view — forked flows (your own) show "Fork" chip.
-
Reason is optional: Fork a flow without entering a reason → still works.
-
Cancel: Open ForkModal → click Cancel → modal closes, nothing forked.