@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.
729 lines (716 loc) โข 26.6 kB
HTML
<html lang="en" x-data="app()" x-init="init()">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GOAT Flow Dashboard</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>๐</text></svg>"
/>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"
></script>
<style type="text/tailwindcss">
@custom-variant dark (&:where(.dark, .dark *));
</style>
<link rel="stylesheet" href="/assets/styles.css" />
<link rel="preload" as="script" href="/assets/xterm.js" />
<link rel="preload" as="script" href="/assets/addon-fit.js" />
<link rel="preload" as="style" href="/assets/xterm.css" />
<!-- Prevent FOUC: apply dark mode before paint -->
<script>
if (
localStorage.getItem("gf-dark") === "true" ||
(!localStorage.getItem("gf-dark") &&
matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
}
</script>
</head>
<body
class="h-screen overflow-hidden"
style="background: #0b0f14; color: #e2e8f0"
:style="darkMode ? { background: '#0b0f14', color: '#e2e8f0' } : { background: '#fafbfc', color: '#0f172a' }"
>
<!-- Screen reader live region -->
<div
aria-live="polite"
aria-atomic="true"
class="sr-only"
x-text="srAnnouncement"
></div>
<svg class="gf-icon-sprite" aria-hidden="true">
<symbol id="gf-side-icon-home" viewBox="0 0 24 24">
<path d="m3 11 9-8 9 8" />
<path d="M5 10v10h14V10" />
<path d="M10 20v-6h4v6" />
</symbol>
<symbol id="gf-side-icon-workspace" viewBox="0 0 24 24">
<rect x="4" y="5" width="16" height="11" rx="2" />
<path d="M9 20h6" />
<path d="M12 16v4" />
</symbol>
<symbol id="gf-side-icon-plans" viewBox="0 0 24 24">
<path d="M9 6h11" />
<path d="M9 12h11" />
<path d="M9 18h11" />
<path d="m4 6 1 1 2-2" />
<path d="m4 12 1 1 2-2" />
<path d="m4 18 1 1 2-2" />
</symbol>
<symbol id="gf-side-icon-context" viewBox="0 0 24 24">
<path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H20v16H6.5A2.5 2.5 0 0 0 4 21.5z" />
<path d="M4 5.5v16" />
<path d="M8 7h8" />
<path d="M8 11h6" />
</symbol>
<symbol id="gf-side-icon-constraints" viewBox="0 0 24 24">
<path d="M12 3 20 7v5c0 5-3.3 8-8 9-4.7-1-8-4-8-9V7z" />
<path d="M9 12h6" />
</symbol>
<symbol id="gf-side-icon-verification" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="8" />
<path d="m8.5 12.5 2.2 2.2 4.8-5.2" />
</symbol>
<symbol id="gf-side-icon-recovery" viewBox="0 0 24 24">
<path d="M4 7v5h5" />
<path d="M20 17v-5h-5" />
<path d="M6.5 9.5A7 7 0 0 1 18 7.8" />
<path d="M17.5 14.5A7 7 0 0 1 6 16.2" />
</symbol>
<symbol id="gf-side-icon-feedback" viewBox="0 0 24 24">
<path d="M7 7h10v6H9l-4 4V9a2 2 0 0 1 2-2z" />
<path d="M14 16h3l2 2v-4" />
</symbol>
<symbol id="gf-side-icon-skills" viewBox="0 0 24 24">
<path
d="m12 3 2.4 5 5.6.8-4 3.9 1 5.5-5-2.6-5 2.6 1-5.5-4-3.9 5.6-.8z"
/>
</symbol>
<symbol id="gf-side-icon-playbooks" viewBox="0 0 24 24">
<path d="M6 4h10a2 2 0 0 1 2 2v14H8a2 2 0 0 1-2-2z" />
<path d="M6 18a2 2 0 0 1 2-2h10" />
<path d="M9 8h6" />
<path d="M9 12h4" />
</symbol>
<symbol id="gf-side-icon-hooks" viewBox="0 0 24 24">
<path d="M7 7a4 4 0 0 1 5.7 0l1.3 1.3" />
<path d="M10 14.7 8.7 16A4 4 0 1 1 3 10.3l1.3-1.3" />
<path d="M14 9.3 15.3 8A4 4 0 1 1 21 13.7L19.7 15" />
<path d="m9 15 6-6" />
</symbol>
<symbol id="gf-side-icon-memory" viewBox="0 0 24 24">
<ellipse cx="12" cy="6" rx="7" ry="3" />
<path d="M5 6v6c0 1.7 3.1 3 7 3s7-1.3 7-3V6" />
<path d="M5 12v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6" />
</symbol>
<symbol id="gf-side-icon-prompts" viewBox="0 0 24 24">
<path d="M5 5h14v10H8l-3 3z" />
<path d="M9 9h6" />
<path d="M9 12h4" />
</symbol>
<symbol id="gf-side-icon-projects" viewBox="0 0 24 24">
<path d="M4 6h6l2 2h8v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
</symbol>
<symbol id="gf-side-icon-setup" viewBox="0 0 24 24">
<path d="M14.7 6.3a4 4 0 0 0-5 5L4 17v3h3l5.7-5.7a4 4 0 0 0 5-5z" />
<path d="m15 5 4 4" />
</symbol>
<symbol id="gf-side-icon-quality" viewBox="0 0 24 24">
<path d="M5 17a7 7 0 1 1 14 0" />
<path d="m12 17 4-6" />
<path d="M8 18h8" />
</symbol>
</svg>
<!-- Floating toast notification -->
<div
x-show="toast"
x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-2"
:style="toastError ? { backgroundColor: '#dc2626' } : { backgroundColor: '#16a34a' }"
class="fixed bottom-14 left-1/2 -translate-x-1/2 z-50 px-4 py-2.5 rounded-xl shadow-lg text-sm font-medium flex items-center gap-2 text-white"
>
<svg
class="w-4 h-4 shrink-0"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<span x-text="toast"></span>
<button
@click="toast = ''"
class="ml-1 text-white/70 hover:text-white text-xs"
>
×
</button>
</div>
<!-- โโโ Max Sessions Modal (global overlay; triggered from any view) โโโ -->
<div
x-show="showMaxSessionsModal"
x-cloak
@click.self="showMaxSessionsModal = false"
@keydown.escape.window="showMaxSessionsModal = false"
style="
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
"
>
<div
class="gf-card"
style="
padding: 24px 28px;
max-width: 380px;
text-align: center;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
"
>
<div style="font-size: 28px; margin-bottom: 12px">⚠</div>
<h3
class="gf-text-primary"
style="font-size: 16px; font-weight: 600; margin: 0 0 6px"
>
Session Limit Reached
</h3>
<p
class="gf-text-muted"
style="font-size: 13px; line-height: 1.5; margin: 0 0 16px"
>
Maximum of <span x-text="serverMaxSessions"></span> terminal sessions.
End an existing session from the
<button
@click="showMaxSessionsModal = false; activeView = 'workspace'"
style="
background: none;
border: none;
padding: 0;
color: var(--accent);
cursor: pointer;
font-weight: 500;
text-decoration: underline;
font-size: inherit;
"
>
Workspace
</button>
sessions panel to start a new one.
</p>
<button
@click="showMaxSessionsModal = false"
class="gf-btn gf-btn-md gf-btn-primary"
style="min-width: 100px"
>
OK
</button>
</div>
</div>
<!-- โโโ Project Browser Overlay โโโ -->
<div
x-show="showBrowser"
x-cloak
@click.self="showBrowser = false"
class="fixed inset-0 z-50 flex items-start justify-center"
style="background: rgba(0, 0, 0, 0.5); padding-top: 80px"
>
<div
class="gf-card"
style="width: 420px; max-height: 60vh; overflow-y: auto; padding: 16px"
>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
"
>
<span
class="gf-text-primary"
style="font-size: 14px; font-weight: 600"
x-text="browserCurrent"
></span>
<button
@click="showBrowser = false"
class="gf-text-muted"
style="
font-size: 18px;
cursor: pointer;
background: none;
border: none;
"
>
×
</button>
</div>
<button
x-show="browserParent"
@click="browseTo(browserParent)"
class="gf-text-muted"
style="
font-size: 12px;
margin-bottom: 8px;
cursor: pointer;
background: none;
border: none;
padding: 4px 0;
"
>
← ..
</button>
<div style="display: flex; flex-direction: column; gap: 2px">
<template x-for="dir in browserDirs" :key="dir.path">
<button
@click="selectDir(dir)"
class="gf-text-primary"
style="
text-align: left;
padding: 6px 8px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
background: none;
border: none;
"
:style="dir.isProject ? { fontWeight: 600 } : {}"
>
<span x-text="dir.name"></span>
<span
x-show="dir.isProject"
class="gf-text-muted"
style="font-size: 10px; margin-left: 4px"
>(project)</span
>
</button>
</template>
</div>
</div>
</div>
<div
class="gf-app-shell"
:class="{ 'gf-side-collapsed': sideNavCollapsed }"
>
<!-- โโโ Side Navigation โโโ -->
<aside class="no-print gf-side-nav" aria-label="Primary navigation">
<div class="gf-side-top">
<button
@click="activeView = 'home'"
class="gf-side-logo"
aria-label="goat-flow Home"
>
<span class="gf-side-logo-emoji" aria-hidden="true">๐</span>
<span class="gf-side-logo-text">goat-flow</span>
</button>
<button
@click="toggleSideNav()"
class="gf-side-collapse-btn"
:title="sideNavCollapsed ? 'Expand side menu' : 'Collapse side menu'"
:aria-label="sideNavCollapsed ? 'Expand side menu' : 'Collapse side menu'"
:aria-expanded="(!sideNavCollapsed).toString()"
type="button"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6" />
</svg>
</button>
</div>
<nav class="gf-side-nav-groups">
<section class="gf-side-nav-group" aria-label="Main">
<button
@click="activeView = 'home'"
class="gf-side-link"
:class="activeView === 'home' && 'gf-side-link-active'"
data-short="H"
aria-label="Home"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-home"></use>
</svg>
<span class="gf-side-link-label">Home</span>
</button>
<button
@click="activeView = 'prompts'"
class="gf-side-link"
:class="activeView === 'prompts' && 'gf-side-link-active'"
data-short="Pr"
aria-label="Prompts"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-prompts"></use>
</svg>
<span class="gf-side-link-label">Prompts</span>
</button>
<button
@click="activeView = 'workspace'"
class="gf-side-link"
:class="activeView === 'workspace' && 'gf-side-link-active'"
data-short="W"
aria-label="Workspace"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-workspace"></use>
</svg>
<span class="gf-side-link-label">Workspace</span>
<span
x-show="(terminalSessionId && !terminalEnded) || terminalSessionCount > 0"
title="Terminal session active"
class="gf-status-dot gf-status-running"
aria-label="Terminal session active"
>โ</span
>
</button>
</section>
<section class="gf-side-nav-group" aria-label="Managers">
<div class="gf-side-group-label">Managers</div>
<button
@click="activeView = 'hooks'"
class="gf-side-link"
:class="activeView === 'hooks' && 'gf-side-link-active'"
data-short="Hk"
aria-label="Hooks"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-hooks"></use>
</svg>
<span class="gf-side-link-label">Hooks</span>
</button>
<button
@click="activeView = 'plans'"
class="gf-side-link"
:class="activeView === 'plans' && 'gf-side-link-active'"
data-short="T"
aria-label="Plans"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-plans"></use>
</svg>
<span class="gf-side-link-label">Plans</span>
</button>
<button
@click="activeView = 'skills'"
class="gf-side-link"
:class="activeView === 'skills' && 'gf-side-link-active'"
data-short="S"
aria-label="Skill Evaluator"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-skills"></use>
</svg>
<span class="gf-side-link-label">Skill Evaluator</span>
</button>
</section>
<section class="gf-side-nav-group" aria-label="Operations">
<div class="gf-side-group-label">Operations</div>
<button
@click="activeView = 'projects'"
class="gf-side-link"
:class="activeView === 'projects' && 'gf-side-link-active'"
data-short="Pj"
aria-label="Projects"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-projects"></use>
</svg>
<span class="gf-side-link-label">Projects</span>
</button>
<button
@click="activeView = 'quality'"
class="gf-side-link"
:class="activeView === 'quality' && 'gf-side-link-active'"
data-short="Q"
aria-label="Quality"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-quality"></use>
</svg>
<span class="gf-side-link-label">Quality</span>
</button>
<button
@click="activeView = 'setup'"
class="gf-side-link"
:class="activeView === 'setup' && 'gf-side-link-active'"
data-short="Se"
aria-label="Setup"
>
<svg class="gf-side-icon" aria-hidden="true">
<use href="#gf-side-icon-setup"></use>
</svg>
<span class="gf-side-link-label">Setup</span>
</button>
</section>
</nav>
</aside>
<div class="gf-main-shell">
<!-- โโโ Header โโโ -->
<header class="no-print gf-header shrink-0 z-30">
<span
x-show="!editingProjectTitle"
class="gf-header-proj"
@click="openBrowser()"
@dblclick.stop="startEditProjectTitle()"
x-text="projectName"
style="cursor: pointer"
title="Click to switch project ยท double-click to rename"
></span>
<input
x-show="editingProjectTitle"
x-model="projectTitleDraft"
@keydown.enter.prevent="saveProjectTitle()"
@keydown.escape.prevent="cancelEditProjectTitle()"
@blur="saveProjectTitle()"
x-init="$watch('editingProjectTitle', v => { if (v) $nextTick(() => $el.focus()); })"
class="gf-header-proj gf-header-proj-edit"
type="text"
maxlength="120"
placeholder="Project title"
title="Enter to save ยท Esc to cancel ยท empty to reset"
/>
<span
x-show="!editingProjectTitle"
class="gf-tooltip-wrap gf-header-tooltip-wrap"
>
<button
@click.stop="startEditProjectTitle()"
class="gf-header-proj-edit-btn"
aria-label="Rename project"
aria-describedby="project-title-rename-tip"
type="button"
>
โ
</button>
<span
id="project-title-rename-tip"
class="gf-tooltip gf-header-tooltip"
role="tooltip"
>Rename project</span
>
</span>
<div class="gf-header-spacer"></div>
<span
class="gf-text-muted gf-runner-label"
style="
font-size: 12px;
margin-right: 6px;
white-space: nowrap;
color: var(--text-disabled);
"
>Runner:</span
>
<div class="gf-asel">
<template x-for="agent in installedAgents" :key="agent.id">
<button
@click="activeRunner = agent.id"
class="gf-ab"
:class="activeRunner === agent.id && 'gf-ab-active'"
x-text="agent.id"
></button>
</template>
<template
x-for="agent in agentSkeletonList"
:key="'skel-' + agent.id"
>
<span class="gf-ab gf-ab-loading" x-text="agent.id"></span>
</template>
<span
x-show="installedAgents.length === 0 && agentsLoaded"
class="gf-ab"
style="cursor: default; font-style: italic"
>no agents</span
>
</div>
<button
@click="activeView = 'settings'"
class="gf-ib"
:class="activeView === 'settings' && 'gf-ib-active'"
title="Settings"
aria-label="Settings"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</button>
<button
class="gf-ib"
:class="activeView === 'about' && 'gf-ib-active'"
@click="activeView = 'about'"
title="About"
aria-label="About"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
</button>
<button
@click="darkMode = !darkMode"
class="gf-ib"
:title="darkMode ? 'Light mode' : 'Dark mode'"
:aria-label="darkMode ? 'Switch to light mode' : 'Switch to dark mode'"
>
<svg
x-show="darkMode"
x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 rotate-[-90deg] scale-0"
x-transition:enter-end="opacity-100 rotate-0 scale-100"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
<svg
x-show="!darkMode"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 rotate-90 scale-0"
x-transition:enter-end="opacity-100 rotate-0 scale-100"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
</button>
</header>
<!-- Main Content -->
<main class="flex-1 overflow-hidden">
<!-- Loading Spinner -->
<div
x-show="auditing"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/20 dark:bg-black/40"
>
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg px-6 py-4 flex items-center gap-3"
>
<svg
class="animate-spin h-5 w-5 text-blue-600"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span class="text-sm font-medium">Auditing...</span>
</div>
</div>
<!-- View fragments (assembled server-side) -->
<!-- include: views/workspace.html -->
<!-- include: views/plans.html -->
<!-- include: views/prompts.html -->
<!-- include: views/quality.html -->
<!-- include: views/skills.html -->
<!-- include: views/setup.html -->
<!-- include: views/hooks.html -->
<!-- include: views/home.html -->
<!-- include: views/projects.html -->
<!-- include: views/settings.html -->
<!-- include: views/about.html -->
<!-- include: views/coming-soon.html -->
</main>
</div>
</div>
<script src="/assets/markdown-it.js"></script>
<script src="/assets/js-yaml.js"></script>
<script src="/assets/markdown.js"></script>
<script src="/assets/dashboard-readers.js"></script>
<script src="/assets/dashboard-model-readers.js"></script>
<script src="/assets/dashboard-setup-quality.js"></script>
<script src="/assets/dashboard-projects.js"></script>
<script src="/assets/dashboard-custom-prompts.js"></script>
<script src="/assets/dashboard-custom-prompts-actions.js"></script>
<script src="/assets/dashboard-prompts.js"></script>
<script src="/assets/dashboard-terminal.js"></script>
<script src="/assets/dashboard-terminal-paste.js"></script>
<script src="/assets/dashboard-terminal-runtime.js"></script>
<script src="/assets/dashboard-terminal-connect.js"></script>
<script src="/assets/dashboard-app-merge.js"></script>
<script src="/assets/dashboard-app-init.js"></script>
<script src="/assets/dashboard-app-state-fragments.js"></script>
<script src="/assets/dashboard-app-prompts-audit-fragments.js"></script>
<script src="/assets/dashboard-app-data-loading-fragments.js"></script>
<script src="/assets/dashboard-app-skill-quality-fragments.js"></script>
<script src="/assets/dashboard-app-project-terminal-fragments.js"></script>
<script src="/assets/app.js"></script>
</body>
</html>