Files
resolutionflow/docs/plans/archive/2026-02-25-tree-fork-ui.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

16 KiB
Raw Permalink Blame History

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:

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 200300 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 60100 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 60130 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 80150 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:

  1. Fork flow (Library): Go to Flow Library → click GitBranch icon on any published flow → ForkModal opens with name pre-filled as "Copy of " → enter a reason → click "Fork Flow" → toast appears → redirected to My Trees.

  2. Fork flow (My Trees): Go to My Trees → find a flow → click Fork button → same modal + behavior.

  3. Fork badge: Fork a flow → go to My Trees → forked flow shows violet "Fork" chip in card header.

  4. Badge in Library views: In Flow Library, switch to grid/list/table view — forked flows (your own) show "Fork" chip.

  5. Reason is optional: Fork a flow without entering a reason → still works.

  6. Cancel: Open ForkModal → click Cancel → modal closes, nothing forked.