diff --git a/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md b/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md index 0b1a89b4..208aeefc 100644 --- a/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md +++ b/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md @@ -73,14 +73,14 @@ interface ScriptGeneratorState { - `setCategory` updates `activeCategoryId`, then calls `loadTemplates()` - `setSearch` updates `searchQuery`, then calls `loadTemplates()`. The component (not the store) debounces its call to `setSearch` — `setSearch` always calls `loadTemplates()` immediately on invocation -- `loadTemplates()` resolves the slug from the `categories` array (`categories.find(c => c.id === activeCategoryId)?.slug`) before sending `{ category_slug, search }` to the API. When `activeCategoryId` is `null` ("All"), `category_slug` is omitted from the request +- `loadTemplates()` resolves the slug from the `categories` array (`categories.find(c => c.id === activeCategoryId)?.slug`) before sending `{ category_slug, search }` to the API. When `activeCategoryId` is `null` ("All"), `category_slug` is omitted from the request. Prerequisite: `loadCategories()` must complete before `loadTemplates()` or `setCategory()` can resolve slugs — the page bootstrap calls them in this order - `selectTemplate(id)` workflow: 1. Sets `isLoadingDetail: true` (does NOT touch `isLoadingTemplates`) 2. Fetches `ScriptTemplateDetail` from the API 3. In a single `set()` call: sets `selectedTemplate`, populates `paramValues` by converting each `parameter.default` to string (`null` → `''`, `true` → `'true'`, `false` → `'false'`, numbers → `String(n)`), clears `formErrors`, `generatedScript`, `generationId`, `generationWarnings`, `generateError`, sets `isLoadingDetail: false` 4. The template list remains fully visible and interactive throughout - `reset()` clears exactly: `paramValues`, `formErrors`, `generatedScript`, `generationId`, `generationWarnings`, `generateError`. Does NOT clear `selectedTemplate`, `categories`, `templates`, or any browsing state. Not called by `selectTemplate()` — that action handles its own inline clear. Exposed as a public store action for Phase 3 callers -- `validate()`: if `selectedTemplate` is `null`, returns `true` immediately (nothing to validate). Otherwise iterates `selectedTemplate.parameters_schema.parameters`, checks `required && !paramValues[key]` for each, writes errors to `formErrors` via `set()`, returns `false` if any required param is missing, `true` otherwise +- `validate()`: if `selectedTemplate` is `null`, returns `true` immediately (nothing to validate). Otherwise iterates `(selectedTemplate.parameters_schema as ScriptParametersSchema).parameters` (cast required — backend types `parameters_schema` as `dict`; see `ScriptTemplateDetail` type comment), checks `required && !paramValues[key]` for each, writes errors to `formErrors` via `set()`, returns `false` if any required param is missing, `true` otherwise. Client-side validation of `ScriptParameterValidation` fields (`pattern`, `min_length`, `max_length`, `min_value`, `max_value`) is NOT implemented in Phase 2 — only required-field presence is checked. The backend validates these constraints on `POST /scripts/generate` and returns errors in `detail`. Phase 2 does not render per-field errors from the backend — all backend validation errors surface as a single `generateError` below the action bar - `generate(sessionId?)`: if `selectedTemplate` is `null`, returns immediately (no-op). Otherwise calls `validate()` first — if it returns `false`, stops (errors are already in store). If valid, sets `isGenerating: true`, clears `generateError`, calls `POST /scripts/generate`. On success: sets `generatedScript`/`generationId`/`generationWarnings`, sets `isGenerating: false`. On error: extracts `error.response?.data?.detail` (FastAPI detail string) or falls back to `'Failed to generate script'`, sets `generateError`, sets `isGenerating: false` - On generate success: `generatedScript` = `response.script`, `generationId` = `response.id`, `generationWarnings` = `response.warnings` @@ -158,8 +158,8 @@ export interface ScriptTemplateDetail extends ScriptTemplateListItem { // return ((detail.parameters_schema as ScriptParametersSchema)?.parameters ?? []) // } parameters_schema: ScriptParametersSchema; - default_values: Record; - validation_rules: Record; + default_values: Record; // template-level metadata from backend; not used by Phase 2 UI + validation_rules: Record; // template-level metadata from backend; not used by Phase 2 UI version: number; created_at: string; updated_at: string; @@ -279,37 +279,39 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx - Visual order within the panel (top to bottom): warnings callout → `ScriptPreview` → action bar → error text - `generationWarnings` shown as amber-400 callout above the preview when `generationWarnings.length > 0` - Action bar (below preview): - - Generate button (`bg-gradient-brand`) — calls `generate()` with no arguments (Phase 3 will pass `sessionId`); shows spinner + disabled while `isGenerating` - - Download `.ps1` button — disabled when `generatedScript` is null; on click triggers `new Blob([generatedScript], { type: 'text/plain' })` → programmatic anchor click with `download="${selectedTemplate.slug}.ps1"` + - Generate button (`bg-gradient-brand`) — calls `generate()` with no arguments (Phase 3 will pass `sessionId`); disabled when `isGenerating` OR `!canGenerate`; shows spinner while `isGenerating` + - Download `.ps1` button — disabled when `generatedScript` is null OR `!canGenerate`; on click triggers `new Blob([generatedScript], { type: 'text/plain' })` → programmatic anchor click with `download="${selectedTemplate.slug}.ps1"` - `generateError` shown as rose-500 text inline below action bar -- Permission check: call `usePermissions()` directly in this component; derive `canGenerate = !isViewer`. Pass `canGenerate` as a prop down to `ScriptParameterForm`. Generate and Download buttons disabled with tooltip "Engineer access required" when `!canGenerate` +- Permission check: call `usePermissions()` directly in this component; derive `canGenerate = isEngineer`. Pass `canGenerate` as a prop down to `ScriptParameterForm`. Generate and Download buttons disabled with tooltip "Engineer access required" when `!canGenerate` **`ScriptParameterForm`** - Accepts `canGenerate: boolean` prop from `ScriptGeneratorPanel` -- Iterates `selectedTemplate.parameters_schema.parameters` sorted by `order` +- Iterates `selectedTemplate.parameters_schema.parameters` sorted by `order`. Access via cast: `(selectedTemplate.parameters_schema as ScriptParametersSchema).parameters` — `parameters_schema` arrives as `dict` at runtime (backend types it as `dict`; helper comment in `ScriptTemplateDetail` type definition) - Groups by `group` field: renders a `font-label uppercase text-muted-foreground` section label before each group boundary (parameters with `group: null` rendered ungrouped at the top) -- Renders a `ScriptParameterField` per parameter, passing `canGenerate` down +- Renders a `ScriptParameterField` per parameter, passing `disabled={!canGenerate}` (converts `canGenerate` boolean to the `disabled` prop expected by `ScriptParameterField`) - Does NOT own the Generate button and does NOT call `generate()` or `validate()` directly — validation is triggered from `ScriptGeneratorPanel` via the store's `generate()` action (which calls `validate()` internally) **`ScriptParameterField`** - Accepts `param: ScriptParameter`, `value: string`, `error: string | undefined`, `disabled: boolean` -- Renders input by `type`. Pass `error` prop to shared components (they render their own error message — do NOT add a separate error `

` below): - - `text` → `` - - `password` → `` with Lucide `Eye`/`EyeOff` toggle - - `textarea` → `