179 lines
6.4 KiB
Markdown
179 lines
6.4 KiB
Markdown
# 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.
|