Files
resolutionflow/docs/superpowers/plans/2026-03-29-parameterize-and-save.md
chihlasm 60e1384a2c 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>
2026-03-29 05:46:40 +00:00

32 KiB

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:

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:

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:

    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:

        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
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:

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:

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
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:

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
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:

// 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):

import { scriptBuilderApi } from '@/api'
  • Step 2: Add the save handler function

Inside the ScriptBuilderPage component, replace handleSaved:

  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:

      {/* 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
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:

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:

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:

  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:

  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:

          {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:

      {/* 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
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
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
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).