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 0008ee63..0b1a89b4 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
@@ -249,9 +249,9 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
- Renders an "All" tab first (calls `setCategory(null)`, active when `activeCategoryId === null`), followed by one tab per category
- Category tabs: pill style, `bg-primary/10` + left 3px cyan accent bar on active tab
-- Search: local `inputValue` state controls the `` value (NOT `searchQuery` from store — avoids input lag during debounce). `useEffect` watches `inputValue`, schedules `setTimeout(() => setSearch(inputValue), 300)`, clears timeout on cleanup
-- Reads `categories`, `activeCategoryId` from store; does NOT use `searchQuery` from store to control the input
-- Exposes a `onClearSearch` callback (or reads a `clearSearch` prop) — see `ScriptTemplateList` below for why. Simplest implementation: `ScriptLibraryPage` passes a `clearSearch` callback to both components; `ScriptFilterBar` exposes it as a function ref or the page wires `setInputValue` via a `useRef`
+- Receives `inputValue: string` and `setInputValue: (v: string) => void` as props from `ScriptLibraryPage` (state is lifted — `ScriptFilterBar` does NOT own local search state)
+- `` is controlled by `inputValue` prop. `useEffect` inside `ScriptFilterBar` watches `inputValue`, schedules `setTimeout(() => setSearch(inputValue), 300)`, clears timeout on cleanup — debounce lives here but the value lives in the page
+- Reads `categories`, `activeCategoryId` from store
**`ScriptTemplateList`**
@@ -260,7 +260,7 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
- Accepts `inputValue: string` and `onClearSearch: () => void` props from `ScriptLibraryPage`
- Shows 3 skeleton placeholder cards while `isLoadingTemplates` is true
- Shows "No templates found" empty state when `templates.length === 0` and `!isLoadingTemplates` and `inputValue === ''`
-- Shows "No templates match your search" + "Clear search" button when `templates.length === 0` and `!isLoadingTemplates` and `inputValue !== ''`. "Clear search" calls `onClearSearch()` which resets both `inputValue` in `ScriptFilterBar` and `searchQuery` in the store. Using `inputValue` (not `store.searchQuery`) avoids the 300ms debounce lag in empty-state detection
+- Shows "No templates match your search" + "Clear search" button when `templates.length === 0` and `!isLoadingTemplates` and `inputValue !== ''`. "Clear search" calls `onClearSearch()` — a callback prop from `ScriptLibraryPage` defined as `() => { setInputValue(''); store.setSearch(''); }`. Using `inputValue` (not `store.searchQuery`) avoids the 300ms debounce lag in empty-state detection
**`TemplateCard`**
@@ -274,13 +274,14 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
**`ScriptGeneratorPanel`**
- Shows placeholder (Terminal icon + "Select a template to get started") when `selectedTemplate` is null
-- Shows spinner overlay when `isLoadingDetail` is true (template list remains visible behind it)
-- When template selected and `!isLoadingDetail`: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, action bar
-- Action bar (visible when template selected):
- - Generate button (`bg-gradient-brand`) — calls `generate()`; shows spinner + disabled while `isGenerating`
+- Shows a full-panel centered spinner when `isLoadingDetail` is true, replacing the panel content (not an overlay — no prior template content shown while loading). The template list column remains fully visible and interactive
+- When template selected and `!isLoadingDetail`: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, action bar (in that order, top to bottom)
+- 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"`
- `generateError` shown as rose-500 text inline below action bar
-- `generationWarnings` shown as amber-400 callout above the preview when `generationWarnings.length > 0`
- 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`
**`ScriptParameterForm`**
@@ -310,9 +311,10 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
- Two modes determined by `generatedScript` in store:
- **Draft mode** (`generatedScript === null`): client-side `{{key}}` substitution in `selectedTemplate.script_body`. Sensitive params (`parameter.sensitive === true`) rendered as `****` regardless of `paramValues` value. Unfilled non-sensitive params left as `{{key}}` for `PowerShellHighlighter` to color amber
- **Generated mode** (`generatedScript !== null`): shows `generatedScript`
+- The component computes `displayScript` as a local variable (the substituted preview string in draft mode, or `generatedScript` in generated mode). Both the render and the copy handler reference this same local variable — no separate store field needed
- Copy icon (Lucide `Copy`, 14px) in top-right corner of code block, visible in both modes:
- - On click: copies the currently displayed script string (preview in draft mode, generated script in generated mode) via `navigator.clipboard.writeText()`. On success: shows "Copied!" for 2s then resets. On failure: silently fails — no error displayed, icon does not change state
-- Passes current script string to `PowerShellHighlighter`
+ - On click: calls `navigator.clipboard.writeText(displayScript)`. On success: shows "Copied!" for 2s then resets. On failure: silently fails — no error displayed, icon does not change state
+- Passes `displayScript` to `PowerShellHighlighter`
**`PowerShellHighlighter`**
@@ -330,6 +332,8 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
7. Keywords: /\b(if|else|elseif|foreach|for|while|function|return|try|catch|finally|param|switch)\b/
```
+ Note: variables (priority 4) consume tokens like `$foreach` before keywords (priority 7) can match — this is intentional. Do not reorder the alternation without reviewing this interaction.
+
Token colors:
- Comments → `text-[#8b949e]`
- String literals → `text-[#a5d6ff]`
@@ -358,9 +362,9 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
| ------ | ------------ |
| View Script Library page | Any authenticated user |
| Browse templates, see draft preview | Any authenticated user |
-| Fill form, generate, copy, download | Engineer, owner, or super_admin (i.e. `!isViewer`) |
+| Fill form, generate, copy, download | Engineer, owner, or super_admin |
-`usePermissions()` is called once in `ScriptGeneratorPanel`. Derive `canGenerate = isEngineer` (which returns `true` for engineer, owner, and super_admin via the role hierarchy — matching "Engineer or above" intent more explicitly than `!isViewer`). Pass `canGenerate` as a prop down to `ScriptParameterForm`. Leaf components (`ScriptParameterField`) receive `disabled` as a prop — they do not call `usePermissions()` directly.
+`usePermissions()` is called once in `ScriptGeneratorPanel`. Derive `canGenerate = isEngineer` (`usePermissions().isEngineer` returns `true` for engineer, owner, and super_admin via the role hierarchy check — equivalent to `!isViewer` given the four-role system, but `isEngineer` is more semantically explicit). Pass `canGenerate` as a prop down to `ScriptParameterForm`. Leaf components (`ScriptParameterField`) receive `disabled` as a prop — they do not call `usePermissions()` directly.
Viewer-blocking is frontend-only in Phase 2 (backend role guard deferred — known limitation, documented in Architecture section).
@@ -374,7 +378,7 @@ Viewer-blocking is frontend-only in Phase 2 (backend role guard deferred — kno
- 300ms debounce in `ScriptFilterBar`: local `inputValue` state, `useEffect` + `setTimeout`/`clearTimeout`
- `setSearch` is called once after the debounce delay; it immediately calls `loadTemplates()`
- Category filter and search compose: `loadTemplates()` sends both `category_slug` and `search` simultaneously
-- `inputValue` is lifted to `ScriptLibraryPage` (as state) and passed as a prop to both `ScriptFilterBar` (controls the input) and `ScriptTemplateList` (drives empty-state variant). `onClearSearch` callback (also from `ScriptLibraryPage`) resets `inputValue` to `''` and calls `setSearch('')`
+- `inputValue` is owned by `ScriptLibraryPage` (lifted state). `ScriptLibraryPage` passes it as a prop to `ScriptFilterBar` (which controls the ``) and to `ScriptTemplateList` (which uses it to choose the correct empty-state variant). `onClearSearch` — also defined in `ScriptLibraryPage` as `() => { setInputValue(''); store.setSearch(''); }` — is passed to `ScriptTemplateList` for the "Clear search" button
---