# 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.
