docs: add Step Library and Procedural Custom Steps design/plan docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-26 08:09:12 -05:00
parent 1f77b7fc32
commit 9e57468a9f
4 changed files with 1636 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
# Procedural Custom Steps — Design
> **Date:** February 24, 2026
> **Status:** Approved
> **Phase:** 2.5 — Feature 2 of 3
---
## What We're Building
Add "Add Custom Step" support to `ProceduralNavigationPage` so engineers can insert ad-hoc steps between any existing checklist items during execution. The inserted step appears inline in the checklist and detail panel — worked through just like a regular step before proceeding.
---
## Key Difference from Troubleshooting Custom Steps
`useCustomStepFlow` is built for tree-graph navigation: `currentNodeId`, `findNode`, `setCurrentNodeId`, path traversal, continuation modal for picking descendants, custom branch mode. **None of that applies here.**
Procedural flows are linear arrays. A custom step is just a new `ProceduralStep`-shaped object injected at a position in the array. No new hook needed — all state lives in `ProceduralNavigationPage`.
---
## Data Model
Custom steps are stored in session `custom_steps` (already a JSONB array on `Session`). The existing `CustomStep` type is:
```ts
interface CustomStep {
id: string
inserted_after_node_id: string // step ID it was inserted after
step_data: Step | CustomStepDraft
timestamp: string
}
```
For procedural flows, `inserted_after_node_id` is the `ProceduralStep.id` of the step it follows.
A custom step is represented in the runtime `ProceduralStep[]` as:
```ts
{
id: customStep.id, // the CustomStep UUID
type: 'procedure_step',
title: step_data.title,
description: step_data.content.instructions,
content_type: 'action',
commands: step_data.content.commands (mapped),
// marker for custom steps:
_isCustom: true // not in ProceduralStep type — use a local union
}
```
Rather than mutating `ProceduralStep`, we use a local discriminated union:
```ts
type RuntimeStep = ProceduralStep | CustomProceduralStep
interface CustomProceduralStep {
id: string
type: 'procedure_step'
title: string
description?: string
content_type: 'action'
commands?: CommandBlock[]
isCustom: true // discriminant
}
```
The `procedureSteps` array used for rendering becomes `RuntimeStep[]` instead of `ProceduralStep[]`.
---
## User Flow
1. Engineer is on any step in a procedural flow
2. Clicks **"+ Add Step"** button below the current step detail
3. `CustomStepModal` opens (existing component — Create tab + Browse Library tab)
4. Engineer creates or selects a step → `PostStepActionModal` appears
5. **"Use Now"**: inserts after current step, closes modals, advances to the new step
6. **"Save for Later"**: saves to library only, no insertion
7. **"Do Both"**: saves to library + inserts
The inserted step renders in `StepDetail` and `StepChecklist` with a visual "Custom" badge. The engineer marks it complete the same way as any other step (the "Mark Complete" button).
No continuation modal. No custom branch mode. No fork flow.
---
## What Changes
### ProceduralNavigationPage.tsx
**New state:**
```ts
const [runtimeSteps, setRuntimeSteps] = useState<RuntimeStep[]>([])
const [showCustomStepModal, setShowCustomStepModal] = useState(false)
const [showPostStepModal, setShowPostStepModal] = useState(false)
const [pendingCustomStep, setPendingCustomStep] = useState<Step | CustomStepDraft | null>(null)
const [pendingIsFromLibrary, setPendingIsFromLibrary] = useState(false)
const [isSavingStep, setIsSavingStep] = useState(false)
const [sessionCustomSteps, setSessionCustomSteps] = useState<CustomStep[]>([])
```
**`runtimeSteps`** is initialized from `tree.tree_structure.steps` and updated when custom steps are inserted. `procedureSteps` (currently `steps.filter(s => s.type === 'procedure_step')`) becomes `runtimeSteps.filter(...)`.
**`handleInsertCustomStep(step, isFromLibrary)`:**
- Builds a `CustomProceduralStep` from the draft/step
- Inserts it into `runtimeSteps` after `currentStepIndex`
- Adds a `CustomStep` entry to `sessionCustomSteps`
- Calls `sessionsApi.update(session.id, { custom_steps: newCustomSteps })`
- Advances `currentStepIndex` by 1 (focus moves to the new step)
**`handleStepCreated(step, isFromLibrary)`:** — same pattern as troubleshooting
- Sets `pendingCustomStep`, `pendingIsFromLibrary`
- Closes `CustomStepModal`, opens `PostStepActionModal`
**Add `handleSaveForLater`, `handleUseNow`, `handleBoth`** — same logic as `useCustomStepFlow` but without path/node navigation.
**"+ Add Step" button:** Renders in the right panel below `StepDetail`, above `StepFeedback`. Only shown on the current active (incomplete) step. Not shown on already-completed steps or on custom steps (no nesting).
**Session resume:** On `resumeSession`, initialize `runtimeSteps` from the tree steps then inject custom steps from `sessionData.custom_steps` at the correct positions.
### StepChecklist.tsx
Accept `RuntimeStep[]` instead of `ProceduralStep[]`. Render a small "Custom" badge (amber dot or label) next to custom step titles.
### StepDetail.tsx
Accept `RuntimeStep` instead of `ProceduralStep`. When `step.isCustom === true`, render slightly differently: no `content_type` badge (use a plain "Custom Step" label instead), show instructions as description, render commands if present using the existing command block renderer.
### New type: `RuntimeStep`
Add to `frontend/src/types/tree.ts` (or a separate `procedural.ts`):
```ts
export interface CustomProceduralStep {
id: string
type: 'procedure_step'
title: string
description?: string
content_type: 'action'
commands?: CommandBlock[]
isCustom: true
}
export type RuntimeStep = ProceduralStep | CustomProceduralStep
```
---
## What Does NOT Change
- `useCustomStepFlow` — not used at all in procedural flows
- `ContinuationModal` — not used (no branching)
- Fork flow — not used
- `CustomStepModal` — reused as-is (already works for both create and browse)
- `PostStepActionModal` — reused as-is
- `sessionsApi.update` with `custom_steps` — already supports this
- Backend — no changes needed
---
## Checklist render (sidebar)
Custom steps show in the checklist with an amber `✦` or small "Custom" chip:
```
✅ 1. Check service status
✅ 2. Restart broker agent
3. [✦ Custom] Verify VDA re-registration ← custom step
4. Check event log
```
---
## Completion behavior
When the engineer marks the last custom step complete and it was inserted before the last regular step, `currentStepIndex` increments normally — they continue with the remaining regular steps. The total step count in the progress bar updates when custom steps are inserted.

View File

@@ -0,0 +1,578 @@
# Procedural Custom Steps — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add "Add Custom Step" support to `ProceduralNavigationPage` so engineers can insert ad-hoc steps between existing checklist items during a session.
**Architecture:** Introduce a `RuntimeStep = ProceduralStep | CustomProceduralStep` discriminated union type. `ProceduralNavigationPage` grows new state + handlers for the custom-step modal flow (reusing `CustomStepModal` and `PostStepActionModal` as-is). `StepChecklist` and `StepDetail` accept `RuntimeStep` instead of `ProceduralStep`. No backend changes needed.
**Tech Stack:** React 19, TypeScript, Tailwind CSS, existing `sessionsApi.update`, `CustomStepModal`, `PostStepActionModal`
---
### Task 1: Add `RuntimeStep` union type
**Files:**
- Modify: `frontend/src/types/tree.ts`
- Modify: `frontend/src/types/index.ts` (re-export if needed)
**Context:** `ProceduralStep` is already in `tree.ts` along with `CommandBlock`. The new `CustomProceduralStep` interface lives in the same file. No hook needed — this is purely a type.
**Step 1: Add the types at the bottom of `frontend/src/types/tree.ts`**
After the existing `ProceduralStep` interface (line ~124), add:
```ts
export interface CustomProceduralStep {
id: string
type: 'procedure_step'
title: string
description?: string
content_type: 'action'
commands?: CommandBlock[]
isCustom: true
}
export type RuntimeStep = ProceduralStep | CustomProceduralStep
```
**Step 2: Export from `frontend/src/types/index.ts`**
Check what is currently re-exported from `tree.ts` in `index.ts` and add `CustomProceduralStep` and `RuntimeStep` to the export list.
Run: `cd frontend && npm run build 2>&1 | tail -20`
Expected: no new type errors
**Step 3: Commit**
```bash
git add frontend/src/types/tree.ts frontend/src/types/index.ts
git commit -m "feat: add RuntimeStep union type for procedural custom steps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
---
### Task 2: Update `StepChecklist` to accept `RuntimeStep[]`
**Files:**
- Modify: `frontend/src/components/procedural/StepChecklist.tsx`
**Context:** Currently `StepChecklistProps.steps` is `ProceduralStep[]`. It filters internally for `type === 'procedure_step'`. Custom steps already have `type: 'procedure_step'` so the filter still works. We need to:
1. Change prop type to `RuntimeStep[]`
2. Render an amber "Custom" badge next to custom step titles
**Step 1: Update the import and prop type**
Change:
```tsx
import type { ProceduralStep } from '@/types'
```
To:
```tsx
import type { RuntimeStep } from '@/types'
```
Change `StepChecklistProps`:
```tsx
interface StepChecklistProps {
steps: RuntimeStep[]
currentStepIndex: number
completedStepIds: Set<string>
onStepClick: (index: number) => void
}
```
**Step 2: Add the "Custom" badge in the step row**
In the button's `<span className="min-w-0 flex-1 truncate">` row, add a badge after the title when `'isCustom' in step && step.isCustom`:
```tsx
<span className="min-w-0 flex-1 flex items-center gap-1.5 truncate">
<span className="truncate">{step.title || 'Untitled step'}</span>
{'isCustom' in step && step.isCustom && (
<span className="shrink-0 rounded-full bg-amber-400/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-400">
Custom
</span>
)}
</span>
```
**Step 3: Build check**
Run: `cd frontend && npm run build 2>&1 | tail -20`
Expected: no new errors
**Step 4: Commit**
```bash
git add frontend/src/components/procedural/StepChecklist.tsx
git commit -m "feat: StepChecklist accepts RuntimeStep[], renders amber Custom badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
---
### Task 3: Update `StepDetail` to accept `RuntimeStep`
**Files:**
- Modify: `frontend/src/components/procedural/StepDetail.tsx`
**Context:** `StepDetail` currently only accepts `ProceduralStep`. For `CustomProceduralStep`, we skip the `content_type` badge (show "Custom Step" label instead), hide `warning_text`/`expected_outcome`/`verification`/`reference_url` sections (they don't exist on custom steps), and show description + commands normally. The `canComplete()` check and Mark Complete button stay the same.
**Step 1: Update import and prop type**
```tsx
import type { RuntimeStep, CommandBlock } from '@/types'
```
Change `StepDetailProps`:
```tsx
interface StepDetailProps {
step: RuntimeStep
// ... rest unchanged
}
```
**Step 2: Handle custom step header (replace content_type badge block)**
The existing header block uses `step.content_type` to pick a config. Add a guard:
```tsx
// At top of component body, after existing state:
const isCustom = 'isCustom' in step && step.isCustom
// In the header JSX, replace the content_type badge span:
{isCustom ? (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-400/15 px-2 py-0.5 text-xs text-amber-400">
Custom Step
</span>
) : (
<span className={cn('inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs', config.bg, config.color)}>
<Icon className="h-3 w-3" />
{config.label}
</span>
)}
```
Note: `contentType` and `config` are still computed but only used in the non-custom branch. TypeScript will be happy because `ProceduralStep` has `content_type` and custom step skips that branch.
**Step 3: Guard sections that don't exist on custom steps**
Sections that need guarding (wrap with `!isCustom &&`):
- `{step.warning_text && ...}` — already implicitly guarded since `CustomProceduralStep` has no `warning_text`, but TypeScript may complain → add `!isCustom &&` before the expression
- `{step.expected_outcome && ...}` — same
- `{verificationPrompt && ...}` — same (derive `verificationPrompt` with `!isCustom &&` check)
- `{step.reference_url && ...}` — same
The description block, commandBlocks block, and notes block all work fine as-is (custom steps have `description?` and `commands?` matching the normalized shapes).
Specifically, for `verificationPrompt` / `verificationType`:
```tsx
const verificationPrompt = isCustom ? undefined : (step.verification_prompt || step.verification?.prompt)
const verificationType = isCustom ? undefined : (step.verification_type || step.verification?.type)
```
For `commandBlocks` normalization — `CustomProceduralStep.commands` is `CommandBlock[]` already, so the existing ternary handles it fine.
**Step 4: Build check**
Run: `cd frontend && npm run build 2>&1 | tail -20`
Expected: no errors
**Step 5: Commit**
```bash
git add frontend/src/components/procedural/StepDetail.tsx
git commit -m "feat: StepDetail accepts RuntimeStep, renders Custom Step badge for custom steps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
---
### Task 4: Wire custom step flow into `ProceduralNavigationPage`
**Files:**
- Modify: `frontend/src/pages/ProceduralNavigationPage.tsx`
**Context:** This is the main task. Current page has no custom step support. We add:
- `runtimeSteps: RuntimeStep[]` state (replaces `procedureSteps` derived value)
- `sessionCustomSteps: CustomStep[]` state
- `showCustomStepModal`, `showPostStepModal`, `pendingCustomStep`, `pendingIsFromLibrary`, `isSavingStep` state
- `handleStepCreated` — closes CustomStepModal, opens PostStepActionModal
- `handleInsertCustomStep` — builds CustomProceduralStep, inserts into runtimeSteps, persists
- `handleSaveForLater` / `handleUseNow` / `handleBoth` — wire PostStepActionModal buttons
- "Add Step" button below StepDetail, above StepFeedback
- Resume: inject custom steps at correct positions
**Step 1: Add new imports**
```tsx
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
import { PostStepActionModal } from '@/components/session/PostStepActionModal'
import type { RuntimeStep, CustomProceduralStep } from '@/types'
import type { CustomStep } from '@/types/session'
import type { Step } from '@/types/step'
import type { CustomStepDraft } from '@/components/step-library/CustomStepModal'
import { Plus } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid'
```
Check if `uuid` package is already available: `grep -r "from 'uuid'" frontend/src/`. If not, use `crypto.randomUUID()` instead (available in all modern browsers and Node 16+).
**Step 2: Add new state (after existing state declarations)**
```tsx
const [runtimeSteps, setRuntimeSteps] = useState<RuntimeStep[]>([])
const [sessionCustomSteps, setSessionCustomSteps] = useState<CustomStep[]>([])
const [showCustomStepModal, setShowCustomStepModal] = useState(false)
const [showPostStepModal, setShowPostStepModal] = useState(false)
const [pendingCustomStep, setPendingCustomStep] = useState<Step | CustomStepDraft | null>(null)
const [pendingIsFromLibrary, setPendingIsFromLibrary] = useState(false)
const [isSavingStep, setIsSavingStep] = useState(false)
```
**Step 3: Replace `procedureSteps` derivation**
Currently:
```tsx
const procedureSteps = steps.filter((s) => s.type === 'procedure_step')
```
Replace with:
```tsx
// runtimeSteps is the authoritative list; filter for rendering (excludes section_headers)
const procedureSteps = runtimeSteps.filter((s) => s.type === 'procedure_step')
```
Also update `estimatedTotalMinutes` — it only sums `estimated_minutes` which only exists on `ProceduralStep`. Cast safely:
```tsx
const estimatedTotalMinutes = procedureSteps.reduce(
(sum, step) => sum + (('estimated_minutes' in step ? step.estimated_minutes : undefined) || 0),
0
)
```
**Step 4: Initialize `runtimeSteps` in `startSession`**
After `setSession(newSession)`, add:
```tsx
// Initialize runtimeSteps from tree steps
const allSteps = getStepsFromTree(tree!)
setRuntimeSteps(allSteps)
setSessionCustomSteps([])
```
The existing step state initialization loop stays the same — custom steps will add to it when inserted.
**Step 5: Initialize `runtimeSteps` in `resumeSession`**
After loading `sessionData`, before computing `pSteps`:
```tsx
// Build runtimeSteps: start with tree steps, then inject custom steps
const allSteps = getStepsFromTree(treeData)
const customSteps = sessionData.custom_steps || []
setSessionCustomSteps(customSteps)
// Inject custom steps at correct positions
const hydrated = buildRuntimeSteps(allSteps, customSteps)
setRuntimeSteps(hydrated)
```
And update the `pSteps` / `firstIncomplete` calculation to use `hydrated` (not `allSteps`):
```tsx
const pSteps = hydrated.filter((s) => s.type === 'procedure_step')
const firstIncomplete = pSteps.findIndex((s) => !initialStates.get(s.id)?.completedAt)
setCurrentStepIndex(firstIncomplete >= 0 ? firstIncomplete : pSteps.length - 1)
```
**Step 6: Add `buildRuntimeSteps` helper**
Add this function before the component (or as a module-level utility):
```ts
function buildRuntimeSteps(baseSteps: ProceduralStep[], customSteps: CustomStep[]): RuntimeStep[] {
const result: RuntimeStep[] = [...baseSteps]
// Sort custom steps by timestamp so earlier insertions come first if multiple
const sorted = [...customSteps].sort((a, b) => a.timestamp.localeCompare(b.timestamp))
for (const cs of sorted) {
// Find the index of the step this was inserted after
const afterIdx = result.findIndex((s) => s.id === cs.inserted_after_node_id)
const insertAt = afterIdx >= 0 ? afterIdx + 1 : result.length
const runtimeCustom: CustomProceduralStep = {
id: cs.id,
type: 'procedure_step',
title: cs.step_data.title,
description: cs.step_data.content?.instructions,
content_type: 'action',
commands: cs.step_data.content?.commands?.map((c) => ({
code: c.command,
label: c.label,
})),
isCustom: true,
}
result.splice(insertAt, 0, runtimeCustom)
}
return result
}
```
Note: `ProceduralStep` needs to be imported in scope here. Since it's used in `getStepsFromTree` already, it's available.
**Step 7: Add `handleStepCreated`**
```tsx
const handleStepCreated = (step: Step | CustomStepDraft, isFromLibrary: boolean) => {
setPendingCustomStep(step)
setPendingIsFromLibrary(isFromLibrary)
setShowCustomStepModal(false)
setShowPostStepModal(true)
}
```
**Step 8: Add `handleInsertCustomStep`**
```tsx
const handleInsertCustomStep = async (step: Step | CustomStepDraft) => {
if (!session) return
const id = crypto.randomUUID()
const currentStep = procedureSteps[currentStepIndex]
const insertedAfterId = currentStep?.id ?? ''
// Build the runtime representation
const runtimeCustom: CustomProceduralStep = {
id,
type: 'procedure_step',
title: step.title,
description: step.content?.instructions,
content_type: 'action',
commands: step.content?.commands?.map((c) => ({
code: c.command,
label: c.label,
})),
isCustom: true,
}
// Insert after currentStepIndex in runtimeSteps
setRuntimeSteps((prev) => {
const next = [...prev]
// Find the global index of the current procedureStep in runtimeSteps
const globalIdx = next.findIndex((s) => s.id === insertedAfterId)
const insertAt = globalIdx >= 0 ? globalIdx + 1 : next.length
next.splice(insertAt, 0, runtimeCustom)
return next
})
// Initialize step state for the new step
setStepStates((prev) => {
const next = new Map(prev)
next.set(id, { notes: '', verificationValue: '', completedAt: null })
return next
})
// Persist to session
const newCustomStep: CustomStep = {
id,
inserted_after_node_id: insertedAfterId,
step_data: step,
timestamp: new Date().toISOString(),
}
const newCustomSteps = [...sessionCustomSteps, newCustomStep]
setSessionCustomSteps(newCustomSteps)
try {
await sessionsApi.update(session.id, { custom_steps: newCustomSteps })
} catch {
toast.error('Failed to save custom step')
}
// Advance to the new step (it's now at currentStepIndex + 1)
setCurrentStepIndex(currentStepIndex + 1)
}
```
**Step 9: Add `handleSaveForLater`, `handleUseNow`, `handleBoth`**
```tsx
const handleSaveForLater = async () => {
if (!pendingCustomStep || pendingIsFromLibrary) return
setIsSavingStep(true)
try {
await stepsApi.create({
title: pendingCustomStep.title,
step_type: pendingCustomStep.step_type,
content: pendingCustomStep.content,
visibility: 'private',
})
toast.success('Step saved to library')
} catch {
toast.error('Failed to save step')
} finally {
setIsSavingStep(false)
setShowPostStepModal(false)
setPendingCustomStep(null)
}
}
const handleUseNow = async () => {
if (!pendingCustomStep) return
setShowPostStepModal(false)
await handleInsertCustomStep(pendingCustomStep)
setPendingCustomStep(null)
}
const handleBoth = async () => {
if (!pendingCustomStep || pendingIsFromLibrary) return
setIsSavingStep(true)
try {
await stepsApi.create({
title: pendingCustomStep.title,
step_type: pendingCustomStep.step_type,
content: pendingCustomStep.content,
visibility: 'private',
})
} catch {
toast.error('Failed to save step to library')
} finally {
setIsSavingStep(false)
}
setShowPostStepModal(false)
await handleInsertCustomStep(pendingCustomStep)
setPendingCustomStep(null)
}
```
Add `stepsApi` import at the top: `import { stepsApi } from '@/api/steps'`
**Step 10: Update `handleMarkComplete` to use `runtimeSteps`-based `procedureSteps`**
The existing `handleMarkComplete` reads `procedureSteps[currentStepIndex]` — since `procedureSteps` is now derived from `runtimeSteps`, this Just Works. But the completion check `currentStepIndex >= procedureSteps.length - 1` also works correctly.
No changes needed to `handleMarkComplete` itself.
**Step 11: Update `StepChecklist` call in JSX**
Change `steps={steps}` to `steps={runtimeSteps}`:
```tsx
<StepChecklist
steps={runtimeSteps}
currentStepIndex={currentStepIndex}
completedStepIds={completedStepIds}
onStepClick={setCurrentStepIndex}
/>
```
**Step 12: Update `StepDetail` call + add "Add Step" button**
Change `step={currentStep}` prop type is now `RuntimeStep` (TypeScript satisfied since `procedureSteps` is `RuntimeStep[]`).
Change `totalSteps={procedureSteps.length}` — still correct.
After the `StepDetail` closing tag and before `StepFeedback`, add the "Add Step" button:
```tsx
{/* Add Custom Step button — only on current active (incomplete) step, not on custom steps */}
{currentStep && !completedStepIds.has(currentStep.id) && !('isCustom' in currentStep && currentStep.isCustom) && (
<div className="mt-4">
<button
onClick={() => setShowCustomStepModal(true)}
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-border px-4 py-2.5 text-sm text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground"
>
<Plus className="h-4 w-4" />
Add Step
</button>
</div>
)}
```
**Step 13: Add modals at the bottom of the JSX (before closing `</div>`)**
After the `ConfirmDialog` and parameters popover, add:
```tsx
{/* Custom Step Modal */}
<CustomStepModal
isOpen={showCustomStepModal}
onClose={() => setShowCustomStepModal(false)}
onInsertStep={handleStepCreated}
/>
{/* Post Step Action Modal */}
{pendingCustomStep && (
<PostStepActionModal
isOpen={showPostStepModal}
onClose={() => { setShowPostStepModal(false); setPendingCustomStep(null) }}
step={pendingCustomStep}
onSaveForLater={handleSaveForLater}
onUseNow={handleUseNow}
onBoth={handleBoth}
isFromLibrary={pendingIsFromLibrary}
isSaving={isSavingStep}
/>
)}
```
**Step 14: ProgressBar total count update**
The `ProgressBar` already uses `procedureSteps.length` as `totalSteps` which is derived from `runtimeSteps` — so the progress bar total automatically updates when custom steps are inserted.
**Step 15: Build check**
Run: `cd frontend && npm run build 2>&1 | tail -30`
Expected: no errors. Watch especially for:
- `CustomStep` import (it's in `@/types/session`, not `@/types` — import directly if needed)
- `buildRuntimeSteps` needing `ProceduralStep` type explicitly imported
**Step 16: Commit**
```bash
git add frontend/src/pages/ProceduralNavigationPage.tsx
git commit -m "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>"
```
---
### Task 5: Manual verification checklist
**No code changes — verification only.**
Start a procedural session (or maintenance session). Verify:
1. **Add Step button appears** below the current step detail on an active, incomplete, non-custom step
2. **Add Step button is hidden** on completed steps
3. **Clicking Add Step** opens `CustomStepModal` with Create + Browse Library tabs
4. **Creating a step**`PostStepActionModal` appears
5. **"Use Now"** → closes modal, custom step appears as next step in checklist with amber "Custom" badge, current view advances to it
6. **"Save for Later"** → closes modal, nothing inserted, no crash
7. **"Do Both"** → saves to library AND inserts
8. **Mark Complete on custom step** → advances to next step normally
9. **Custom step in StepDetail** → shows "Custom Step" amber badge instead of content_type badge
10. **Progress bar total** increments when custom step is inserted
11. **Resume session** (navigate away and back with `{ state: { sessionId } }`) → custom step reappears at correct position in checklist
---
### Task 6: Final build validation
Run: `cd frontend && npm run build`
Expected: build succeeds with zero errors (pre-existing chunk-size warnings are fine).
Commit build validation result if no issues:
```bash
git add -p # stage any incidental fixes
git commit -m "chore: final build validation for procedural custom steps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
Only commit if there were actual changes to stage.

View File

@@ -0,0 +1,175 @@
# Step Library Page — Design
> **Date:** February 24, 2026
> **Status:** Approved
> **Phase:** 2.5 — Feature 1 of 3
---
## What We're Building
Replace the "Coming Soon" stub in `StepLibraryPage` with a fully functional standalone Step Library. Users can browse, search, create, edit, delete, preview, and save steps to their own library.
All backend endpoints and frontend components are already built. This is primarily a wiring and UX integration task.
---
## Scope
**In scope:**
- Full-page Step Library with header, filters, grouped results
- Create step flow (modal with `StepForm`)
- Edit step flow (same modal, pre-filled)
- Delete step with confirmation
- "Save to My Library" for steps you don't own (copies step as private)
- Preview via `StepDetailModal`
- `StepCard` actions adapted for library context (vs. session insert context)
**Out of scope:**
- Rating/reviewing steps from this page (deferred — will come when steps are used in sessions)
- Admin category management (already exists in Admin Panel)
- Session custom step insertion (Feature 2 of 3)
---
## Card Actions by Context
| Ownership | Actions |
|-----------|---------|
| Your own step | Preview · Edit · Delete |
| Team or public step | Preview · Save to My Library |
"Save to My Library" POSTs a new step copying the title, step_type, content, category, and tags — with `visibility: 'private'` and the current user as owner.
---
## Page Layout
```
┌─────────────────────────────────────────────────────────┐
│ Step Library [+ Create Step] │
│ Reusable steps you can insert into any flow │
├─────────────────────────────────────────────────────────┤
│ [Search...] [Category ▼] [Type ▼] [Rating ▼] [Sort▼]│
│ Popular Tags: [powershell] [dns] [quick-fix] ... │
├─────────────────────────────────────────────────────────┤
│ MY STEPS (3) [▲] │
│ ┌────────────┐ ┌────────────┐ │
│ │ StepCard │ │ StepCard │ (Preview · Edit · Del) │
│ └────────────┘ └────────────┘ │
│ │
│ TEAM STEPS (12) [▲] │
│ ┌────────────┐ ┌────────────┐ │
│ │ StepCard │ │ StepCard │ (Preview · Save) │
│ └────────────┘ └────────────┘ │
│ │
│ COMMUNITY (47) [▲] │
│ ┌────────────┐ ┌────────────┐ │
│ │ StepCard │ │ StepCard │ (Preview · Save) │
│ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## Component Architecture
### StepLibraryPage (rewritten)
- Owns modal state: `createOpen`, `editingStep`, `deletingStepId`
- Renders page header with "+ Create Step" button
- Renders `StepLibraryBrowser` with page-context props
- Renders `StepFormModal` (create/edit)
- Renders delete confirmation dialog
### StepLibraryBrowser (extend props)
Add props:
```ts
interface StepLibraryBrowserProps {
onInsert: (step: Step) => void // existing (used in session context)
onEdit?: (step: Step) => void // new — library page only
onDelete?: (stepId: string) => void // new — library page only
onSave?: (step: Step) => void // new — save to my library
onCreateNew?: () => void // existing
showCreateButton?: boolean // existing
currentUserId?: string // new — to determine ownership
}
```
### StepCard (extend props)
Add props mirroring the browser:
```ts
interface StepCardProps {
step: StepListItem
onPreview: (step: StepListItem) => void
onInsert?: (step: StepListItem) => void // optional — session context
onEdit?: (step: StepListItem) => void // library context
onDelete?: (stepId: string) => void // library context
onSave?: (step: StepListItem) => void // library context
currentUserId?: string
}
```
`StepCard` renders actions based on which callbacks are present and whether `step.created_by === currentUserId`.
### StepFormModal (new wrapper component)
Thin modal shell around the existing `StepForm`:
- Fixed header ("Create Step" or "Edit Step")
- Scrollable body with `StepForm`
- Handles create (`stepsApi.create`) and update (`stepsApi.update`) API calls
- Calls `onSuccess(step)` so the browser list can refresh
---
## Data Flow
### Create
1. User clicks "+ Create Step"
2. `StepFormModal` opens in create mode
3. On submit → `stepsApi.create(data)``onSuccess` → reload steps
### Edit
1. User clicks Edit on their own step card
2. `stepsApi.get(stepId)` fetches full step (need content fields)
3. `StepFormModal` opens pre-filled with step data
4. On submit → `stepsApi.update(id, data)``onSuccess` → reload steps
### Delete
1. User clicks Delete on their own step card
2. Confirmation dialog: "Delete '[title]'? This cannot be undone."
3. On confirm → `stepsApi.delete(id)` → remove from local state
### Save to My Library
1. User clicks "Save to My Library" on a team/public step
2. No confirmation needed — silent copy
3. `stepsApi.create({ ...step fields, visibility: 'private' })` → toast "Saved to My Steps"
4. Reload steps so the copy appears under "My Steps"
---
## Files to Change
| File | Change |
|------|--------|
| `pages/StepLibraryPage.tsx` | Rewrite — wire up browser + modals |
| `components/step-library/StepCard.tsx` | Add `onEdit`, `onDelete`, `onSave`, `currentUserId` props; render contextual buttons |
| `components/step-library/StepLibraryBrowser.tsx` | Add `onEdit`, `onDelete`, `onSave`, `currentUserId` props; pass through to cards; expose refresh trigger |
| `components/step-library/StepFormModal.tsx` | Create — thin modal wrapper around `StepForm` |
No backend changes required.
---
## StepCard Button Layout
**Own step:**
```
[Preview] [Edit] [🗑]
```
Edit and Preview are full-width-ish buttons; delete is an icon-only button in red on hover.
**Others' step:**
```
[Preview] [Save to My Library]
```
Both full-width buttons, same pattern as current Insert layout.

View File

@@ -0,0 +1,705 @@
# Step Library Page Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Wire up the existing Step Library components into a fully functional standalone page — replacing the "Coming Soon" stub — with create, edit, delete, preview, and save-to-library actions.
**Architecture:** `StepLibraryPage` owns all modal state and orchestrates four components: `StepLibraryBrowser` (list + filters), `StepFormModal` (new wrapper for create/edit), `StepDetailModal` (already exists), and a delete confirmation dialog. `StepCard` and `StepLibraryBrowser` get new optional props for library-page-specific actions. No new API calls beyond what already exists in `stepsApi`.
**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (`useAuthStore` for current user ID), `stepsApi` + `stepCategoriesApi` (all endpoints already wired to backend).
---
## Key Files Reference
- `frontend/src/pages/StepLibraryPage.tsx` — currently a "Coming Soon" stub; will be rewritten
- `frontend/src/components/step-library/StepLibraryBrowser.tsx` — list + filters component
- `frontend/src/components/step-library/StepCard.tsx` — individual step card
- `frontend/src/components/step-library/StepDetailModal.tsx` — preview modal (already complete)
- `frontend/src/components/step-library/StepForm.tsx` — create/edit form (already complete)
- `frontend/src/api/steps.ts``stepsApi.create`, `.get`, `.update`, `.delete` already implemented
- `frontend/src/types/step.ts``Step`, `StepListItem`, `StepCreate`, `StepUpdate` types
- `frontend/src/store/authStore.ts` — use `useAuthStore((s) => s.user)` to get current user
- `frontend/src/hooks/usePermissions.ts``canCreateSteps` already defined
---
## How to Get Current User ID
```tsx
import { useAuthStore } from '@/store/authStore'
const user = useAuthStore((s) => s.user)
// user.id is the current user's UUID string
```
---
## Task 1: Extend StepCard with library-page actions
**Files:**
- Modify: `frontend/src/components/step-library/StepCard.tsx`
This task adds `onEdit`, `onDelete`, `onSave`, and `currentUserId` props. When on the library page (these props are present), the action buttons change based on ownership.
**Step 1: Read the current file**
Already read above. Current interface:
```ts
interface StepCardProps {
step: StepListItem
onPreview: (step: StepListItem) => void
onInsert: (step: StepListItem) => void
}
```
**Step 2: Update the interface and button logic**
Replace the `StepCardProps` interface and the Actions section at the bottom of `StepCard.tsx`.
New interface (all new props are optional so existing `CustomStepModal` usage is unchanged):
```tsx
interface StepCardProps {
step: StepListItem
onPreview: (step: StepListItem) => void
onInsert?: (step: StepListItem) => void // session context (existing)
onEdit?: (step: StepListItem) => void // library page
onDelete?: (stepId: string) => void // library page
onSave?: (step: StepListItem) => void // library page (save copy)
currentUserId?: string // to determine ownership
}
```
Replace the Actions section (the `<div className="flex gap-2">` at the bottom) with:
```tsx
{/* Actions */}
<div className="flex gap-2">
{/* Library page context */}
{(onEdit || onDelete || onSave) ? (
isOwn ? (
// Own step: Preview + Edit + Delete icon
<>
<button
onClick={() => onPreview(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Eye className="h-4 w-4" />
Preview
</button>
<button
onClick={() => onEdit?.(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Pencil className="h-4 w-4" />
Edit
</button>
<button
onClick={() => onDelete?.(step.id)}
className="flex items-center justify-center rounded-md border border-border px-3 py-2 text-muted-foreground hover:bg-red-400/10 hover:text-red-400 hover:border-red-400/30 transition-colors"
aria-label="Delete step"
>
<Trash2 className="h-4 w-4" />
</button>
</>
) : (
// Others' step: Preview + Save
<>
<button
onClick={() => onPreview(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Eye className="h-4 w-4" />
Preview
</button>
<button
onClick={() => onSave?.(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium hover:opacity-90 transition-colors"
>
<Bookmark className="h-4 w-4" />
Save
</button>
</>
)
) : (
// Session context (original): Preview + Insert
<>
<button
onClick={() => onPreview(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Eye className="h-4 w-4" />
Preview
</button>
<button
onClick={() => onInsert?.(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium hover:opacity-90 transition-colors"
>
<Plus className="h-4 w-4" />
Insert
</button>
</>
)}
</div>
```
Add `isOwn` derived value near top of the component function (before the return):
```tsx
const isOwn = currentUserId ? step.created_by === currentUserId : false
```
Add new imports at top of file:
```tsx
import { Star, User, Calendar, TrendingUp, Eye, Plus, HelpCircle, Zap, CheckCircle, Pencil, Trash2, Bookmark } from 'lucide-react'
```
**Step 3: Verify TypeScript compiles**
```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
```
Expected: no errors related to StepCard.
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepCard.tsx
git commit -m "feat: add library-page action props to StepCard (edit/delete/save)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
---
## Task 2: Extend StepLibraryBrowser with library-page props
**Files:**
- Modify: `frontend/src/components/step-library/StepLibraryBrowser.tsx`
Pass the new `onEdit`, `onDelete`, `onSave`, `currentUserId` props through from browser to each `StepCard`. Also expose a `refreshKey` prop so the page can trigger a reload after create/edit/delete/save.
**Step 1: Update the interface**
Current interface:
```ts
interface StepLibraryBrowserProps {
onInsert: (step: Step) => void
onCreateNew?: () => void
showCreateButton?: boolean
}
```
New interface:
```ts
interface StepLibraryBrowserProps {
onInsert?: (step: Step) => void // now optional (not needed on library page)
onCreateNew?: () => void
showCreateButton?: boolean
onEdit?: (step: StepListItem) => void
onDelete?: (stepId: string) => void
onSave?: (step: StepListItem) => void
currentUserId?: string
refreshKey?: number // increment to trigger reload
}
```
**Step 2: Wire refreshKey into the steps useEffect**
In the existing `useEffect` that calls `loadSteps`, add `refreshKey` to the dependency array:
```tsx
useEffect(() => {
const loadSteps = async () => { ... }
loadSteps()
}, [searchQuery, selectedCategoryId, selectedStepType, minRating, sortBy, selectedTag, refreshKey])
```
**Step 3: Pass new props to StepCard**
In all three `groupedSteps.private/team/public` map blocks, update `StepCard` usage:
```tsx
<StepCard
key={step.id}
step={step}
onPreview={handlePreview}
onInsert={onInsert ? handleInsertFromCard : undefined}
onEdit={onEdit}
onDelete={onDelete}
onSave={onSave}
currentUserId={currentUserId}
/>
```
**Step 4: Verify TypeScript compiles**
```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
```
**Step 5: Commit**
```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepLibraryBrowser.tsx
git commit -m "feat: pass library-page action props through StepLibraryBrowser + refreshKey
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
---
## Task 3: Create StepFormModal
**Files:**
- Create: `frontend/src/components/step-library/StepFormModal.tsx`
A thin modal wrapper around the existing `StepForm`. Handles both create and edit modes.
**Step 1: Create the file**
```tsx
import { useState } from 'react'
import { X } from 'lucide-react'
import { stepsApi } from '@/api/steps'
import { StepForm } from './StepForm'
import type { Step, StepCreate, StepListItem } from '@/types/step'
interface StepFormModalProps {
isOpen: boolean
onClose: () => void
onSuccess: (step: Step) => void
editingStep?: StepListItem | null // if set, edit mode; if null/undefined, create mode
}
export function StepFormModal({ isOpen, onClose, onSuccess, editingStep }: StepFormModalProps) {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
if (!isOpen) return null
const isEditMode = !!editingStep
const handleSubmit = async (data: StepCreate) => {
setIsSubmitting(true)
setError(null)
try {
let result: Step
if (isEditMode && editingStep) {
result = await stepsApi.update(editingStep.id, data)
} else {
result = await stepsApi.create(data)
}
onSuccess(result)
} catch (err) {
console.error('Failed to save step:', err)
setError('Failed to save step. Please try again.')
} finally {
setIsSubmitting(false)
}
}
// Build initialData from editingStep for edit mode
// StepListItem doesn't have `content`, so for edit we need to fetch full step
// This is handled by the parent (StepLibraryPage fetches full step before opening modal)
const initialData = editingStep ? {
title: editingStep.title,
step_type: editingStep.step_type as 'decision' | 'action' | 'solution',
visibility: editingStep.visibility as 'private' | 'team' | 'public',
category_id: editingStep.category_id,
tags: editingStep.tags,
} : undefined
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="relative flex h-[90vh] w-full max-w-2xl flex-col bg-card border border-border rounded-2xl shadow-lg">
{/* Header */}
<div className="flex items-center justify-between border-b border-border p-6 pb-4">
<h2 className="text-lg font-semibold text-foreground">
{isEditMode ? 'Edit Step' : 'Create Step'}
</h2>
<button
onClick={onClose}
disabled={isSubmitting}
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Error */}
{error && (
<div className="mx-6 mt-4 rounded-lg border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
{error}
</div>
)}
{/* Body */}
<div className="flex-1 overflow-y-auto p-6">
<StepForm
onSubmit={handleSubmit}
onCancel={onClose}
initialData={initialData}
submitLabel={isEditMode ? 'Save Changes' : 'Create Step'}
isSubmitting={isSubmitting}
/>
</div>
</div>
</div>
)
}
```
**Step 2: Update StepForm to accept `submitLabel` and `isSubmitting` props**
`StepForm` currently has a hardcoded submit button label ("Insert Step") and no loading state. Add these two optional props:
```ts
interface StepFormProps {
onSubmit: (data: StepCreate) => void
onCancel: () => void
initialData?: Partial<StepCreate>
submitLabel?: string // default: 'Insert Step'
isSubmitting?: boolean // default: false
}
```
In `StepForm`, use them on the submit button:
```tsx
<button
type="submit"
disabled={isSubmitting}
className="flex-1 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
{isSubmitting ? 'Saving...' : (submitLabel ?? 'Insert Step')}
</button>
```
**Step 3: Verify TypeScript compiles**
```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
```
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepFormModal.tsx \
frontend/src/components/step-library/StepForm.tsx
git commit -m "feat: StepFormModal wrapper + submitLabel/isSubmitting props on StepForm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
---
## Task 4: Rewrite StepLibraryPage
**Files:**
- Modify: `frontend/src/pages/StepLibraryPage.tsx`
This is the main wiring task. Replace the stub with the full page.
**Step 1: Write the new page**
```tsx
import { useState } from 'react'
import { Bookmark, Trash2 } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
import { stepsApi } from '@/api/steps'
import { StepLibraryBrowser } from '@/components/step-library/StepLibraryBrowser'
import { StepFormModal } from '@/components/step-library/StepFormModal'
import type { Step, StepListItem } from '@/types/step'
export default function StepLibraryPage() {
const user = useAuthStore((s) => s.user)
const { canCreateSteps } = usePermissions()
// Modal state
const [createOpen, setCreateOpen] = useState(false)
const [editingStep, setEditingStep] = useState<StepListItem | null>(null)
const [deletingStep, setDeletingStep] = useState<StepListItem | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const [deleteError, setDeleteError] = useState<string | null>(null)
const [saveToast, setSaveToast] = useState<string | null>(null)
// Increment to trigger StepLibraryBrowser reload
const [refreshKey, setRefreshKey] = useState(0)
const refresh = () => setRefreshKey(k => k + 1)
const handleEdit = (step: StepListItem) => {
setEditingStep(step)
}
const handleDeleteRequest = (stepId: string) => {
// Find the step in order to show its title in the confirmation
// We store the StepListItem via the browser's onDelete callback
// The step object is passed from StepCard which has the full StepListItem
setDeletingStep({ id: stepId } as StepListItem)
}
const handleDeleteConfirm = async () => {
if (!deletingStep) return
setIsDeleting(true)
setDeleteError(null)
try {
await stepsApi.delete(deletingStep.id)
setDeletingStep(null)
refresh()
} catch (err) {
console.error('Failed to delete step:', err)
setDeleteError('Failed to delete step. Please try again.')
} finally {
setIsDeleting(false)
}
}
const handleSave = async (step: StepListItem) => {
try {
// Fetch full step to get content fields
const full = await stepsApi.get(step.id)
await stepsApi.create({
title: full.title,
step_type: full.step_type,
content: full.content,
visibility: 'private',
category_id: full.category_id,
tags: full.tags,
})
setSaveToast(`"${full.title}" saved to My Steps`)
setTimeout(() => setSaveToast(null), 3000)
refresh()
} catch (err) {
console.error('Failed to save step:', err)
}
}
const handleFormSuccess = (_step: Step) => {
setCreateOpen(false)
setEditingStep(null)
refresh()
}
return (
<div className="flex h-full flex-col">
{/* Page Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<div className="flex items-center gap-3">
<span title="Step Library">
<Bookmark className="h-6 w-6 text-muted-foreground" />
</span>
<div>
<h1 className="text-xl font-bold font-heading text-foreground">Step Library</h1>
<p className="text-sm text-muted-foreground">Reusable steps you can insert into any flow</p>
</div>
</div>
{canCreateSteps && (
<button
onClick={() => setCreateOpen(true)}
className="rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90"
>
+ Create Step
</button>
)}
</div>
{/* Browser fills remaining height */}
<div className="flex-1 overflow-hidden">
<StepLibraryBrowser
onEdit={handleEdit}
onDelete={(stepId) => {
// We need the full StepListItem for the confirmation title.
// Pass a minimal object; the title will show as "this step" if not available.
setDeletingStep({ id: stepId, title: '' } as StepListItem)
}}
onSave={handleSave}
currentUserId={user?.id}
refreshKey={refreshKey}
showCreateButton={false}
/>
</div>
{/* Create / Edit Modal */}
<StepFormModal
isOpen={createOpen || !!editingStep}
onClose={() => { setCreateOpen(false); setEditingStep(null) }}
onSuccess={handleFormSuccess}
editingStep={editingStep}
/>
{/* Delete Confirmation Dialog */}
{deletingStep && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="w-full max-w-sm rounded-xl bg-card border border-border p-6 shadow-lg">
<div className="mb-4 flex items-center gap-3">
<div className="rounded-full bg-red-400/10 p-2">
<Trash2 className="h-5 w-5 text-red-400" />
</div>
<h2 className="text-base font-semibold text-foreground">Delete Step</h2>
</div>
<p className="mb-2 text-sm text-muted-foreground">
{deletingStep.title
? <>Are you sure you want to delete <span className="font-medium text-foreground">"{deletingStep.title}"</span>?</>
: 'Are you sure you want to delete this step?'
}
</p>
<p className="mb-6 text-xs text-muted-foreground">This cannot be undone.</p>
{deleteError && (
<p className="mb-4 text-sm text-red-400">{deleteError}</p>
)}
<div className="flex gap-2">
<button
onClick={() => { setDeletingStep(null); setDeleteError(null) }}
disabled={isDeleting}
className="flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
>
Cancel
</button>
<button
onClick={handleDeleteConfirm}
disabled={isDeleting}
className="flex-1 rounded-md bg-red-500 px-4 py-2 text-sm font-medium text-white hover:bg-red-600 disabled:opacity-50"
>
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
)}
{/* Save Toast */}
{saveToast && (
<div className="fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-lg border border-border bg-card px-4 py-2 text-sm text-foreground shadow-lg">
{saveToast}
</div>
)}
</div>
)
}
```
**NOTE on delete title:** The `onDelete` callback from `StepCard` only passes `stepId: string`, not the full `StepListItem`. To show the step title in the confirmation dialog, change the `StepLibraryBrowser`'s `onDelete` prop type to pass the full `StepListItem` instead:
In `StepLibraryBrowser.tsx`, change:
```ts
onDelete?: (stepId: string) => void
```
to:
```ts
onDelete?: (step: StepListItem) => void
```
And update where it calls `onDelete` from cards — pass the full `step` object. Update `StepCard` similarly: change `onDelete?: (stepId: string) => void` to `onDelete?: (step: StepListItem) => void` and call `onDelete?.(step)` instead of `onDelete?.(step.id)`.
Then in `StepLibraryPage`, use `handleDeleteRequest(step: StepListItem)` and set `setDeletingStep(step)` directly — no need to pass a minimal object.
**Step 2: Run TypeScript check**
```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -40
```
Fix any type errors before proceeding.
**Step 3: Run build**
```bash
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
```
Expected: build succeeds with no errors.
**Step 4: Commit**
```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/pages/StepLibraryPage.tsx \
frontend/src/components/step-library/StepLibraryBrowser.tsx \
frontend/src/components/step-library/StepCard.tsx
git commit -m "feat: Step Library page — create, edit, delete, save-to-library
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
---
## Task 5: Manual verification checklist
Start the dev server and verify these flows work end-to-end:
```bash
docker start patherly_postgres
cd /home/michaelchihlas/dev/patherly/backend && source venv/bin/activate && uvicorn app.main:app --reload &
cd /home/michaelchihlas/dev/patherly/frontend && npm run dev
```
Navigate to `http://localhost:5173/step-library` and verify:
- [ ] Page loads without errors (not "Coming Soon")
- [ ] "+ Create Step" button appears (login as engineer or admin)
- [ ] Creating a step via the modal saves it and it appears under "My Steps" on reload
- [ ] "Edit" button appears on your own step cards
- [ ] Editing a step opens the form pre-filled (note: `content` fields won't pre-fill since `StepListItem` doesn't have content — this is acceptable for now; see note below)
- [ ] "Delete" button appears on your own step cards
- [ ] Delete confirmation shows step title; confirming removes it from the list
- [ ] "Save" button appears on team/community step cards
- [ ] Saving a step copies it to "My Steps" and shows toast
- [ ] "Preview" opens `StepDetailModal` correctly on all card types
- [ ] Filters (category, type, rating, sort) work
- [ ] Popular tags clickable and filter results
**Note on edit pre-fill:** `StepListItem` does not include `content`. The `StepFormModal` passes `initialData` from `editingStep`, but `content` will be missing. For a full pre-fill, `StepLibraryPage.handleEdit` should fetch the full step via `stepsApi.get(step.id)` before opening the modal, and store the result as a `Step` (not `StepListItem`) in `editingStep` state. Update `editingStep` state type to `Step | null` and fetch in `handleEdit`:
```tsx
const [editingStep, setEditingStep] = useState<Step | null>(null)
const handleEdit = async (step: StepListItem) => {
try {
const full = await stepsApi.get(step.id)
setEditingStep(full)
} catch (err) {
console.error('Failed to load step for edit:', err)
}
}
```
Update `StepFormModal`'s `editingStep` prop type to accept `Step | null` and build `initialData` from the full `Step` including `content`:
```tsx
editingStep?: Step | null
const initialData = editingStep ? {
title: editingStep.title,
step_type: editingStep.step_type,
content: editingStep.content,
visibility: editingStep.visibility,
category_id: editingStep.category_id,
tags: editingStep.tags,
} : undefined
```
This should be done as part of Task 4 before verifying.
---
## Task 6: Final build validation and commit
```bash
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
```
Expected: clean build, no TypeScript errors, no warnings about missing exports.
If clean:
```bash
cd /home/michaelchihlas/dev/patherly
git add -A
git status # confirm only expected files changed
git commit -m "chore: step library page final build validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```