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) <noreply@anthropic.com>
This commit is contained in:
937
docs/superpowers/plans/2026-03-29-parameterize-and-save.md
Normal file
937
docs/superpowers/plans/2026-03-29-parameterize-and-save.md
Normal file
@@ -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) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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<void>
|
||||
/** 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<ScriptParameter[]>([])
|
||||
|
||||
// Detection state
|
||||
const [candidates, setCandidates] = useState<ParameterCandidate[]>([])
|
||||
const [detectionRan, setDetectionRan] = useState(false)
|
||||
const [showStepper, setShowStepper] = useState(false)
|
||||
const [detectionSummary, setDetectionSummary] = useState<string | null>(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<ScriptCategoryResponse[]>([])
|
||||
|
||||
// Save state
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const panelRef = useRef<HTMLDivElement>(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 */}
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Panel */}
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="fixed top-0 right-0 z-50 h-full w-[480px] max-w-full bg-page border-l border-default flex flex-col shadow-2xl"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-default shrink-0">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<FileCode size={18} className="text-primary" />
|
||||
<h3 className="text-sm font-heading font-bold text-foreground">
|
||||
{isPasteMode ? 'Import Script to Library' : 'Save to Library'}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-elevated transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 overflow-y-auto p-5 space-y-5">
|
||||
|
||||
{/* Paste mode: script input area (before confirmation) */}
|
||||
{isPasteMode && !scriptConfirmed && (
|
||||
<section className="space-y-3">
|
||||
<p className="font-sans text-xs uppercase tracking-widest text-muted-foreground">
|
||||
Paste Your Script
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
{LANGUAGES.map((lang) => (
|
||||
<button
|
||||
key={lang.value}
|
||||
type="button"
|
||||
onClick={() => setSelectedLanguage(lang.value)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-md text-xs font-medium transition-all',
|
||||
selectedLanguage === lang.value
|
||||
? 'bg-primary text-white'
|
||||
: 'text-muted-foreground hover:text-foreground bg-elevated'
|
||||
)}
|
||||
>
|
||||
{lang.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
value={pastedScript}
|
||||
onChange={(e) => setPastedScript(e.target.value)}
|
||||
rows={12}
|
||||
className={cn(
|
||||
'w-full rounded-lg px-3 py-2 text-sm font-mono resize-none',
|
||||
'border border-default bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:outline-none focus:border-primary/30 transition-colors'
|
||||
)}
|
||||
placeholder="Paste your PowerShell, Bash, or Python script here..."
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConfirmPaste}
|
||||
disabled={!pastedScript.trim()}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-all',
|
||||
'bg-primary text-white hover:brightness-110 active:scale-[0.98]',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
Detect Parameters
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Script preview (visible after paste confirm, or always in script mode) */}
|
||||
{(!isPasteMode || scriptConfirmed) && (
|
||||
<section className="space-y-2">
|
||||
<p className="font-sans text-xs uppercase tracking-widest text-muted-foreground">
|
||||
Script Preview
|
||||
</p>
|
||||
<div className="rounded-lg bg-black/30 border border-default overflow-hidden max-h-48 overflow-y-auto">
|
||||
<pre className="p-3 text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
||||
{workingScript.split(/({{.*?}})/).map((part, i) =>
|
||||
/^{{.*}}$/.test(part)
|
||||
? <span key={i} className="text-amber-400 font-semibold">{part}</span>
|
||||
: <span key={i}>{part}</span>
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Parameter detection zone */}
|
||||
{detectionRan && !showStepper && detectionSummary && (
|
||||
<div className="flex items-start gap-2 rounded-lg bg-elevated p-3">
|
||||
<AlertCircle size={14} className="text-muted-foreground mt-0.5 shrink-0" />
|
||||
<p className="text-xs text-muted-foreground">{detectionSummary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showStepper && candidates.length > 0 && (
|
||||
<section className="space-y-2">
|
||||
<p className="font-sans text-xs uppercase tracking-widest text-muted-foreground">
|
||||
Detected Parameters
|
||||
</p>
|
||||
<ParameterDetectorStepper
|
||||
candidates={candidates}
|
||||
existingKeys={parameters.map(p => p.key)}
|
||||
onAccept={handleAcceptCandidate}
|
||||
onSkip={handleSkipCandidate}
|
||||
onFinish={handleDetectionFinish}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Accepted parameters summary */}
|
||||
{parameters.length > 0 && !showStepper && (
|
||||
<section className="space-y-2">
|
||||
<p className="font-sans text-xs uppercase tracking-widest text-muted-foreground">
|
||||
Parameters ({parameters.length})
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{parameters.map((p) => (
|
||||
<div
|
||||
key={p.key}
|
||||
className="flex items-center justify-between rounded-lg bg-elevated px-3 py-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-xs font-mono text-amber-400">{`{{${p.key}}}`}</code>
|
||||
<span className="text-xs text-muted-foreground">{p.label}</span>
|
||||
</div>
|
||||
<span className="text-[0.625rem] text-muted-foreground uppercase tracking-wide">
|
||||
{p.type}{p.sensitive ? ' · sensitive' : ''}{p.required ? '' : ' · optional'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Metadata form — shown after detection is done (or immediately if no candidates) */}
|
||||
{detectionRan && !showStepper && (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<p className="font-sans text-xs uppercase tracking-widest text-muted-foreground">
|
||||
Template Details
|
||||
</p>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="font-sans text-xs text-muted-foreground mb-1.5 block">
|
||||
Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
className={cn(
|
||||
'w-full rounded-lg px-3 py-2 text-sm',
|
||||
'border border-default bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:outline-none focus:border-primary/30 transition-colors'
|
||||
)}
|
||||
placeholder="Script name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="font-sans text-xs text-muted-foreground mb-1.5 block">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className={cn(
|
||||
'w-full rounded-lg px-3 py-2 text-sm resize-none',
|
||||
'border border-default bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:outline-none focus:border-primary/30 transition-colors'
|
||||
)}
|
||||
placeholder="What does this script do?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label className="font-sans text-xs text-muted-foreground mb-1.5 block">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
value={categoryId}
|
||||
onChange={(e) => setCategoryId(e.target.value)}
|
||||
className={cn(
|
||||
'w-full rounded-lg px-3 py-2 text-sm',
|
||||
'border border-default bg-card text-foreground',
|
||||
'focus:outline-none focus:border-primary/30 transition-colors'
|
||||
)}
|
||||
>
|
||||
<option value="">No category</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Share with team */}
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={shareWithTeam}
|
||||
onChange={(e) => setShareWithTeam(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-border bg-card text-orange-500 focus:ring-orange-500/20"
|
||||
/>
|
||||
<span className="text-sm text-foreground">Share with team</span>
|
||||
</label>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<p className="text-xs text-rose-400">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Save button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canSave}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-sm font-semibold transition-all',
|
||||
'bg-primary text-white hover:brightness-110 active:scale-[0.98]',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isSaving && <Loader2 size={14} className="animate-spin" />}
|
||||
{isSaving ? 'Saving...' : 'Save to Library'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the component compiles**
|
||||
|
||||
Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30`
|
||||
|
||||
Expected: No errors in `ParameterizeAndSavePanel.tsx`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
|
||||
git commit -m "feat: add ParameterizeAndSavePanel component
|
||||
|
||||
Slide-in panel for saving scripts to library with parameter detection,
|
||||
stepper review, template rewriting, and metadata collection. Supports
|
||||
both script mode (from AI builder) and paste mode (raw script import).
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Frontend — Wire `ParameterizeAndSavePanel` into `ScriptBuilderPage`
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/pages/ScriptBuilderPage.tsx`
|
||||
|
||||
Replace the `SaveToLibraryDialog` import and usage with `ParameterizeAndSavePanel`.
|
||||
|
||||
- [ ] **Step 1: Update imports**
|
||||
|
||||
In `frontend/src/pages/ScriptBuilderPage.tsx`, replace the `SaveToLibraryDialog` import:
|
||||
|
||||
```typescript
|
||||
// REMOVE this line:
|
||||
import { SaveToLibraryDialog } from '@/components/script-builder/SaveToLibraryDialog'
|
||||
|
||||
// ADD this line:
|
||||
import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel'
|
||||
```
|
||||
|
||||
Also add `scriptBuilderApi` to the existing import if not already there (it is already imported on line 8):
|
||||
|
||||
```typescript
|
||||
import { scriptBuilderApi } from '@/api'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the save handler function**
|
||||
|
||||
Inside the `ScriptBuilderPage` component, replace `handleSaved`:
|
||||
|
||||
```typescript
|
||||
const handleSaved = async (payload: {
|
||||
name: string
|
||||
description: string | undefined
|
||||
category_id: string | undefined
|
||||
share_with_team: boolean
|
||||
script_body: string
|
||||
parameters_schema: { parameters: import('@/types').ScriptParameter[] }
|
||||
}) => {
|
||||
if (!session) return
|
||||
await scriptBuilderApi.saveToLibrary(session.id, {
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
category_id: payload.category_id,
|
||||
share_with_team: payload.share_with_team,
|
||||
script_body: payload.script_body,
|
||||
parameters_schema: payload.parameters_schema,
|
||||
})
|
||||
setShowSaveDialog(false)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace the dialog JSX with the panel**
|
||||
|
||||
Replace the `SaveToLibraryDialog` JSX block (lines 194-200) with:
|
||||
|
||||
```tsx
|
||||
{/* Save panel */}
|
||||
{showSaveDialog && session && session.latest_script && (
|
||||
<ParameterizeAndSavePanel
|
||||
scriptBody={session.latest_script}
|
||||
language={session.language}
|
||||
defaultName={defaultSaveName}
|
||||
onSave={handleSaved}
|
||||
onClose={() => setShowSaveDialog(false)}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify it compiles**
|
||||
|
||||
Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30`
|
||||
|
||||
Expected: No type errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/pages/ScriptBuilderPage.tsx
|
||||
git commit -m "feat: wire ParameterizeAndSavePanel into ScriptBuilderPage
|
||||
|
||||
Replace SaveToLibraryDialog with the new panel that includes parameter
|
||||
detection, review, and template rewriting before saving to library.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Frontend — Add "New from Script" button to `ScriptLibraryPage`
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/pages/ScriptLibraryPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Add imports**
|
||||
|
||||
Add these imports to the top of `ScriptLibraryPage.tsx`:
|
||||
|
||||
```typescript
|
||||
import { FileUp } from 'lucide-react'
|
||||
import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel'
|
||||
import { scriptsApi } from '@/api'
|
||||
import type { ScriptParameter } from '@/types'
|
||||
```
|
||||
|
||||
Update the existing lucide import to include `FileUp` alongside `Terminal`, `Settings`, `Wand2`:
|
||||
|
||||
```typescript
|
||||
import { Terminal, Settings, Wand2, FileUp } from 'lucide-react'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add state and handler for the import panel**
|
||||
|
||||
Inside `ScriptLibraryPage`, add state and a save handler:
|
||||
|
||||
```typescript
|
||||
const [showImportPanel, setShowImportPanel] = useState(false)
|
||||
|
||||
const handleImportSave = async (payload: {
|
||||
name: string
|
||||
description: string | undefined
|
||||
category_id: string | undefined
|
||||
share_with_team: boolean
|
||||
script_body: string
|
||||
parameters_schema: { parameters: ScriptParameter[] }
|
||||
}) => {
|
||||
// createTemplate requires category_id — if user didn't pick one,
|
||||
// fall back to the first available category
|
||||
const categoryId = payload.category_id || categories[0]?.id
|
||||
if (!categoryId) {
|
||||
throw new Error('No categories available. Please create a category first.')
|
||||
}
|
||||
await scriptsApi.createTemplate({
|
||||
category_id: categoryId,
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
script_body: payload.script_body,
|
||||
parameters_schema: payload.parameters_schema,
|
||||
})
|
||||
setShowImportPanel(false)
|
||||
// Reload templates to show the newly created one
|
||||
const filters = activeTab === 'mine' ? { mine: true } : { shared: true }
|
||||
loadTemplates(filters)
|
||||
}
|
||||
```
|
||||
|
||||
Note: `categories` needs to be read from the store. Add this line alongside the other store selectors:
|
||||
|
||||
```typescript
|
||||
const categories = useScriptGeneratorStore(s => s.categories)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the "New from Script" button**
|
||||
|
||||
In the page header section, next to the "Manage Templates" link, add:
|
||||
|
||||
```tsx
|
||||
{isEngineer && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Link
|
||||
to="/scripts/manage"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-primary bg-accent-dim hover:bg-primary/15 px-2.5 py-1 rounded-full transition-colors group"
|
||||
>
|
||||
<Settings size={12} className="group-hover:rotate-90 transition-transform duration-300" />
|
||||
Manage Templates
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowImportPanel(true)}
|
||||
className="inline-flex items-center gap-1.5 text-xs text-primary bg-accent-dim hover:bg-primary/15 px-2.5 py-1 rounded-full transition-colors"
|
||||
>
|
||||
<FileUp size={12} />
|
||||
New from Script
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
This replaces the existing `{isEngineer && (...)}` block that only had the Manage Templates link.
|
||||
|
||||
- [ ] **Step 4: Add the panel JSX**
|
||||
|
||||
At the end of the component return, before the closing `</div>`, add:
|
||||
|
||||
```tsx
|
||||
{/* Import script panel */}
|
||||
{showImportPanel && (
|
||||
<ParameterizeAndSavePanel
|
||||
onSave={handleImportSave}
|
||||
onClose={() => setShowImportPanel(false)}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify it compiles**
|
||||
|
||||
Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30`
|
||||
|
||||
Expected: No type errors.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/pages/ScriptLibraryPage.tsx
|
||||
git commit -m "feat: add 'New from Script' button to ScriptLibraryPage
|
||||
|
||||
Opens the ParameterizeAndSavePanel in paste mode, letting users import
|
||||
raw scripts with parameter detection and review before saving to library.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Frontend — Delete `SaveToLibraryDialog` and clean up imports
|
||||
|
||||
**Files:**
|
||||
- Delete: `frontend/src/components/script-builder/SaveToLibraryDialog.tsx`
|
||||
|
||||
- [ ] **Step 1: Verify no other files import `SaveToLibraryDialog`**
|
||||
|
||||
Run: `grep -r "SaveToLibraryDialog" frontend/src/ --include="*.ts" --include="*.tsx" | grep -v "node_modules"`
|
||||
|
||||
Expected: No results (ScriptBuilderPage was updated in Task 4).
|
||||
|
||||
- [ ] **Step 2: Delete the file**
|
||||
|
||||
```bash
|
||||
rm frontend/src/components/script-builder/SaveToLibraryDialog.tsx
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run full build check**
|
||||
|
||||
Run: `cd /home/coder/resolutionflow/frontend && npm run build 2>&1 | tail -20`
|
||||
|
||||
Expected: Build succeeds with no errors. This is the strictest check (`tsc -b` enforces `noUnusedLocals`).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -u frontend/src/components/script-builder/SaveToLibraryDialog.tsx
|
||||
git commit -m "chore: delete SaveToLibraryDialog, replaced by ParameterizeAndSavePanel
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Full build verification and final commit
|
||||
|
||||
**Files:** None (verification only)
|
||||
|
||||
- [ ] **Step 1: Run frontend build**
|
||||
|
||||
Run: `cd /home/coder/resolutionflow/frontend && npm run build 2>&1 | tail -20`
|
||||
|
||||
Expected: Build succeeds. This catches any `noUnusedLocals`/`noUnusedParameters` errors the earlier `tsc --noEmit` might miss.
|
||||
|
||||
- [ ] **Step 2: Verify backend imports are clean**
|
||||
|
||||
Run: `cd /home/coder/resolutionflow/backend && source venv/bin/activate && python -c "from app.api.endpoints.script_builder import router; print('OK')"`
|
||||
|
||||
Expected: `OK`
|
||||
|
||||
- [ ] **Step 3: Verify git status is clean**
|
||||
|
||||
Run: `git status`
|
||||
|
||||
Expected: On branch `feat/task-lane-persistence`, working tree clean (all changes committed in Tasks 1-6).
|
||||
Reference in New Issue
Block a user