# Parameterize & Save Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Fix AI-generated scripts saving to library without parameters by adding parameter detection, user review, and template rewriting to the save flow — plus a "New from Script" paste entry point on the library page. **Architecture:** A new `ParameterizeAndSavePanel` slide-in panel component handles two modes: `script` (pre-populated from AI builder) and `paste` (user pastes raw script). It auto-runs `detectParameterCandidates()`, renders `ParameterDetectorStepper` for review, rewrites the script body with `{{ key }}` placeholders, and sends enriched payload to the backend. Backend `save_to_library()` accepts the provided `script_body` + `parameters_schema` instead of hardcoding empty values. **Tech Stack:** React 19, TypeScript, Tailwind CSS v4, FastAPI, Pydantic v2, SQLAlchemy 2.0 **Spec:** `docs/superpowers/specs/2026-03-29-parameterize-and-save-design.md` --- ### Task 1: Backend — Update `SaveToLibraryRequest` schema and `save_to_library()` service **Files:** - Modify: `backend/app/schemas/script_builder.py:79-84` (SaveToLibraryRequest) - Modify: `backend/app/services/script_builder_service.py:317-377` (save_to_library function) - Modify: `backend/app/api/endpoints/script_builder.py:156-188` (save_to_library endpoint) - [ ] **Step 1: Update `SaveToLibraryRequest` schema** In `backend/app/schemas/script_builder.py`, add two optional fields to `SaveToLibraryRequest`: ```python class SaveToLibraryRequest(BaseModel): """Request to save a generated script to the Script Library.""" name: str = Field(min_length=1, max_length=200) description: str | None = None category_id: UUID | None = None share_with_team: bool = False script_body: str | None = None parameters_schema: dict | None = None ``` - [ ] **Step 2: Update `save_to_library()` service function signature and body** In `backend/app/services/script_builder_service.py`, add two new parameters to `save_to_library()` and use them in the `ScriptTemplate` constructor: ```python async def save_to_library( db: AsyncSession, session: ScriptBuilderSession, name: str, description: str | None, category_id: UUID | None, share_with_team: bool, user_id: UUID, team_id: UUID | None, script_body: str | None = None, parameters_schema: dict | None = None, ) -> "ScriptTemplate": ``` And in the `ScriptTemplate(...)` constructor inside the same function, change the two hardcoded lines: ```python template = ScriptTemplate( id=uuid_mod.uuid4(), category_id=resolved_category_id, created_by=user_id, team_id=team_id if share_with_team else None, name=name, slug=slug, description=description, script_body=script_body or session.latest_script, parameters_schema=parameters_schema or {"parameters": []}, default_values={}, validation_rules={}, tags=[session.language, "ai-generated"], complexity="intermediate", is_verified=False, is_active=True, version=1, usage_count=0, ) ``` - [ ] **Step 3: Update the endpoint to pass new fields through** In `backend/app/api/endpoints/script_builder.py`, update the `save_to_library` endpoint to pass the new fields to the service: ```python template = await script_builder_service.save_to_library( db=db, session=session, name=data.name, description=data.description, category_id=data.category_id, share_with_team=data.share_with_team, user_id=current_user.id, team_id=current_user.team_id, script_body=data.script_body, parameters_schema=data.parameters_schema, ) ``` - [ ] **Step 4: Verify backend still starts cleanly** Run: `cd /home/coder/resolutionflow/backend && source venv/bin/activate && python -c "from app.schemas.script_builder import SaveToLibraryRequest; print('OK')"` Expected: `OK` - [ ] **Step 5: Commit** ```bash git add backend/app/schemas/script_builder.py backend/app/services/script_builder_service.py backend/app/api/endpoints/script_builder.py git commit -m "feat: accept script_body and parameters_schema in save-to-library flow Previously save_to_library() hardcoded parameters_schema to empty and always used session.latest_script. Now accepts optional overrides from the frontend for parameterized script bodies. Co-Authored-By: Claude Opus 4.6 (1M context) " ``` --- ### Task 2: Frontend — Update `SaveToLibraryRequest` type **Files:** - Modify: `frontend/src/types/script-builder.ts:45-50` (SaveToLibraryRequest interface) - [ ] **Step 1: Add new fields to the TypeScript interface** In `frontend/src/types/script-builder.ts`, update `SaveToLibraryRequest`: ```typescript export interface SaveToLibraryRequest { name: string description?: string category_id?: string share_with_team?: boolean script_body?: string parameters_schema?: { parameters: ScriptParameter[] } } ``` Add the import for `ScriptParameter` at the top of the file: ```typescript import type { ScriptParameter } from './scripts' ``` - [ ] **Step 2: Verify types compile** Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30` Expected: No errors related to `SaveToLibraryRequest` or `ScriptParameter`. - [ ] **Step 3: Commit** ```bash git add frontend/src/types/script-builder.ts git commit -m "feat: add script_body and parameters_schema to SaveToLibraryRequest type Co-Authored-By: Claude Opus 4.6 (1M context) " ``` --- ### Task 3: Frontend — Create `ParameterizeAndSavePanel` component **Files:** - Create: `frontend/src/components/scripts/ParameterizeAndSavePanel.tsx` This is the core new component. It handles two modes (`script` and `paste`), auto-runs parameter detection, embeds the existing `ParameterDetectorStepper`, rewrites the script body with `{{ key }}` placeholders, collects metadata, and calls the save handler. - [ ] **Step 1: Create the component file** Create `frontend/src/components/scripts/ParameterizeAndSavePanel.tsx`: ```tsx import { useState, useEffect, useRef, useCallback } from 'react' import { X, Loader2, FileCode, AlertCircle } from 'lucide-react' import { cn } from '@/lib/utils' import { scriptsApi } from '@/api' import { detectParameterCandidates } from '@/lib/scriptParameterDetector' import { ParameterDetectorStepper } from '@/components/script-editor/ParameterDetectorStepper' import type { ScriptCategoryResponse, ScriptParameter, ScriptParametersSchema, ParameterCandidate, } from '@/types' interface ParameterizeAndSavePanelProps { /** Pre-populated script body (script mode). Undefined triggers paste mode. */ scriptBody?: string /** Script language. Undefined shows language picker in paste mode. */ language?: string /** Default name for the template. */ defaultName?: string /** Default description for the template. */ defaultDescription?: string /** Called with the final enriched payload when user saves. */ onSave: (payload: { name: string description: string | undefined category_id: string | undefined share_with_team: boolean script_body: string parameters_schema: ScriptParametersSchema }) => Promise /** Called when the panel is closed without saving. */ onClose: () => void } const LANGUAGES = [ { value: 'powershell', label: 'PowerShell' }, { value: 'bash', label: 'Bash' }, { value: 'python', label: 'Python' }, ] export function ParameterizeAndSavePanel({ scriptBody, language: initialLanguage, defaultName = '', defaultDescription = '', onSave, onClose, }: ParameterizeAndSavePanelProps) { // Mode: script (body provided) vs paste (user pastes) const isPasteMode = scriptBody === undefined // Paste mode state const [pastedScript, setPastedScript] = useState('') const [selectedLanguage, setSelectedLanguage] = useState(initialLanguage || 'powershell') const [scriptConfirmed, setScriptConfirmed] = useState(false) // Working state — the script body being rewritten as params are accepted const effectiveScript = isPasteMode ? pastedScript : scriptBody! const [workingScript, setWorkingScript] = useState(effectiveScript) const [parameters, setParameters] = useState([]) // Detection state const [candidates, setCandidates] = useState([]) const [detectionRan, setDetectionRan] = useState(false) const [showStepper, setShowStepper] = useState(false) const [detectionSummary, setDetectionSummary] = useState(null) // Metadata state const [name, setName] = useState(defaultName) const [description, setDescription] = useState(defaultDescription) const [categoryId, setCategoryId] = useState('') const [shareWithTeam, setShareWithTeam] = useState(false) const [categories, setCategories] = useState([]) // Save state const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState(null) const panelRef = useRef(null) // Load categories on mount useEffect(() => { scriptsApi.getCategories().then(setCategories).catch(() => {}) }, []) // Auto-run detection when script is ready (script mode: on mount, paste mode: after confirm) const runDetection = useCallback((script: string) => { const detected = detectParameterCandidates(script) setCandidates(detected) setDetectionRan(true) if (detected.length > 0) { setShowStepper(true) } else { setDetectionSummary('No parameters detected — script will be saved as-is. Parameter detection currently supports PowerShell only.') } }, []) // Script mode: run detection on mount useEffect(() => { if (!isPasteMode && effectiveScript) { setWorkingScript(effectiveScript) runDetection(effectiveScript) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Paste mode: run detection after script is confirmed useEffect(() => { if (isPasteMode && scriptConfirmed && pastedScript) { setWorkingScript(pastedScript) runDetection(pastedScript) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [scriptConfirmed]) // Escape key closes panel useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [onClose]) const handleConfirmPaste = () => { if (!pastedScript.trim()) return setScriptConfirmed(true) } const handleAcceptCandidate = ( candidate: ParameterCandidate, overrides: { key: string label: string type: ScriptParameter['type'] sensitive: boolean required: boolean defaultValue: string | boolean | number | null } ) => { // Rewrite the script body — replace the default value with {{ key }} placeholder let updatedScript = workingScript if (candidate.source === 'param_block') { const defaultMatch = candidate.matchedLine.match(/=\s*(.+?)(?:\s*,?\s*$)/) if (defaultMatch) { updatedScript = updatedScript.replace( candidate.matchedLine, candidate.matchedLine.replace(defaultMatch[1], `'{{${overrides.key}}}'`) ) } } else { const assignMatch = candidate.matchedLine.match(/=\s*(.+)$/) if (assignMatch) { updatedScript = updatedScript.replace( candidate.matchedLine, candidate.matchedLine.replace(assignMatch[1], `'{{${overrides.key}}}'`) ) } } setWorkingScript(updatedScript) // Add parameter to accumulated schema const newParam: ScriptParameter = { key: overrides.key, label: overrides.label, type: overrides.type, required: overrides.required, placeholder: null, group: null, order: parameters.length + 1, help_text: null, options: null, default: overrides.defaultValue, validation: null, sensitive: overrides.sensitive, } setParameters(prev => [...prev, newParam]) } const handleSkipCandidate = () => { // Stepper advances internally — nothing to do here } const handleDetectionFinish = (acceptedCount: number, totalCount: number) => { setShowStepper(false) setCandidates([]) setDetectionSummary( acceptedCount === 0 ? 'No parameters were added. Script will be saved as-is.' : `Added ${acceptedCount} of ${totalCount} detected parameter${totalCount !== 1 ? 's' : ''}.` ) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) return setIsSaving(true) setError(null) try { await onSave({ name: name.trim(), description: description.trim() || undefined, category_id: categoryId || undefined, share_with_team: shareWithTeam, script_body: workingScript, parameters_schema: { parameters }, }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save script. Please try again.') } finally { setIsSaving(false) } } // Determine if save button should be enabled const canSave = name.trim().length > 0 && !isSaving && !showStepper && (isPasteMode ? scriptConfirmed : true) return ( <> {/* Scrim */}
{/* Panel */}
{/* Header */}

{isPasteMode ? 'Import Script to Library' : 'Save to Library'}

{/* Scrollable content */}
{/* Paste mode: script input area (before confirmation) */} {isPasteMode && !scriptConfirmed && (

Paste Your Script

{LANGUAGES.map((lang) => ( ))}