# 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 " ``` --- ### 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 onStepClick: (index: number) => void } ``` **Step 2: Add the "Custom" badge in the step row** In the button's `` row, add a badge after the title when `'isCustom' in step && step.isCustom`: ```tsx {step.title || 'Untitled step'} {'isCustom' in step && step.isCustom && ( Custom )} ``` **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 " ``` --- ### 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 ? ( ✦ Custom Step ) : ( {config.label} )} ``` 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 " ``` --- ### 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([]) const [sessionCustomSteps, setSessionCustomSteps] = useState([]) const [showCustomStepModal, setShowCustomStepModal] = useState(false) const [showPostStepModal, setShowPostStepModal] = useState(false) const [pendingCustomStep, setPendingCustomStep] = useState(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 ``` **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) && (
)} ``` **Step 13: Add modals at the bottom of the JSX (before closing ``)** After the `ConfirmDialog` and parameters popover, add: ```tsx {/* Custom Step Modal */} setShowCustomStepModal(false)} onInsertStep={handleStepCreated} /> {/* Post Step Action Modal */} {pendingCustomStep && ( { 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 " ``` --- ### 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 " ``` Only commit if there were actual changes to stage.