Files
resolutionflow/docs/plans/2026-02-25-tree-fork-ui.md
chihlasm e6a0c0549b feat: Step Library sync + service account for default tree ownership
* feat: maintenance flow UX redesign — batch status hub, context strip, detail page upgrades (#85)

- Add BatchStatusPage (/flows/:id/batches/:batchId): per-target Start/Resume/View cards, progress bar, 5s polling while in-progress, completion outcome summary
- Add BatchStatusCard: handles not-started/in-progress/complete states with step progress for in-progress targets
- Add ActiveBatchBanner: amber banner on detail page when a batch is running, links to BatchStatusPage
- Add MaintenanceContextStrip: amber strip in ProceduralNavigationPage for maintenance flows showing target name, batch progress (X/Y complete), and Back to Batch nav
- Update MaintenanceFlowDetailPage: active batch banner, clickable run history rows with mini progress dots and outcome summaries, Run button loading state, post-launch navigates to BatchStatusPage
- Update ProceduralNavigationPage: renders MaintenanceContextStrip between top bar and content when tree_type === 'maintenance'; fetches batch progress once on mount
- Add batch_id filter to GET /sessions backend endpoint and SessionListParams frontend type
- Add /flows/:id/batches/:batchId route to router

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: session detail page — completion action + outcome summary card

- In-progress sessions: amber banner with "Complete Session" button opens
  SessionOutcomeModal to set outcome/notes/next-steps and finalize
- Completed sessions: colored outcome summary card (icon + outcome label +
  duration + notes + next steps) replaces dense header metadata; "Copy for
  Ticket" promoted to primary action inside the card
- Export toolbar de-emphasized to secondary row of smaller controls below
  the summary card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add library-page action props to StepCard (edit/delete/save)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: pass library-page action props through StepLibraryBrowser + refreshKey

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: StepFormModal wrapper + submitLabel/isSubmitting props on StepForm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: Step Library page — create, edit, delete, save-to-library

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add RuntimeStep union type for procedural custom steps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: StepChecklist accepts RuntimeStep[], renders amber Custom badge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: StepDetail accepts RuntimeStep, renders Custom Step badge for custom steps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: custom step insertion in procedural flow sessions

Engineers can add custom steps inline during execution. Steps are
persisted to session.custom_steps and restored on resume.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: suppress StepFeedback on custom steps, fix resume stepState seeding, functional updater for step index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add tree forking UI design doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add tree fork UI implementation plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add ForkInfo type and fork fields to Tree/TreeListItem

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: align ForkInfo type with backend schema, remove redundant fork fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: ForkInfo placement, required fork_info field, add JSDoc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add ForkModal component with name and reason fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: ForkModal accessibility and UX (escape, click-outside, labels, maxLength)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: open ForkModal on fork action in TreeLibraryPage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add ForkModal to MyTreesPage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: show Fork chip badge on forked tree cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add flow-to-library step sync design doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add flow-to-library sync implementation plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add sync tracking columns to step_library

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add sync columns and source_tree relationship to StepLibrary model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add group_label to StepContent, is_flow_synced/source_tree_name to StepLibraryResponse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: include is_flow_synced and source_tree_name in step list/detail responses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add is_flow_synced and source_tree_name to step list response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add selectinload and sync fields to search and get_step endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add step_sync module with extraction and upsert logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: safe NOT IN placeholders for asyncpg, add deactivate docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: trigger step library sync on tree publish and deactivate on delete

- Call sync_steps_from_tree in update_tree whenever the tree is published
  (status transitions to 'published' or is already published and structure changes)
- Call deactivate_synced_steps_for_tree in delete_tree before db.commit()
  so the FK SET NULL does not nullify source_tree_id before the WHERE clause runs
- Fix ::jsonb cast syntax in step_sync.py (asyncpg rejects :: operator in text()
  queries; replaced with CAST(:content AS jsonb))
- Add UniqueConstraint('source_tree_id','source_node_id') to StepLibrary model
  so Base.metadata.create_all (used by tests) creates the constraint that the
  ON CONFLICT clause in sync_steps_from_tree depends on

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add is_flow_synced and source_tree_name to Step types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: show From Flow badge and lock icon on flow-synced StepCard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: show source flow name in StepDetailModal for synced steps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add Library Visibility select to procedural StepEditor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address code review issues in flow-to-library sync

- Fix sync trigger: only fire on publish transition, not every PUT
- Add TestSyncOnPublish integration tests (2 tests, 16 total passing)
- Add group_label to frontend StepContent interface
- Guard Library Visibility select to procedure_step nodes only
- Block API edits to flow-synced steps (400 read-only guard)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: handle None author_id in step sync to avoid invalid UUID error

When a system/default tree has no author (author_id is None),
str(None) produces the literal string 'None' which asyncpg
rejects as an invalid UUID for the created_by column.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add ResolutionFlow service account to own default tree steps in library

Default/system trees had no author_id (NULL), causing a NOT NULL violation
when syncing steps to step_library.created_by on publish.

- Add is_service_account flag to users table (migration 4f4137ce)
- Add service_account.py: idempotent ensure_service_account() creates
  noreply@resolutionflow.com with unusable password on startup
- Cache service account ID on app.state at lifespan startup
- Add get_service_account_id() FastAPI dep (returns None in tests)
- sync_steps_from_tree: resolve author_id or service_account_id as created_by
- create_tree: set author_id=service_account_id for is_default trees
- Migration 1490781700bc: backfill author_id on 31 existing default trees

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 23:17:29 -05:00

16 KiB
Raw 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.