UNPKG

@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.

311 lines (310 loc) 11 kB
"use strict"; /** * Prompt-library helpers for the dashboard Alpine app. * These keep filtering, grouping, and prompt text transforms out of app.ts. */ const PRESET_CATEGORY_ACCENTS = { debug: "#60a5fa", review: "#2dd4bf", plan: "#fbbf24", critique: "#a78bfa", qa: "#f472b6", security: "#f87171", custom: "var(--accent)", }; /** Toggle a preset favorite state and persist the combined dashboard state. */ function dashboardToggleFavorite(ctx, id) { const idx = ctx.presetFavorites.indexOf(id); if (idx === -1) ctx.presetFavorites.push(id); else ctx.presetFavorites.splice(idx, 1); ctx._saveDashboardState(); } /** Check whether a preset is marked as a favorite. */ function dashboardIsFavorite(ctx, id) { return ctx.presetFavorites.includes(id); } /** Move the preview selection up (-1) or down (1) in screen order, with wrap. */ function dashboardSelectPresetByOffset(ctx, delta) { const order = ctx.flatPresetOrder; if (order.length === 0) return; const currentId = ctx.selectedPreset?.id; const currentIdx = currentId ? order.indexOf(currentId) : -1; const nextIdx = currentIdx === -1 ? delta > 0 ? 0 : order.length - 1 : (currentIdx + delta + order.length) % order.length; const nextId = order[nextIdx]; const next = dashboardAllPresets(ctx).find((p) => p.id === nextId); if (!next) return; ctx.selectedPreset = next; requestAnimationFrame(() => { const rowElement = document.getElementById(`preset-row-${nextId}`); if (rowElement) rowElement.scrollIntoView({ block: "nearest" }); }); } /** Built-in presets plus local custom prompts, without mutating the shipped JSON. */ function dashboardAllPresets(ctx) { return [ ...ctx.presets, ...ctx.customPrompts.map((custom) => dashboardCustomPromptToPreset(custom)), ]; } /** Presets visible in normal browsing; quality prompts live only on the Quality page. */ function dashboardBrowsablePresets(ctx) { const list = dashboardAllPresets(ctx); return list.filter((p) => !p.qualityMode && !p.internalOnly); } /** Return the preset category filters. */ function dashboardPresetCats(ctx) { const cats = new Map(); const labelOverrides = { custom: "Custom", qa: "QA" }; for (const p of dashboardBrowsablePresets(ctx)) { if (!cats.has(p.cat)) { cats.set(p.cat, labelOverrides[p.cat] ?? p.cat.charAt(0).toUpperCase() + p.cat.slice(1)); } } return [ { id: "all", label: "All" }, { id: "favorites", label: "\u2605 Favorites" }, ...Array.from(cats, ([id, label]) => ({ id, label })), ]; } /** Return flag-driven preset badges before dynamic surface/global-safe badges are appended. */ function presetFlagBadgeRules(preset) { return [ { enabled: preset.internalOnly, badge: { label: "Internal", title: "Intended for goat-flow framework maintenance", tone: "danger", }, }, { enabled: preset.qualityMode, badge: { label: "Quality", title: "Quality or skill-assessment workflow", tone: "neutral", }, }, { enabled: preset.requiresPrOrIssue, badge: { label: "Needs PR", title: "Requires a PR, issue, branch, or pasted diff context", tone: "warn", }, }, { enabled: preset.requiresLocalDiff, badge: { label: "Needs diff", title: "Requires local changes, a branch comparison, or pasted diff context", tone: "warn", }, }, { enabled: preset.requiresGh, badge: { label: "Needs gh", title: "Uses GitHub CLI when available; prompt must provide fallback context otherwise", tone: "warn", }, }, { enabled: preset.mayCheckoutBranch, badge: { label: "May checkout", title: "May ask to checkout a branch after clean-worktree confirmation", tone: "warn", }, }, { enabled: preset.requiresCleanWorktree, badge: { label: "Clean worktree", title: "Requires a clean worktree or explicit user approval before checkout", tone: "warn", }, }, { enabled: preset.mayWriteFiles, badge: { label: "May write", title: "May write files only with prompt or user approval", tone: "danger", }, }, { enabled: preset.requiresUiApp, badge: { label: "UI workflow", title: "Best suited to app/UI testing", tone: "ui", }, }, { enabled: preset.requiresDependencyFiles, badge: { label: "Dependency files", title: "Requires package manifests or lockfiles for dependency evidence", tone: "warn", }, }, { enabled: preset.requiresGoatFlowInstall, badge: { label: "GOAT install", title: "Requires goat-flow to be installed in the selected target project", tone: "warn", }, }, { enabled: preset.artifactRequired, badge: { label: "Artifact required", title: "Requires a plan, report, or other artifact to assess", tone: "warn", }, }, ] .filter((rule) => Boolean(rule.enabled)) .map((rule) => rule.badge); } /** Return compact prerequisite/fit badges for one preset because the card layout cannot show full metadata. */ function dashboardPresetBadges(preset) { const badges = presetFlagBadgeRules(preset); const surfaces = new Set(preset.bestTargetSurfaces ?? []); if (surfaces.has("library") || surfaces.has("api")) { badges.push({ label: "Library/API friendly", title: "Suitable for libraries, APIs, or non-UI projects", tone: "good", }); } if (preset.globalSafe && dashboardGlobalSafeAllowed(preset)) { badges.push({ label: "Global safe", title: "Can run against external target projects without goat-flow installed", tone: "good", }); } return badges; } /** Return the route label shown on prompt cards. */ function dashboardPresetRouteLabel(preset) { const route = preset.route || dashboardInferPromptRoute(preset.prompt); return route === "direct" ? "direct" : `/${route}`; } /** Return the left-edge accent color for prompt cards. */ function dashboardPresetCategoryAccent(preset) { return PRESET_CATEGORY_ACCENTS[preset.cat] ?? "var(--border-subtle)"; } /** * Favorites stay pinned to the top unless the user explicitly switches into * the favorites-only filter, which keeps mixed browsing fast on large lists. */ function dashboardFilteredPresets(ctx) { let list; const browsable = dashboardBrowsablePresets(ctx); if (ctx.presetFilter === "favorites") { list = browsable.filter((p) => ctx.presetFavorites.includes(p.id)); } else { list = ctx.presetFilter === "all" ? browsable : browsable.filter((p) => p.cat === ctx.presetFilter); } if (ctx.presetSearch.trim()) { const query = ctx.presetSearch.toLowerCase(); list = list.filter((p) => p.name.toLowerCase().includes(query) || p.desc.toLowerCase().includes(query) || p.prompt.toLowerCase().includes(query)); } else if (ctx.presetFilter !== "favorites") { const favSet = new Set(ctx.presetFavorites); list = [ ...list.filter((p) => favSet.has(p.id)), ...list.filter((p) => !favSet.has(p.id)), ]; } return list; } /** Presets grouped by category for the Prompts page grouped rendering. */ function dashboardPresetsByCategory(ctx) { const cats = dashboardPresetCats(ctx).filter((c) => c.id !== "all" && c.id !== "favorites"); const browsable = dashboardBrowsablePresets(ctx); return cats.map((cat) => ({ id: cat.id, label: cat.label, items: browsable.filter((p) => p.cat === cat.id), })); } /** Build the unified list rows for the Prompts page. */ function dashboardRenderedPresetEntries(ctx) { const entries = []; if (ctx.presetFilter === "all" && !ctx.presetSearch.trim()) { for (const group of ctx.presetsByCategory) { if (group.items.length === 0) continue; entries.push({ kind: "header", id: group.id, label: `${group.label} (${group.items.length})`, }); for (const p of group.items) entries.push({ kind: "row", preset: p }); } return entries; } for (const p of ctx.filteredPresets) entries.push({ kind: "row", preset: p }); return entries; } /** Return preset IDs in screen order for keyboard navigation. */ function dashboardFlatPresetOrder(ctx) { if (ctx.presetFilter === "all" && !ctx.presetSearch.trim()) { const ids = []; for (const group of ctx.presetsByCategory) { for (const p of group.items) ids.push(p.id); } return ids; } return ctx.filteredPresets.map((p) => p.id); } /** Return escaped, optionally search-highlighted HTML for the prompt preview. */ function dashboardHighlightedPromptHtml(ctx) { const prompt = ctx.adaptPrompt(ctx.selectedPreset?.prompt ?? ""); const escaped = prompt .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;"); const query = ctx.presetSearch.trim(); if (!query) return escaped; const qEscaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(qEscaped, "gi"); return escaped.replace(re, "<mark>$&</mark>"); } /** Adapt a preset prompt to the syntax expected by the selected runner. */ function dashboardAdaptPrompt(ctx, prompt, runner) { const selectedRunner = runner ?? ctx.activeRunner; const style = ctx.supportedAgents.find((agent) => agent.id === selectedRunner) ?.promptInvocationStyle ?? "slash"; if (style === "dollar") return prompt.replace(/^\/goat\b/, "$goat"); return prompt; } /** Copy a preset prompt after applying runner-specific syntax tweaks. */ function dashboardCopyPreset(ctx, prompt) { ctx.copyText(ctx.adaptPrompt(prompt)); }