@blundergoat/goat-flow
Version:
AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.
784 lines (764 loc) • 32.3 kB
HTML
<!-- ═══ Prompts View ═══ -->
<div
x-show="activeView === 'prompts'"
x-cloak
style="height: calc(100vh - 3.5rem); overflow-y: auto"
>
<div style="max-width: 1100px; margin: 0 auto; padding: 20px 24px 24px">
<div style="margin-bottom: 14px">
<h2
class="gf-text-primary"
style="font-size: 18px; font-weight: 600; margin-bottom: 4px"
>
Prompts
</h2>
<p class="gf-text-muted" style="font-size: 14px; max-width: 760px">
Ready-made prompts for goat-flow skills. Pick one, optionally add
context, and launch it in the Runner selected above.
</p>
</div>
<div style="position: relative; margin-bottom: 10px">
<svg
style="
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-dim);
"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"
/>
</svg>
<input
x-model="presetSearch"
x-ref="presetSearchInput"
class="gf-input"
style="
width: 100%;
box-sizing: border-box;
padding: 9px 14px 9px 38px;
border-radius: 6px;
font-size: 14px;
"
placeholder="Search name, description, or prompt text..."
/>
</div>
<div
style="
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
margin-bottom: 14px;
"
>
<button
@click="openNewCustomPrompt()"
class="gf-btn gf-btn-sm gf-btn-primary"
>
New Custom
</button>
<button
class="gf-btn gf-btn-sm"
:style="presetFilter === 'favorites' ? { background: 'var(--accent-bg)', borderColor: 'var(--accent-border)', color: 'var(--accent)' } : {}"
@click="presetFilter = 'favorites'"
>
★ Favorites
<span
x-text="allPresets.filter(p => !p.qualityMode && !p.internalOnly && presetFavorites.includes(p.id)).length"
></span>
</button>
<template x-for="cat in presetCats" :key="'mock-cat-' + cat.id">
<button
x-show="cat.id !== 'favorites'"
class="gf-btn gf-btn-sm"
@click="presetFilter = cat.id"
:style="presetFilter === cat.id
? { background: { all: 'var(--accent-bg)', debug: 'rgba(96,165,250,0.15)', review: 'rgba(45,212,191,0.15)', plan: 'rgba(251,191,36,0.15)', critique: 'rgba(167,139,250,0.15)', qa: 'rgba(244,114,182,0.15)', security: 'rgba(248,113,113,0.15)' }[cat.id] || 'rgba(148,163,184,0.08)',
color: { all: 'var(--accent)', debug: '#60a5fa', review: '#2dd4bf', plan: '#fbbf24', critique: '#a78bfa', qa: '#f472b6', security: '#f87171' }[cat.id] || 'var(--text-primary)',
borderColor: { all: 'var(--accent-border)', debug: 'rgba(96,165,250,0.3)', review: 'rgba(45,212,191,0.3)', plan: 'rgba(251,191,36,0.3)', critique: 'rgba(167,139,250,0.3)', qa: 'rgba(244,114,182,0.3)', security: 'rgba(248,113,113,0.3)' }[cat.id] || 'var(--border-default)' }
: {}"
x-text="cat.id === 'all' ? `All ${allPresets.filter(p => !p.qualityMode && !p.internalOnly).length}` : `${cat.label} ${allPresets.filter(p => p.cat === cat.id && !p.qualityMode && !p.internalOnly).length}`"
></button>
</template>
</div>
<div
class="gf-prompts-layout"
style="
display: grid;
grid-template-columns: minmax(0, 290px) minmax(0, 1fr);
gap: 12px;
"
>
<!-- Left prompt list -->
<div
style="
display: flex;
flex-direction: column;
gap: 6px;
max-height: 680px;
overflow-y: auto;
padding-right: 4px;
"
>
<template x-for="p in filteredPresets" :key="'mock-preset-' + p.id">
<div
@click="selectPreset(p)"
class="gf-card gf-prompt-list-card"
:style="{
...(selectedPreset?.id === p.id ? { borderColor: 'var(--accent-border)', background: 'var(--accent-bg)' } : {}),
borderLeftColor: presetCategoryAccent(p)
}"
>
<button
@click.stop="toggleFavorite(p.id)"
class="gf-prompt-favorite"
:style="isFavorite(p.id) ? { color: 'var(--amber-400)' } : {}"
:title="isFavorite(p.id) ? 'Remove from favorites' : 'Add to favorites'"
x-text="isFavorite(p.id) ? '★' : '☆'"
></button>
<div class="gf-prompt-card-content">
<div
class="gf-text-primary gf-prompt-card-title"
x-text="p.name"
></div>
<p class="gf-text-muted gf-prompt-card-desc" x-text="p.desc"></p>
<div class="gf-prompt-card-meta">
<span
class="gf-text-secondary gf-prompt-route-chip"
x-text="presetRouteLabel(p)"
></span>
<span
class="gf-text-muted gf-prompt-category-label"
x-text="p.cat"
></span>
<template
x-for="badge in presetBadges(p).slice(0, 3)"
:key="p.id + '-' + badge.label"
>
<span
:class="'gf-preset-badge gf-preset-badge-' + badge.tone"
:title="badge.title"
x-text="badge.label"
></span>
</template>
</div>
</div>
</div>
</template>
</div>
<!-- Right preview -->
<div
class="gf-card"
style="padding: 18px 20px; min-width: 0"
x-data="{ extraContext: '' }"
>
<template x-if="showCustomPromptEditor">
<form
class="gf-custom-prompt-form"
novalidate
@submit.prevent="saveCustomPrompt()"
>
<div class="gf-custom-form-head">
<div>
<div class="section-label">Custom prompt</div>
<h3
class="gf-text-primary"
x-text="editingCustomPromptId ? 'Edit custom prompt' : 'New custom prompt'"
></h3>
</div>
<div class="gf-custom-form-head-actions">
<div class="gf-start-picker">
<button
type="button"
class="gf-btn gf-btn-sm gf-btn-text gf-start-picker-trigger"
@click="showPromptStartPicker = !showPromptStartPicker"
aria-controls="custom-prompt-start-picker"
:aria-expanded="showPromptStartPicker ? 'true' : 'false'"
>
Start from existing prompt ▾
</button>
<div
x-show="showPromptStartPicker"
x-cloak
id="custom-prompt-start-picker"
class="gf-start-picker-panel"
>
<label class="sr-only" for="custom-prompt-start">
Existing prompt
</label>
<select
id="custom-prompt-start"
x-model="customPromptStartId"
class="gf-input"
>
<option value="">Choose a prompt...</option>
<template
x-for="p in allPresets.filter(p => !p.qualityMode && !p.internalOnly)"
:key="'start-' + p.id"
>
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
<button
type="button"
class="gf-btn gf-btn-sm gf-btn-secondary"
@click="startCustomPromptFromPreset()"
:disabled="!customPromptStartId"
>
Use prompt
</button>
</div>
</div>
<div class="gf-custom-form-actions">
<button
type="button"
@click="cancelCustomPromptEdit()"
class="gf-btn gf-btn-md gf-btn-text"
>
Cancel
</button>
<button
type="submit"
class="gf-btn gf-btn-md gf-btn-secondary gf-custom-save-secondary"
>
Save
</button>
<button
type="button"
@click="saveAndRunCustomPrompt()"
:disabled="!activeRunner || launching || serverSessions.length >= serverMaxSessions"
:title="!activeRunner ? 'Select a runner before launching' : (serverSessions.length >= serverMaxSessions ? 'Maximum sessions reached' : '')"
class="gf-btn gf-btn-md gf-btn-primary gf-custom-run-primary"
>
Save and run with <span x-text="activeRunner"></span>
</button>
</div>
</div>
</div>
<div
x-show="customPromptSubmitAttempted && customPromptErrors().length > 0"
x-cloak
class="gf-validation-summary"
aria-live="polite"
>
<div class="gf-validation-title">Fix these fields</div>
<template
x-for="error in customPromptErrors()"
:key="'custom-error-' + error.field + '-' + error.message"
>
<a
href="#"
@click.prevent="focusCustomPromptField(error.anchor)"
x-text="error.message"
></a>
</template>
</div>
<div class="gf-custom-form-body">
<div class="gf-custom-form-main">
<section class="gf-custom-section">
<h4 class="gf-custom-section-title">Identity</h4>
<div class="gf-custom-grid-2">
<label class="gf-field" for="custom-prompt-name">
<span class="section-label">Name <span>*</span></span>
<input
id="custom-prompt-name"
x-model="customPromptDraft.name"
class="gf-input"
placeholder="Custom review prompt"
required
aria-required="true"
:aria-invalid="customPromptFieldError('name') ? 'true' : 'false'"
aria-describedby="custom-prompt-name-help custom-prompt-name-error"
/>
<span id="custom-prompt-name-help" class="gf-field-help">
Used as the display name. The stored id is generated
from it.
</span>
<span
id="custom-prompt-name-error"
class="gf-field-error"
x-show="customPromptFieldError('name') && (customPromptSubmitAttempted || customPromptDraft.name.trim())"
x-text="customPromptFieldError('name')"
></span>
</label>
<div
class="gf-field"
id="custom-prompt-route"
tabindex="-1"
>
<span class="section-label">Route <span>*</span></span>
<div
class="gf-route-pills"
role="radiogroup"
aria-label="Custom prompt route"
aria-required="true"
aria-describedby="custom-prompt-route-help custom-prompt-route-error"
>
<template
x-for="route in customPromptRouteOptions()"
:key="'route-' + route.id"
>
<button
type="button"
class="gf-route-pill"
:class="customPromptDraft.route === route.id ? 'is-active' : ''"
:aria-pressed="customPromptDraft.route === route.id ? 'true' : 'false'"
@click="customPromptDraft.route = route.id"
x-text="route.label"
></button>
</template>
</div>
<span
id="custom-prompt-route-help"
class="gf-field-help"
x-text="selectedCustomPromptRoute().desc"
></span>
<span
id="custom-prompt-route-error"
class="gf-field-error"
x-show="customPromptSubmitAttempted && customPromptFieldError('route')"
x-text="customPromptFieldError('route')"
></span>
</div>
</div>
<label class="gf-field" for="custom-prompt-desc">
<span class="section-label">Description (optional)</span>
<input
id="custom-prompt-desc"
x-model="customPromptDraft.desc"
class="gf-input"
placeholder="Short note shown in the prompt list"
aria-describedby="custom-prompt-desc-help"
/>
<span id="custom-prompt-desc-help" class="gf-field-help">
Shown under the name in the prompt list - keep to one
line.
</span>
</label>
</section>
<section class="gf-custom-section">
<h4
id="custom-prompt-body-heading"
class="gf-custom-section-title"
>
Prompt <span>*</span>
</h4>
<div class="gf-field">
<textarea
id="custom-prompt-body"
x-model="customPromptDraft.prompt"
class="gf-input gf-custom-prompt-textarea"
placeholder="/goat-review review the selected target project..."
required
aria-required="true"
aria-labelledby="custom-prompt-body-heading"
:aria-invalid="customPromptFieldError('prompt') ? 'true' : 'false'"
aria-describedby="custom-prompt-body-error custom-prompt-body-count custom-prompt-body-warning"
></textarea>
<span
id="custom-prompt-body-error"
class="gf-field-error"
x-show="customPromptSubmitAttempted && customPromptFieldError('prompt')"
x-text="customPromptFieldError('prompt')"
></span>
<span
id="custom-prompt-body-warning"
class="gf-field-warning"
x-show="customPromptWarning()"
x-text="customPromptWarning()"
></span>
<span
id="custom-prompt-body-count"
class="gf-count-line"
x-text="`${customPromptDraft.prompt.length} chars · ${customPromptDraft.prompt ? customPromptDraft.prompt.split('\n').length : 0} lines`"
></span>
</div>
</section>
<section class="gf-custom-section">
<h4 class="gf-custom-section-title">Behaviour</h4>
<div class="gf-flag-groups">
<template
x-for="group in customPromptFlagGroups()"
:key="'flag-group-' + group.id"
>
<div class="gf-flag-group">
<div
class="gf-flag-group-title"
x-text="group.label"
></div>
<div class="gf-flag-grid">
<template
x-for="flag in group.flags"
:key="'flag-' + flag.field"
>
<div class="gf-flag-row">
<label
class="gf-check-row"
:for="'custom-flag-' + flag.field"
>
<input
type="checkbox"
:id="'custom-flag-' + flag.field"
x-model="customPromptDraft[flag.field]"
:disabled="customPromptFlagDisabled(flag)"
:aria-describedby="'custom-flag-tip-' + flag.field"
@change="syncCustomPromptFlag(flag)"
/>
<span x-text="flag.label"></span>
<span
x-show="flag.field === 'globalSafe' && !customPromptFlagDisabled(flag)"
class="gf-flag-default-badge"
>default</span
>
</label>
<span class="gf-tooltip-wrap">
<button
type="button"
class="gf-info-button"
@click.stop
:aria-label="flag.label + ' information'"
:aria-describedby="'custom-flag-tip-' + flag.field"
>
?
</button>
<span
class="gf-tooltip"
role="tooltip"
:id="'custom-flag-tip-' + flag.field"
x-text="flag.title"
></span>
</span>
</div>
</template>
</div>
</div>
</template>
</div>
<div class="gf-custom-grid-2">
<div class="gf-field">
<span
id="custom-prompt-surfaces-label"
class="section-label"
>Target surfaces (optional)</span
>
<div
class="gf-tag-input"
id="custom-prompt-surfaces"
role="group"
aria-labelledby="custom-prompt-surfaces-label"
aria-describedby="custom-prompt-surfaces-help"
>
<template
x-for="surface in customPromptSurfaceTags()"
:key="'surface-' + surface"
>
<span class="gf-tag-chip">
<span x-text="surface"></span>
<button
type="button"
@click="removeCustomPromptSurface(surface)"
:aria-label="'Remove ' + surface"
>
×
</button>
</span>
</template>
<input
id="custom-prompt-surface-entry"
x-model="customPromptSurfaceDraft"
@keydown="if ($event.key === 'Enter' || $event.key === ',') { $event.preventDefault(); commitCustomPromptSurfaceDraft(); }"
@blur="commitCustomPromptSurfaceDraft()"
placeholder="Add surface"
aria-label="Add target surface"
aria-describedby="custom-prompt-surfaces-help"
/>
</div>
<span
id="custom-prompt-surfaces-help"
class="gf-field-help"
>
Where this prompt is appropriate. Type to add a new
surface or pick from suggestions.
</span>
<div class="gf-surface-suggestions">
<template
x-for="surface in customPromptSurfaceSuggestions().slice(0, 8)"
:key="'surface-suggestion-' + surface"
>
<button
type="button"
class="gf-suggestion-chip"
@click="addCustomPromptSurface(surface)"
x-text="surface"
></button>
</template>
</div>
</div>
<label class="gf-field" for="custom-prompt-notes">
<span class="section-label">Notes (optional)</span>
<textarea
id="custom-prompt-notes"
x-model="customPromptDraft.notes"
class="gf-input gf-custom-notes"
placeholder="Fallbacks or launch notes"
aria-describedby="custom-prompt-notes-help"
></textarea>
<span id="custom-prompt-notes-help" class="gf-field-help">
Internal fallback notes - not appended before launch.
</span>
</label>
</div>
</section>
</div>
<aside class="gf-custom-preview-pane" aria-label="Live preview">
<div class="section-label">Live preview</div>
<div
class="gf-card gf-prompt-list-card gf-custom-preview-card"
:style="{
borderLeftColor: presetCategoryAccent(customPromptPreview())
}"
>
<span
class="gf-prompt-favorite gf-prompt-favorite-preview"
aria-hidden="true"
>☆</span
>
<div class="gf-prompt-card-content">
<div
class="gf-prompt-card-title"
:class="customPromptDraft.name.trim() ? 'gf-text-primary' : 'gf-text-muted gf-prompt-placeholder'"
x-text="customPromptPreviewName()"
></div>
<p
class="gf-text-muted gf-prompt-card-desc"
:class="customPromptDraft.desc.trim() ? '' : 'gf-prompt-placeholder'"
x-text="customPromptPreviewDescription()"
></p>
<div class="gf-prompt-card-meta">
<span
class="gf-text-secondary gf-prompt-route-chip"
x-text="presetRouteLabel(customPromptPreview())"
></span>
<span
class="gf-text-muted gf-prompt-category-label"
x-text="customPromptPreview().cat"
></span>
<template
x-for="badge in presetBadges(customPromptPreview()).slice(0, 3)"
:key="'preview-' + badge.label"
>
<span
:class="'gf-preset-badge gf-preset-badge-' + badge.tone"
:title="badge.title"
x-text="badge.label"
></span>
</template>
</div>
</div>
</div>
</aside>
</div>
</form>
</template>
<template x-if="!showCustomPromptEditor && selectedPreset">
<div>
<div
style="
display: flex;
gap: 8px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-subtle);
"
>
<button
x-data="{ copied: false }"
@click="copyPreset(selectedPreset.prompt); copied = true; setTimeout(() => copied = false, 1200)"
class="gf-btn gf-btn-md gf-btn-secondary"
>
<span x-text="copied ? 'Copied' : 'Copy'"></span>
</button>
<button
@click="duplicateSelectedCustomPrompt()"
class="gf-btn gf-btn-md gf-btn-secondary"
>
Duplicate
</button>
<template x-if="selectedPreset.source === 'custom'">
<div style="display: flex; gap: 6px">
<button
@click="editSelectedCustomPrompt()"
class="gf-btn gf-btn-md gf-btn-secondary"
>
Edit
</button>
<button
@click="deleteSelectedCustomPrompt()"
class="gf-btn gf-btn-md gf-btn-secondary"
>
Delete
</button>
</div>
</template>
<div style="flex: 1"></div>
<button
@click="launchPreset(extraContext.trim() ? selectedPreset.prompt + '\n\n' + extraContext.trim() : selectedPreset.prompt, activeRunner, selectedPreset.name, { presetId: selectedPreset.id })"
:disabled="launching || serverSessions.length >= 10"
class="gf-btn gf-btn-md gf-btn-primary"
>
Launch with <span x-text="activeRunner"></span>
</button>
</div>
<div
style="
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
"
>
<div style="min-width: 0; flex: 1">
<h3
class="gf-text-primary"
style="font-size: 16px; font-weight: 600; margin-bottom: 8px"
x-text="selectedPreset.name"
></h3>
<div
style="
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
"
>
<span
style="
font-family: var(--font-mono);
font-size: 10px;
padding: 1px 6px;
border-radius: 3px;
background: var(--accent-bg);
color: var(--accent);
border: 1px solid var(--accent-border);
"
x-text="presetRouteLabel(selectedPreset)"
></span>
<span
class="gf-text-muted"
style="
font-size: 11px;
padding: 2px 10px;
border-radius: 999px;
background: rgba(148, 163, 184, 0.08);
"
x-text="selectedPreset.cat"
></span>
<template
x-for="badge in presetBadges(selectedPreset)"
:key="selectedPreset.id + '-' + badge.label"
>
<span
:class="'gf-preset-badge gf-preset-badge-' + badge.tone"
:title="badge.title"
x-text="badge.label"
></span>
</template>
</div>
</div>
<button
@click="toggleFavorite(selectedPreset.id)"
style="
background: transparent;
border: none;
color: var(--amber-400);
cursor: pointer;
"
:title="isFavorite(selectedPreset.id) ? 'Remove from favorites' : 'Add to favorites'"
>
<span
x-text="isFavorite(selectedPreset.id) ? '★' : '☆'"
style="font-size: 18px"
></span>
</button>
</div>
<p
class="gf-text-muted"
style="font-size: 14px; margin-top: 14px; line-height: 1.6"
x-text="selectedPreset.desc"
></p>
<div style="margin-top: 16px">
<div class="section-label" style="margin-bottom: 8px">Prompt</div>
<pre
class="gf-prompt-body"
style="max-height: 260px; overflow-y: auto"
x-text="adaptPrompt(selectedPreset.prompt)"
></pre>
</div>
<div style="margin-top: 14px">
<div
style="
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 8px;
"
>
<div class="section-label">
Extra context
<span
style="
text-transform: none;
letter-spacing: 0;
color: var(--text-dim);
font-weight: 400;
"
>(optional)</span
>
</div>
<span style="font-size: 11px; color: var(--text-dim)"
>Appended before launch</span
>
</div>
<textarea
x-model="extraContext"
class="gf-input"
style="
width: 100%;
min-height: 92px;
padding: 10px 12px;
resize: vertical;
font-family: var(--font-mono);
font-size: 12.5px;
"
placeholder="e.g. Focus on dispatcher routes and ignore integration tests for now..."
></textarea>
</div>
</div>
</template>
<template x-if="!showCustomPromptEditor && !selectedPreset">
<div
style="
height: 100%;
min-height: 420px;
display: flex;
align-items: center;
justify-content: center;
"
>
<p class="gf-text-muted">Select a prompt to preview it.</p>
</div>
</template>
</div>
</div>
</div>
</div>