From 60e1384a2c7788bd9f460b85284f7b6b9351a00d Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sun, 29 Mar 2026 05:46:40 +0000 Subject: [PATCH] docs: add parameterize-and-save implementation plan 7-task plan covering backend schema updates, ParameterizeAndSavePanel component, ScriptBuilderPage integration, ScriptLibraryPage "New from Script" entry point, and SaveToLibraryDialog deletion. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-29-parameterize-and-save.md | 937 ++++++++++++++++++ 1 file changed, 937 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-29-parameterize-and-save.md diff --git a/docs/superpowers/plans/2026-03-29-parameterize-and-save.md b/docs/superpowers/plans/2026-03-29-parameterize-and-save.md new file mode 100644 index 00000000..23d36095 --- /dev/null +++ b/docs/superpowers/plans/2026-03-29-parameterize-and-save.md @@ -0,0 +1,937 @@ +# 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) => ( + + ))} +
+