@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.
314 lines (298 loc) • 10.9 kB
HTML
<!-- ═══ Hooks View ═══ -->
<div x-show="activeView === 'hooks'" x-cloak class="gf-hooks-view">
<main class="gf-hooks-page">
<header class="gf-page-head gf-hooks-head">
<div>
<h1>Hooks</h1>
<p>Manage project hook state for the selected workspace.</p>
</div>
<button
@click="loadHooks()"
:disabled="hooksLoading"
class="gf-btn gf-btn-md gf-btn-secondary"
title="Refresh hook state"
>
<span x-text="hooksLoading ? 'Refreshing...' : 'Refresh'"></span>
</button>
</header>
<div
x-show="hooksLoading && hooksState.length === 0"
class="gf-hooks-panel"
role="status"
>
Loading hooks...
</div>
<div x-show="hooksError" x-cloak class="gf-hooks-alert" role="alert">
<div class="gf-text-primary">Hooks could not be loaded</div>
<p x-text="hooksError"></p>
</div>
<section
x-show="hooksState.length > 0"
class="gf-hooks-summary"
aria-label="Hook state summary"
>
<div class="gf-hooks-stat">
<span class="gf-hooks-stat-label">Total hooks</span>
<strong x-text="hooksState.length"></strong>
</div>
<div class="gf-hooks-stat">
<span class="gf-hooks-stat-label">Enabled</span>
<strong class="is-success" x-text="hooksEnabledCount()"></strong>
</div>
<div class="gf-hooks-stat">
<span class="gf-hooks-stat-label">Drift</span>
<strong
:class="hooksDriftCount() > 0 ? 'is-warning' : ''"
x-text="hooksDriftCount()"
></strong>
</div>
<div class="gf-hooks-stat">
<span class="gf-hooks-stat-label">Installed surfaces</span>
<strong x-text="hooksInstalledSurfaceCount()"></strong>
</div>
</section>
<section
x-show="hooksState.length > 0"
class="gf-hooks-toolbar"
aria-label="Hook filters"
>
<div class="gf-hooks-filter-group" role="group" aria-label="Hook state">
<button
type="button"
class="gf-hooks-chip"
:class="hooksFilter === 'all' && 'is-active'"
:aria-pressed="String(hooksFilter === 'all')"
@click="hooksFilter = 'all'"
>
All
<span
class="gf-hooks-chip-count"
x-text="hookFilterCount('all')"
></span>
</button>
<button
type="button"
class="gf-hooks-chip"
:class="hooksFilter === 'enabled' && 'is-active'"
:aria-pressed="String(hooksFilter === 'enabled')"
@click="hooksFilter = 'enabled'"
>
Enabled
<span
class="gf-hooks-chip-count"
x-text="hookFilterCount('enabled')"
></span>
</button>
<button
type="button"
class="gf-hooks-chip"
:class="hooksFilter === 'disabled' && 'is-active'"
:aria-pressed="String(hooksFilter === 'disabled')"
@click="hooksFilter = 'disabled'"
>
Disabled
<span
class="gf-hooks-chip-count"
x-text="hookFilterCount('disabled')"
></span>
</button>
<button
type="button"
class="gf-hooks-chip"
:class="hooksFilter === 'drift' && 'is-active'"
:aria-pressed="String(hooksFilter === 'drift')"
@click="hooksFilter = 'drift'"
>
Drift
<span
class="gf-hooks-chip-count"
x-text="hookFilterCount('drift')"
></span>
</button>
</div>
<label class="gf-hooks-search">
<span class="sr-only">Search hooks</span>
<input
type="search"
x-model="hooksSearch"
placeholder="Search hooks"
aria-label="Search hooks"
/>
</label>
</section>
<div
x-show="!hooksLoading && !hooksError && hooksState.length === 0"
class="gf-hooks-empty"
>
<div class="gf-empty-mark">HK</div>
<p>No hook state is available for this project.</p>
</div>
<div
x-show="hooksState.length > 0 && filteredHooks().length === 0"
class="gf-hooks-empty"
>
<div class="gf-empty-mark">0</div>
<p>No hooks match the current filter.</p>
</div>
<template
x-for="section in [
{ id: 'safety', label: 'Safety', tone: 'danger' },
{ id: 'workflow', label: 'Workflow', tone: 'workflow' },
{ id: 'git', label: 'Git & deploy', tone: 'warning' },
{ id: 'quality', label: 'Quality', tone: 'neutral' }
]"
:key="section.id"
>
<section
x-show="hookSectionCount(section.id) > 0"
class="gf-hooks-section"
>
<div class="gf-hooks-section-head">
<span
class="gf-hooks-pip"
:class="'gf-hooks-pip-' + section.tone"
aria-hidden="true"
></span>
<div class="gf-hooks-section-title">
<span x-text="section.label"></span>
<span
class="gf-section-count"
x-text="hookSectionCount(section.id) + (hookSectionCount(section.id) === 1 ? ' hook' : ' hooks')"
></span>
</div>
<span class="gf-hooks-section-rule" aria-hidden="true"></span>
</div>
<div class="gf-hooks-list">
<template x-for="hook in hooksForSection(section.id)" :key="hook.id">
<article
class="gf-hook-row"
:class="{ 'is-disabled': !hook.enabled, 'is-saving': hookSavingId === hook.id }"
>
<div
class="gf-hook-rail"
:class="'gf-hook-rail-' + hookTone(hook)"
aria-hidden="true"
></div>
<div class="gf-hook-main">
<div class="gf-hook-title-row">
<h2 x-text="hook.name"></h2>
<code x-text="hook.id"></code>
</div>
<p class="gf-hook-description" x-text="hook.description"></p>
<div class="gf-hook-pills" aria-label="Hook properties">
<span
class="gf-hook-pill"
:class="hook.enabled ? 'is-on' : 'is-muted'"
x-text="hook.enabled ? 'enabled' : 'disabled'"
></span>
<span
class="gf-hook-pill"
:class="hook.defaultEnabled ? 'is-on' : 'is-muted'"
x-text="hook.defaultEnabled ? 'default on' : 'default off'"
></span>
<span
x-show="hook.requiresConfirmDialog"
class="gf-hook-pill is-danger"
>confirm required</span
>
<span
x-show="hookHasDrift(hook)"
class="gf-hook-pill is-warning"
>drift</span
>
</div>
<div class="gf-hook-agent-row">
<span class="gf-hook-agent-label">Agents</span>
<template
x-for="[agentId, state] in hookAgents(hook)"
:key="hook.id + '-' + agentId"
>
<span
class="gf-hook-agent"
:class="hookAgentStatusClass(state)"
:title="state.reason || state.scriptPath || hookAgentStatusLabel(state)"
:aria-label="agentId + ': ' + hookAgentStatusLabel(state) + (state.reason ? '. ' + state.reason : '')"
>
<span x-text="agentId"></span>
<strong x-text="hookAgentStatusLabel(state)"></strong>
</span>
</template>
</div>
<div
x-show="unsupportedHookAgents(hook).length > 0"
class="gf-hook-unsupported"
aria-label="Unsupported hook agents"
>
<template
x-for="[agentId, state] in unsupportedHookAgents(hook)"
:key="hook.id + '-unsupported-' + agentId"
>
<div class="gf-hook-unsupported-item">
<span
class="gf-hook-unsupported-agent"
x-text="agentId + ' unsupported'"
></span>
<span
class="gf-hook-unsupported-reason"
x-text="state.reason"
></span>
</div>
</template>
</div>
</div>
<div class="gf-hook-controls">
<span
x-show="hook.togglable"
class="gf-hook-control-label"
:class="hook.enabled ? 'is-on' : 'is-off'"
x-text="hookSavingId === hook.id ? 'Saving...' : (hook.enabled ? 'Enabled' : 'Disabled')"
></span>
<button
x-show="hook.togglable"
type="button"
role="switch"
class="gf-hook-switch"
:class="hook.enabled && 'is-on'"
:aria-checked="String(hook.enabled)"
:disabled="hookSavingId === hook.id"
@click="toggleHook(hook, !hook.enabled)"
>
<span
class="sr-only"
x-text="(hook.enabled ? 'Disable ' : 'Enable ') + hook.name"
></span>
<span class="gf-hook-switch-knob" aria-hidden="true"></span>
</button>
<span x-show="!hook.togglable" class="gf-hook-pill is-on"
>always on</span
>
<button
x-show="hook.togglable && hookHasDrift(hook)"
@click="resyncHook(hook)"
:disabled="hookSavingId === hook.id"
class="gf-btn gf-btn-sm gf-btn-secondary"
>
Re-sync
</button>
</div>
</article>
</template>
</div>
</section>
</template>
<div x-show="hooksState.length > 0" class="gf-hooks-legend">
<span>
<span class="gf-hooks-swatch is-installed" aria-hidden="true"></span>
installed and enforced
</span>
<span>
<span class="gf-hooks-swatch is-available" aria-hidden="true"></span>
available, not installed
</span>
<span>
<span class="gf-hooks-swatch is-drift" aria-hidden="true"></span>
desired state drift
</span>
</div>
</main>
</div>