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.

729 lines (716 loc) โ€ข 26.6 kB
<!doctype 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" > &times; </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">&#9888;</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; " > &times; </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; " > &#8592; .. </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>