Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ DerivedData/
.classpath
.settings/
.devcontainer/
.devcontainer

# JetBrains
*.iws
Expand Down
38 changes: 26 additions & 12 deletions src/lib/components/vault/prompts/PromptPackEditor.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { untrack } from 'svelte'
import type { FullPack } from '$lib/services/packs/types'
import { systemSamples, runtimeSamples } from './sampleContext'
import { getSamplesForTemplate, getSampleAtPath, stringifySample } from './sampleContext'
import { getVariablesForTemplate } from '$lib/services/templates/templateContextMap'
import { packService } from '$lib/services/packs/pack-service'
import { createIsMobile } from '$lib/hooks/is-mobile.svelte'
import TemplateGroupList from './TemplateGroupList.svelte'
Expand Down Expand Up @@ -71,11 +72,18 @@
$effect(() => {
const vars = fullPack?.variables
if (!vars) return
// Build complete defaults: system + runtime samples + custom variable defaults
const defaults: Record<string, string> = { ...systemSamples, ...runtimeSamples }
// Build complete defaults: registry variable samples (by declared path) + custom variable defaults
const samples = selectedTemplateId ? getSamplesForTemplate(selectedTemplateId) : {}
const registryVars = selectedTemplateId ? getVariablesForTemplate(selectedTemplateId) : []
const defaults: Record<string, string> = {}
for (const v of registryVars) {
const sample = getSampleAtPath(samples, v.name)
if (sample === null || sample === undefined) continue
defaults[v.name] = stringifySample(sample)
}
for (const v of vars) {
if (v.defaultValue) {
defaults[v.variableName] = v.defaultValue
defaults['packVariables.' + v.variableName] = v.defaultValue
}
}
// Read current testValues without creating a dependency (avoid infinite loop)
Expand Down Expand Up @@ -308,6 +316,7 @@
{/if}

<VariablePalette
templateId={selectedTemplateId}
iconOnly
customVariables={fullPack?.variables ?? []}
onInsert={(name) => editorRef?.insertVariable(name)}
Expand Down Expand Up @@ -405,6 +414,7 @@
{/if}

<VariablePalette
templateId={selectedTemplateId}
customVariables={fullPack?.variables ?? []}
onInsert={(name) => editorRef?.insertVariable(name)}
/>
Expand Down Expand Up @@ -628,14 +638,18 @@
</Drawer.Root>
{/if}

<!-- Test Variables modal -->
<TestVariablesModal
open={showTestVars}
customVariables={fullPack?.variables ?? []}
{testValues}
onOpenChange={(open) => (showTestVars = open)}
onTestValuesChange={handleTestValuesChange}
/>
<!-- Test Variables modal — only mounted when a template is selected to avoid
rendering an empty palette with no context. -->
{#if selectedTemplateId}
<TestVariablesModal
templateId={selectedTemplateId}
open={showTestVars}
customVariables={fullPack?.variables ?? []}
{testValues}
onOpenChange={(open) => (showTestVars = open)}
onTestValuesChange={handleTestValuesChange}
/>
{/if}

<!-- Dirty guard dialog -->
<Dialog.Root bind:open={showDirtyDialog}>
Expand Down
17 changes: 9 additions & 8 deletions src/lib/components/vault/prompts/TemplateEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import { database } from '$lib/services/database'
import { packService } from '$lib/services/packs/pack-service'
import { validateTemplate } from '$lib/services/templates/validator'
import { variableRegistry } from '$lib/services/templates/variables'
import { getVariablesForTemplate } from '$lib/services/templates/templateContextMap'
import { getSamplesForTemplate } from './sampleContext'
import type { ValidationError, VariableDefinition } from '$lib/services/templates/types'
import type { CustomVariable } from '$lib/services/packs/types'
import type { Completion } from '@codemirror/autocomplete'
import { allSamples } from './sampleContext'
import { AlertTriangle, CircleCheck, FlaskConical } from 'lucide-svelte'
import { Button } from '$lib/components/ui/button'
import TemplatePreview from './TemplatePreview.svelte'
Expand Down Expand Up @@ -123,7 +123,7 @@
vals.textContent = `Values: ${v.enumValues.join(', ')}`
} else if (v.type === 'text' || v.type === 'number') {
// text/number variables show example value from sampleContext
const sample = allSamples[v.name]
const sample = getSamplesForTemplate(templateId)[v.name]
if (sample != null) {
const example = dom.appendChild(document.createElement('p'))
example.className = 'cm-var-hint'
Expand All @@ -138,17 +138,17 @@
// Build variable completions for CodeMirror autocomplete
let completions: Completion[] = $derived.by(() => {
const result: Completion[] = []
for (const v of variableRegistry.getAll()) {
for (const v of getVariablesForTemplate(templateId)) {
result.push({
label: v.name,
type: 'variable',
detail: v.category,
detail: v.type,
info: buildTooltipInfo(v),
})
}
for (const v of customVariables) {
result.push({
label: v.variableName,
label: 'packVariables.' + v.variableName,
type: 'variable',
detail: 'custom',
info: v.displayName,
Expand Down Expand Up @@ -335,8 +335,8 @@
function validateContent(content: string) {
clearTimeout(validationTimer)
validationTimer = setTimeout(() => {
const additionalVarNames = customVariables.map((v) => v.variableName)
const result = validateTemplate(content, additionalVarNames)
const additionalVarNames = customVariables.map((v) => 'packVariables.' + v.variableName)
const result = validateTemplate(content, templateId, additionalVarNames)
validationErrors = result.errors
}, 500)
}
Expand Down Expand Up @@ -494,6 +494,7 @@
class:hidden={isMobile.current && mobileView === 'editor'}
>
<TemplatePreview
{templateId}
content={currentContent}
{customVariables}
hideHeader={isMobile.current}
Expand Down
92 changes: 66 additions & 26 deletions src/lib/components/vault/prompts/TemplatePreview.svelte
Original file line number Diff line number Diff line change
@@ -1,54 +1,91 @@
<script lang="ts">
import { templateEngine } from '$lib/services/templates/engine'
import { variableRegistry } from '$lib/services/templates/variables'
import { allSamples } from './sampleContext'
import { getSamplesForTemplate } from './sampleContext'
import { getVariablesForTemplate } from '$lib/services/templates/templateContextMap'
import type { CustomVariable } from '$lib/services/packs/types'
import type { TemplateContext } from '$lib/services/templates/types'
import type { TemplateContext, VariableType } from '$lib/services/templates/types'
import { AlertTriangle } from 'lucide-svelte'

interface Props {
templateId: string
content: string
customVariables: CustomVariable[]
hideHeader?: boolean
testValues?: Record<string, string>
}

let { content, customVariables, hideHeader = false, testValues }: Props = $props()
let { templateId, content, customVariables, hideHeader = false, testValues }: Props = $props()

function buildSampleContext(
vars: CustomVariable[],
overrides?: Record<string, string>,
): TemplateContext {
// Start with rich sample data (includes structured arrays/objects)
const context: TemplateContext = { ...allSamples }
function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown) {
const parts = path.split('.')
let current: Record<string, unknown> = obj
for (let i = 0; i < parts.length - 1; i++) {
if (!(parts[i] in current) || typeof current[parts[i]] !== 'object') {
current[parts[i]] = {}
}
current = current[parts[i]] as Record<string, unknown>
}
current[parts[parts.length - 1]] = value
}

// Bracket-name fallback only for variables missing from allSamples
for (const v of variableRegistry.getByCategory('system')) {
if (!(v.name in context)) {
context[v.name] = `[${v.name}]`
type ParseResult = { ok: true; value: unknown } | { ok: false; error: string }

function parseOverride(value: string, type: VariableType | undefined): ParseResult {
if (type === 'array' || type === 'object') {
try {
return { ok: true, value: JSON.parse(value) }
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return { ok: false, error: msg }
}
}
if (type === 'number') {
const n = Number(value)
if (!Number.isFinite(n)) {
return { ok: false, error: `'${value}' is not a valid number` }
}
return { ok: true, value: n }
}
for (const v of variableRegistry.getByCategory('runtime')) {
if (type === 'boolean') {
return { ok: true, value: value === 'true' }
}
return { ok: true, value }
}

function buildSampleContext(
vars: CustomVariable[],
overrides: Record<string, string> | undefined,
): { context: TemplateContext; overrideErrors: string[] } {
const context: TemplateContext = { ...getSamplesForTemplate(templateId) }
const registryVars = getVariablesForTemplate(templateId)
const typesByName = new Map(registryVars.map((v) => [v.name, v.type]))
for (const v of registryVars) {
if (!(v.name in context)) {
context[v.name] = `[${v.name}]`
}
}
// Custom pack variables: place under packVariables namespace
for (const v of vars) {
if (!(v.variableName in context)) {
context[v.variableName] = `[${v.displayName}]`
}
const path = 'packVariables.' + v.variableName
setNestedValue(context, path, `[${v.displayName}]`)
}

// Apply test value overrides (string overrides from TestVariablesModal)
const overrideErrors: string[] = []
if (overrides) {
for (const [key, value] of Object.entries(overrides)) {
if (value !== '') {
context[key] = value
if (value === '') continue
const result = parseOverride(value, typesByName.get(key))
if (!result.ok) {
overrideErrors.push(`${key}: ${result.error}`)
continue
}
if (key.includes('.')) {
setNestedValue(context, key, result.value)
} else {
context[key] = result.value
}
}
}

return context
return { context, overrideErrors }
}

// Debounced rendering
Expand All @@ -70,15 +107,18 @@
return
}

const context = buildSampleContext(currentVars, currentTestValues)
const { context, overrideErrors } = buildSampleContext(currentVars, currentTestValues)
const result = templateEngine.render(currentContent, context)

if (result === null) {
previewError = 'Template could not be rendered. Check for syntax errors.'
previewOutput = ''
} else {
previewOutput = result
previewError = ''
previewError =
overrideErrors.length > 0
? `Ignored invalid test override${overrideErrors.length === 1 ? '' : 's'}: ${overrideErrors.join('; ')}`
: ''
}
}, 300)

Expand Down
Loading