Sync accepted GitHop state and prompt records

This commit is contained in:
Hop
2026-07-12 01:57:25 +00:00
parent e5bb6a5600
commit b780eadd33
164 changed files with 4811 additions and 12224 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/bin/sh
set -eu
env_file=${ENV_FILE:-.env}
if [ ! -f "$env_file" ]; then
echo "missing $env_file; copy .env.example and configure it first" >&2
exit 1
fi
docker compose --env-file "$env_file" up -d gitea
docker compose --env-file "$env_file" exec -T -u root gitea \
mkdir -p /data/gitea/templates/base /data/gitea/templates/custom /data/gitea/public/assets/js /data/gitea/public/assets/css /data/gitea/public/assets/img
docker compose --env-file "$env_file" cp \
deploy/gitea/templates/home.tmpl \
gitea:/data/gitea/templates/home.tmpl
docker compose --env-file "$env_file" cp \
deploy/gitea/templates/base/head.tmpl \
gitea:/data/gitea/templates/base/head.tmpl
docker compose --env-file "$env_file" cp \
deploy/gitea/templates/custom/header.tmpl \
gitea:/data/gitea/templates/custom/header.tmpl
docker compose --env-file "$env_file" cp \
deploy/gitea/templates/custom/footer.tmpl \
gitea:/data/gitea/templates/custom/footer.tmpl
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/js/hop-native.js \
gitea:/data/gitea/public/assets/js/hop-native.js
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/js/hop-native.js \
gitea:/data/gitea/public/assets/js/hop-native-v12.js
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/js/hop-native.js \
gitea:/data/gitea/public/assets/js/hop-native-v13.js
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/js/hop-native.js \
gitea:/data/gitea/public/assets/js/hop-native-v14.js
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/css/hop-home.css \
gitea:/data/gitea/public/assets/css/hop-home.css
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/css/hop-prompts.css \
gitea:/data/gitea/public/assets/css/hop-prompts.css
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/img/hop.svg \
gitea:/data/gitea/public/assets/img/hop.svg
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/img/hop.svg \
gitea:/data/gitea/public/assets/img/logo.svg
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/img/logo.png \
gitea:/data/gitea/public/assets/img/logo.png
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/img/avatar_default.png \
gitea:/data/gitea/public/assets/img/avatar_default.png
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/img/hop.svg \
gitea:/data/gitea/public/assets/img/favicon.svg
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/img/favicon.png \
gitea:/data/gitea/public/assets/img/favicon.png
docker compose --env-file "$env_file" exec -T -u root gitea \
chown -R git:git /data/gitea/templates /data/gitea/public
docker compose --env-file "$env_file" exec -T -u git \
-e GITEA____APP_NAME=Hop gitea \
gitea --config /data/gitea/conf/app.ini config edit-ini --in-place --apply-env
docker compose --env-file "$env_file" restart gitea
attempt=0
until curl --fail --silent http://localhost:3000/api/healthz >/dev/null; do
attempt=$((attempt + 1))
if [ "$attempt" -ge 30 ]; then
echo "Gitea did not become healthy" >&2
exit 1
fi
sleep 1
done
echo "Hop-native Gitea semantics installed at http://localhost:3000"
@@ -0,0 +1,61 @@
:root {
--color-primary: #724bdb !important;
--color-primary-contrast: #fff !important;
--color-primary-hover: #6540c7 !important;
--color-primary-active: #5935b3 !important;
--color-primary-dark-1: #6540c7 !important;
--color-primary-dark-2: #5935b3 !important;
--color-primary-dark-3: #4c2c9f !important;
--color-primary-dark-4: #3f2485 !important;
--color-primary-dark-5: #2c195d !important;
--color-primary-dark-6: #1a0f38 !important;
--color-primary-dark-7: #0b0618 !important;
--color-primary-light-1: #805ee0 !important;
--color-primary-light-2: #8e71e5 !important;
--color-primary-light-3: #9c84e9 !important;
--color-primary-light-4: #aa97ed !important;
--color-primary-light-5: #c6b8f3 !important;
--color-primary-light-6: #e2dcf9 !important;
--color-primary-light-7: #f4f1fd !important;
--color-primary-alpha-10: #724bdb19 !important;
--color-primary-alpha-20: #724bdb33 !important;
--color-primary-alpha-30: #724bdb4c !important;
--color-primary-alpha-40: #724bdb66 !important;
--color-primary-alpha-50: #724bdb80 !important;
--color-primary-alpha-60: #724bdb99 !important;
--color-primary-alpha-70: #724bdbb3 !important;
--color-primary-alpha-80: #724bdbcc !important;
--color-primary-alpha-90: #724bdbe6 !important;
}
.hop-home { max-width: 1120px; margin: 0 auto; padding: 5.5rem 2rem 4rem; }
.hop-home h1, .hop-home h2, .hop-home h3 { color: var(--color-text); letter-spacing: -0.035em; }
.hop-home p { color: var(--color-text-light-2); line-height: 1.65; }
.hop-eyebrow { color: var(--color-primary) !important; font-size: .78rem; font-weight: 700; letter-spacing: .13em; margin: 0 0 1rem; text-transform: uppercase; }
.hop-hero { align-items: center; display: grid; gap: 5rem; grid-template-columns: 1.25fr .75fr; min-height: 390px; }
.hop-hero h1 { font-size: clamp(2.75rem, 6vw, 4.8rem); line-height: 1.02; margin: 0; max-width: 750px; }
.hop-lede { font-size: 1.18rem; margin: 1.5rem 0 2rem; max-width: 650px; }
.hop-actions { display: flex; flex-wrap: wrap; gap: .75rem; }
.hop-actions .button, .hop-closing .button { margin: 0; }
.hop-hero-mark { display: grid; justify-items: center; position: relative; }
.hop-hero-mark img { filter: drop-shadow(0 18px 24px color-mix(in srgb, var(--color-primary) 22%, transparent)); width: min(100%, 220px); }
.hop-state-card { align-items: center; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; box-shadow: 0 12px 28px rgba(0,0,0,.12); color: var(--color-text); display: flex; font-size: .85rem; gap: .55rem; margin-top: -1rem; padding: .72rem .9rem; position: relative; }
.hop-state-dot { background: #2da44e; border-radius: 50%; height: .55rem; width: .55rem; }
.hop-intro { border-top: 1px solid var(--color-secondary); margin-top: 4.5rem; max-width: 780px; padding-top: 4.5rem; }
.hop-intro h2, .hop-flow h2, .hop-closing h2 { font-size: clamp(2rem, 3.5vw, 3rem); line-height: 1.12; margin: 0 0 1.1rem; }
.hop-intro p { font-size: 1.12rem; }
.hop-benefits { display: grid; gap: 1px; grid-template-columns: repeat(3, 1fr); margin: 5rem 0; background: var(--color-secondary); border: 1px solid var(--color-secondary); }
.hop-benefits article { background: var(--color-box-body); padding: 2rem; }
.hop-number { color: var(--color-primary); font-size: .82rem; font-weight: 700; letter-spacing: .1em; }
.hop-benefits h2 { font-size: 1.35rem; margin: 3.5rem 0 .8rem; }
.hop-benefits p { font-size: .97rem; margin: 0; }
.hop-flow { display: grid; gap: 4rem; grid-template-columns: .8fr 1.2fr; padding: 2rem 0 5rem; }
.hop-flow h2 { font-size: 2.3rem; }
.hop-flow ol { border-left: 1px solid var(--color-secondary); list-style: none; margin: 0; padding: 0 0 0 2rem; }
.hop-flow li { display: grid; gap: 1rem; grid-template-columns: 2rem 1fr; margin: 0 0 2rem; position: relative; }
.hop-flow li:last-child { margin-bottom: 0; }
.hop-flow li > span { align-items: center; background: var(--color-box-body); border: 1px solid var(--color-primary); border-radius: 50%; color: var(--color-primary); display: flex; font-size: .8rem; font-weight: 700; height: 2rem; justify-content: center; margin-left: -3.05rem; }
.hop-flow h3 { font-size: 1.12rem; margin: .25rem 0 .3rem; }
.hop-flow li p { margin: 0; }
.hop-closing { align-items: center; background: color-mix(in srgb, var(--color-primary) 9%, var(--color-box-body)); border: 1px solid color-mix(in srgb, var(--color-primary) 32%, var(--color-secondary)); display: flex; justify-content: space-between; gap: 2rem; padding: 2.5rem; }
.hop-closing h2 { font-size: 2rem; margin-bottom: 0; max-width: 540px; }
@media (max-width: 760px) { .hop-home { padding: 3.5rem 1.25rem; } .hop-hero, .hop-flow { gap: 2.5rem; grid-template-columns: 1fr; } .hop-hero-mark { justify-items: start; } .hop-hero-mark img { width: 145px; } .hop-benefits { grid-template-columns: 1fr; } .hop-benefits h2 { margin-top: 1.5rem; } .hop-closing { align-items: flex-start; flex-direction: column; } }
@@ -0,0 +1,202 @@
.hop-native-prompts-tab svg {
margin-right: 0.4rem;
}
.hop-prompts {
margin: 0 auto;
max-width: 1080px;
padding: 2.25rem clamp(1rem, 3vw, 2.5rem) 4rem;
}
.hop-prompts__header {
align-items: end;
border-bottom: 1px solid var(--color-secondary);
display: flex;
gap: 1.5rem;
justify-content: space-between;
padding-bottom: 1.5rem;
}
.hop-prompts__eyebrow {
color: var(--color-text-light-2);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.1em;
margin: 0 0 0.4rem;
text-transform: uppercase;
}
.hop-prompts h1 {
color: var(--color-text);
font-size: 1.55rem;
letter-spacing: -0.025em;
margin: 0;
}
.hop-prompts__lede {
color: var(--color-text-light-2);
line-height: 1.55;
margin: 0.65rem 0 0;
max-width: 62ch;
}
.hop-prompts__refresh {
align-items: center;
display: inline-flex;
gap: 0.45rem;
white-space: nowrap;
}
.hop-prompts__status {
color: var(--color-text-light-2);
font-size: 0.9rem;
margin: 1.25rem 0;
}
.hop-prompts__empty {
border: 1px dashed var(--color-secondary);
color: var(--color-text-light-2);
line-height: 1.6;
margin-top: 1.25rem;
max-width: 720px;
padding: 1.5rem;
}
.hop-prompts__empty strong {
color: var(--color-text);
display: block;
font-size: 1rem;
margin-bottom: 0.35rem;
}
.hop-prompt-list {
display: grid;
gap: 0;
margin-top: 1.25rem;
}
.hop-prompt {
border-bottom: 1px solid var(--color-secondary);
border-top: 1px solid var(--color-secondary);
margin-top: -1px;
}
.hop-prompt summary {
align-items: center;
cursor: pointer;
display: grid;
gap: 0.75rem 1rem;
grid-template-columns: minmax(0, 1fr) auto;
list-style: none;
padding: 1rem 0;
}
.hop-prompt summary::-webkit-details-marker { display: none; }
.hop-prompt__request {
color: var(--color-text);
font-size: 1rem;
font-weight: 600;
line-height: 1.45;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hop-prompt__meta {
color: var(--color-text-light-2);
font-size: 0.82rem;
text-align: right;
}
.hop-prompt__status {
align-items: center;
color: var(--color-text-light-2);
display: inline-flex;
gap: 0.4rem;
margin-right: 0.55rem;
}
.hop-prompt__status::before {
background: var(--color-text-light-3);
border-radius: 50%;
content: "";
height: 0.45rem;
width: 0.45rem;
}
.hop-prompt__status[data-status="completed"]::before { background: var(--color-green); }
.hop-prompt__status[data-status="failed"]::before { background: var(--color-red); }
.hop-prompt__status[data-status="cancelled"]::before { background: var(--color-orange); }
.hop-prompt__status[data-status="running"]::before { background: var(--color-primary); }
.hop-prompt__body {
border-top: 1px solid var(--color-secondary);
display: grid;
gap: 1.25rem;
grid-template-columns: minmax(0, 1.45fr) minmax(14rem, 0.55fr);
padding: 1.25rem 0 1.5rem;
}
.hop-prompt h2,
.hop-prompt h3 {
color: var(--color-text);
font-size: 0.78rem;
letter-spacing: 0.07em;
margin: 0 0 0.55rem;
text-transform: uppercase;
}
.hop-prompt h2.hop-prompt__outcome-heading {
margin-top: 1.25rem;
}
.hop-prompt pre,
.hop-prompt__response {
background: var(--color-box-body);
border: 1px solid var(--color-secondary);
border-radius: var(--radius-medium);
color: var(--color-text);
font: inherit;
line-height: 1.55;
margin: 0;
overflow-wrap: anywhere;
padding: 0.9rem 1rem;
white-space: pre-wrap;
}
.hop-prompt__response { color: var(--color-text-light-1); }
.hop-prompt__facts {
display: grid;
gap: 0.6rem;
margin: 0;
}
.hop-prompt__facts div {
border-bottom: 1px solid var(--color-secondary);
padding-bottom: 0.55rem;
}
.hop-prompt__facts dt {
color: var(--color-text-light-2);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.hop-prompt__facts dd {
color: var(--color-text);
font-size: 0.88rem;
margin: 0.2rem 0 0;
overflow-wrap: anywhere;
}
@media (max-width: 700px) {
.hop-prompts__header, .hop-prompt__body { align-items: start; grid-template-columns: 1fr; }
.hop-prompts__header { flex-direction: column; }
.hop-prompt summary { grid-template-columns: 1fr; }
.hop-prompt__meta { text-align: left; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" fill="none" shape-rendering="auto" aria-hidden="true" width="512" height="512"><!-- Generated by DiceBear (https://dicebear.com) --><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Bottts</dc:title><dc:creator>Pablo Stanley</dc:creator><dc:source xsi:type="dcterms:URI">https://bottts.com/</dc:source><dcterms:license xsi:type="dcterms:URI">https://bottts.com/</dcterms:license><dc:rights>Remix of “Bottts” (https://bottts.com/) by “Pablo Stanley”, licensed under “Free for personal and commercial use” (https://bottts.com/)</dc:rights></rdf:Description></rdf:RDF></metadata><defs><g id="sides-antenna01-b3dae8fb"><mask id="sidesAntenna01-a" style="mask-type:luminance"><rect y="31" width="36" height="14" rx="4" fill="white"/><rect x="12" y="14" width="36" height="48" rx="4" fill="white"/><rect x="120" y="28" width="36" height="24" rx="4" fill="white"/><rect x="5" y="11" width="2" height="20" fill="white"/></mask><g mask="url(#sidesAntenna01-a)"><path d="M-6 0h180v76H-6z" fill="#9975ea"/><rect x="-6" width="180" height="76" fill="white" fill-opacity=".3"/><rect x="-6" y="38" width="180" height="38" fill="black" fill-opacity=".1"/></g><rect x="5" y="11" width="2" height="20" fill="white" fill-opacity=".4"/><circle cx="6" cy="8" r="4" fill="#FFEA8F"/><circle cx="7" cy="7" r="2" fill="white"/></g><g id="top-antennaCrooked-b3dae8fb"><mask id="topAntennaCrooked-a" style="mask-type:luminance"><path d="M47.54 34.98 43 45.59h-3.74l4.92-10.43-6.05-10.43 3.22-11.84 2.9.8-2.9 10.6z" fill="white"/><rect x="30" y="39.59" width="24" height="13" fill="white"/></mask><g mask="url(#topAntennaCrooked-a)"><path d="M-8 .6H92v52H-8z" fill="#9975ea"/><path d="M-8 6.6H92v52H-8z" fill="white" fill-opacity=".3"/><rect x="30" y="39.59" width="24" height="13" fill="white" fill-opacity=".2"/></g><circle cx="42" cy="8.59" r="8" fill="#FFE65C"/><circle cx="45" cy="5.59" r="3" fill="white"/></g><g id="texture-circuits-b3dae8fb"><path d="M66 36h49.42a5 5 0 1 1 0 4H62V6h4zm-8-12h-4V6h4z" fill="black" fill-opacity=".1"/><path d="M163 31a5 5 0 0 1 2 9.58V66h19v4h-23V40.58a5 5 0 0 1 2-9.58" fill="white" fill-opacity=".2"/><path d="M157 74h27v4h-31V52h4zm-85 26a5 5 0 0 0-2 9.58V135H51v4h23v-29.42a5 5 0 0 0-2-9.58" fill="black" fill-opacity=".2"/></g><g id="head-round02-b3dae8fb"><mask id="headRound02-a" style="mask-type:luminance"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 31v-1c.18-.48.4-1.5 1-3 .83-3.06 2.78-6.56 6-10C16.7 6.6 35.17 0 65 0s48.3 6.6 58 17c3.22 3.44 5.17 6.94 6 10 .6 1.5.82 2.52 1 3v40c0-.1-.03.5 0 1a54 54 0 0 1-1 6c-1.19 6-3.4 11.91-7 17-9.72 16.34-27.74 26-57 26s-47.28-9.66-57-26c-3.6-5.1-5.8-11-7-17a54 54 0 0 1-1-6c.03-.45 0-1.32 0-1z" fill="white"/></mask><g mask="url(#headRound02-a)"><path d="M-4-2h138v124H-4z" fill="#9975ea"/><use transform="translate(-44 -7)" href="#texture-circuits-b3dae8fb"/></g></g><g id="mouth-smile01-b3dae8fb"><path d="M23.05 4.44a2 2 0 1 1 3.9-.88C27.72 6.96 30.4 9 34 9s6.28-2.04 7.05-5.44a2 2 0 1 1 3.9.88C43.75 9.7 39.43 13 34 13s-9.76-3.3-10.95-8.56" fill="black" fill-opacity=".6"/></g><g id="eyes-frame1-b3dae8fb"><rect y="4" width="104" height="42" rx="4" fill="black" fill-opacity=".8"/><rect x="28" y="25" width="10" height="11" rx="2" fill="#8BDDFF"/><rect x="66" y="25" width="10" height="11" rx="2" fill="#8BDDFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M21 4h8L12 46H4z" fill="white" fill-opacity=".4"/></g><clipPath id="clip-b3dae8fb"><rect width="180" height="180" rx="0" ry="0"/></clipPath></defs><g clip-path="url(#clip-b3dae8fb)"><use transform="translate(6 66)" href="#sides-antenna01-b3dae8fb"/><use transform="translate(49 -.6)" href="#top-antennaCrooked-b3dae8fb"/><use transform="translate(25 44)" href="#head-round02-b3dae8fb"/><use transform="translate(56 128)" href="#mouth-smile01-b3dae8fb"/><use transform="translate(38 76)" href="#eyes-frame1-b3dae8fb"/></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+471
View File
@@ -0,0 +1,471 @@
(() => {
'use strict';
const exactLabels = new Map([
['Code', 'Files'],
['Issues', 'Tasks'],
['Pull Requests', 'Proposals'],
['Pull requests', 'Proposals'],
['Actions', 'Evidence'],
['Commits', 'Checkpoints'],
['Branches', 'Attempts'],
['New Issue', 'New Task'],
['Create Issue', 'Create Task'],
['New Pull Request', 'New Proposal'],
['Create Pull Request', 'Create Proposal'],
['Merge Pull Request', 'Accept Proposal'],
['Compare & pull request', 'Review as Proposal'],
]);
const phraseLabels = [
[/\bPull Requests\b/g, 'Proposals'],
[/\bPull requests\b/g, 'Proposals'],
[/\bpull requests\b/g, 'proposals'],
[/\bNew Issue\b/g, 'New Task'],
[/\bnew issue\b/g, 'new task'],
[/\bIssues\b/g, 'Tasks'],
[/\bissues\b/g, 'tasks'],
[/\bActions\b/g, 'Evidence'],
[/\bCommits\b/g, 'Checkpoints'],
[/\bBranches\b/g, 'Attempts'],
];
const semanticRoots = [
'#navbar',
'.secondary-nav',
'[role="main"] .ui.header',
'[role="main"] h1',
'[role="main"] h2',
'[role="main"] h3',
'[role="main"] .ui.button',
'[role="main"] .menu',
'[role="main"] .breadcrumb',
'[role="main"] label',
].join(',');
function replaceExactText(root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
for (const node of nodes) {
const original = node.nodeValue;
const trimmed = original.trim();
const replacement = exactLabels.get(trimmed);
if (!replacement) continue;
node.nodeValue = original.replace(trimmed, replacement);
}
}
function replacePhrases(value) {
let result = value;
for (const [pattern, replacement] of phraseLabels) {
result = result.replace(pattern, replacement);
}
return result;
}
function replaceAccessibleLabels(root) {
const attributes = ['aria-label', 'data-text', 'data-tooltip-content', 'placeholder', 'title'];
for (const element of root.querySelectorAll('[aria-label], [data-text], [data-tooltip-content], [placeholder], [title]')) {
for (const attribute of attributes) {
const value = element.getAttribute(attribute);
if (!value) continue;
const replacement = replacePhrases(value);
if (replacement !== value) element.setAttribute(attribute, replacement);
}
}
}
function repoNavigation() {
return document.querySelector('.secondary-nav .overflow-menu-items');
}
function navigationLink(suffix) {
const navigation = repoNavigation();
if (!navigation) return null;
return [...navigation.querySelectorAll(':scope > a.item')].find((link) => {
const path = new URL(link.href, window.location.origin).pathname.replace(/\/$/, '');
return path.endsWith(suffix);
}) || null;
}
function repositoryPath() {
const issues = navigationLink('/issues');
if (issues) return new URL(issues.href, window.location.origin).pathname.replace(/\/issues$/, '');
const match = window.location.pathname.match(/^\/[^/]+\/[^/]+/);
return match ? match[0].replace(/\/$/, '') : null;
}
function promptsHref() {
const path = repositoryPath();
return path ? `${path}/issues?hop=prompts` : null;
}
function promptsPage() {
return new URLSearchParams(window.location.search).get('hop') === 'prompts';
}
function promptsIcon() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 16 16');
svg.setAttribute('width', '16');
svg.setAttribute('height', '16');
svg.setAttribute('aria-hidden', 'true');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M2 2.75A1.75 1.75 0 0 1 3.75 1h8.5A1.75 1.75 0 0 1 14 2.75v6.5A1.75 1.75 0 0 1 12.25 11H7.6l-2.82 2.1A.5.5 0 0 1 4 12.7V11h-.25A1.75 1.75 0 0 1 2 9.25Zm1.75-.75a.75.75 0 0 0-.75.75v6.5c0 .414.336.75.75.75H4.5a.5.5 0 0 1 .5.5v1.2l2.15-1.6A.5.5 0 0 1 7.45 10h4.8a.75.75 0 0 0 .75-.75v-6.5a.75.75 0 0 0-.75-.75Z');
svg.append(path);
return svg;
}
function addPromptsNavigation() {
const navigation = repoNavigation();
const tasks = navigationLink('/issues');
const href = promptsHref();
if (!navigation || !tasks || !href) return;
let link = navigation.querySelector('.hop-native-prompts-tab');
if (!link) {
link = document.createElement('a');
link.className = tasks.className;
link.classList.add('hop-native-prompts-tab');
link.append(promptsIcon());
link.append(document.createTextNode('Prompts'));
navigation.insertBefore(link, tasks);
}
link.href = href;
link.setAttribute('aria-label', 'Prompts');
link.classList.toggle('active', promptsPage());
if (promptsPage()) tasks.classList.remove('active');
}
function workflowLink(source, label) {
if (!source) return null;
const link = document.createElement('a');
link.className = 'flex-text-block muted';
link.href = source.href;
link.setAttribute('aria-label', label);
const icon = source.querySelector('svg');
if (icon) link.append(icon.cloneNode(true));
link.append(document.createTextNode(` ${label}`));
const count = source.querySelector('.ui.label');
if (count) link.append(count.cloneNode(true));
return link;
}
function addWorkflowSummary() {
const sidebar = document.querySelector('.repo-home-sidebar-top');
if (!sidebar || sidebar.querySelector('.hop-native-workflow')) return;
const links = [
workflowLink(document.querySelector('.hop-native-prompts-tab'), 'Prompts'),
workflowLink(navigationLink('/issues'), 'Tasks'),
workflowLink(navigationLink('/pulls'), 'Proposals'),
workflowLink(navigationLink('/actions'), 'Evidence'),
].filter(Boolean);
if (!links.length) return;
const list = document.createElement('div');
list.className = 'flex-list hop-native-workflow';
const item = document.createElement('div');
item.className = 'flex-item';
const main = document.createElement('div');
main.className = 'flex-item-main';
const title = document.createElement('div');
title.className = 'flex-item-title';
title.textContent = 'Hop workflow';
const body = document.createElement('div');
body.className = 'flex-item-body tw-text-15';
const linkList = document.createElement('div');
linkList.className = 'tw-flex tw-flex-col tw-gap-2 tw-mt-2';
for (const link of links) linkList.append(link);
body.append(linkList);
main.append(title, body);
item.append(main);
list.append(item);
sidebar.prepend(list);
}
function makeElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
}
function formatTime(value) {
if (!value) return '';
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
}
function promptFact(list, label, value) {
if (!value) return;
const row = document.createElement('div');
row.append(makeElement('dt', '', label), makeElement('dd', '', String(value)));
list.append(row);
}
function promptDetails(prompt) {
const body = makeElement('div', 'hop-prompt__body');
const record = document.createElement('div');
record.append(makeElement('h2', '', 'Captured request'));
record.append(makeElement('pre', '', prompt.prompt));
if (prompt.response_summary) {
record.append(makeElement('h2', 'hop-prompt__outcome-heading', 'Proposal outcome'));
record.append(makeElement('div', 'hop-prompt__response', prompt.response_summary));
}
const facts = document.createElement('dl');
facts.className = 'hop-prompt__facts';
promptFact(facts, 'State', prompt.state_id);
promptFact(facts, 'Task', prompt.task_id);
promptFact(facts, 'Attempt', prompt.attempt_id);
promptFact(facts, 'Agent', [prompt.agent_name, prompt.agent_model].filter(Boolean).join(' · '));
promptFact(facts, 'Captured', formatTime(prompt.created_at));
promptFact(facts, 'Completed', formatTime(prompt.completed_at));
if (prompt.metadata && typeof prompt.metadata === 'object') {
Object.entries(prompt.metadata).slice(0, 8).forEach(([key, value]) => {
if (value === null || typeof value === 'object') return;
promptFact(facts, key.replace(/_/g, ' '), value);
});
}
body.append(record, facts);
return body;
}
function promptRow(prompt) {
const details = document.createElement('details');
details.className = 'hop-prompt';
const summary = document.createElement('summary');
const request = makeElement('div', 'hop-prompt__request', prompt.prompt);
request.title = prompt.prompt;
const meta = makeElement('div', 'hop-prompt__meta');
const status = makeElement('span', 'hop-prompt__status', prompt.status || 'running');
status.dataset.status = prompt.status || 'running';
meta.append(status);
const agent = [prompt.agent_name, prompt.agent_model].filter(Boolean).join(' · ');
meta.append(document.createTextNode(agent || formatTime(prompt.created_at)));
summary.append(request, meta);
details.append(summary, promptDetails(prompt));
return details;
}
async function portablePromptRecords(path) {
const [owner, repo] = path.slice(1).split('/');
let branches = ['main', 'master'];
try {
const repository = await fetch(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, {credentials: 'same-origin'});
if (repository.ok) {
const payload = await repository.json();
if (payload.default_branch) branches = [payload.default_branch, ...branches.filter((branch) => branch !== payload.default_branch)];
}
} catch (_) {
// The raw-file fallback below still works for the common default branches.
}
for (const branch of branches) {
const treeResponse = await fetch(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(branch)}?recursive=true`, {credentials: 'same-origin'});
if (treeResponse.status === 404) continue;
if (!treeResponse.ok) throw new Error('Could not inspect the published prompt records');
const tree = await treeResponse.json();
const records = (Array.isArray(tree.tree) ? tree.tree : [])
.filter((entry) => entry.type === 'blob' && /^\.hop\/records\/prompts\/[^/]+\.json$/.test(entry.path));
if (!records.length) return [];
const prompts = await Promise.all(records.map(async (entry) => {
const recordPath = entry.path.split('/').map(encodeURIComponent).join('/');
const response = await fetch(`${path}/raw/branch/${encodeURIComponent(branch)}/${recordPath}`, {credentials: 'same-origin'});
if (!response.ok) throw new Error('Could not load a published prompt record');
return response.json();
}));
return prompts
.filter((prompt) => prompt.status !== 'active' && prompt.status !== 'running')
.sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)));
}
return [];
}
function renderPromptPage() {
if (!promptsPage()) return;
const main = document.querySelector('[role="main"].page-content.repository.issue-list');
const path = repositoryPath();
if (!main || !path || main.dataset.hopPromptsRendered === 'true') return;
main.dataset.hopPromptsRendered = 'true';
main.setAttribute('aria-label', 'Prompts');
const navigation = main.querySelector(':scope > .secondary-nav');
for (const child of [...main.children]) {
if (child !== navigation) child.remove();
}
const page = makeElement('section', 'hop-prompts');
page.setAttribute('aria-labelledby', 'hop-prompts-title');
const header = makeElement('header', 'hop-prompts__header');
const heading = document.createElement('div');
heading.append(
makeElement('p', 'hop-prompts__eyebrow', 'Hop causal record'),
makeElement('h1', '', 'Prompts'),
makeElement('p', 'hop-prompts__lede', 'Review what you requested, which agent acted, and the proposal that resulted. Internal agent instructions are not published.'),
);
heading.querySelector('h1').id = 'hop-prompts-title';
const refresh = makeElement('button', 'ui button hop-prompts__refresh', 'Refresh');
refresh.type = 'button';
header.append(heading, refresh);
const status = makeElement('p', 'hop-prompts__status', 'Loading prompt records…');
status.setAttribute('role', 'status');
page.append(header, status);
main.append(page);
document.title = `Prompts - ${path.slice(1).replace('/', ' / ')} - Hop`;
const load = async () => {
status.textContent = 'Loading prompt records…';
page.querySelector('.hop-prompt-list, .hop-prompts__empty')?.remove();
try {
const prompts = await portablePromptRecords(path);
if (!prompts.length) {
status.textContent = 'No published prompt records';
const empty = makeElement('div', 'hop-prompts__empty');
empty.append(
makeElement('strong', '', 'No prompts have been published yet.'),
document.createTextNode('Hop publishes an immutable record for each prompt when it creates a proposal. Those files travel with the repository and appear here after the push.'),
);
page.append(empty);
return;
}
status.textContent = prompts.length === 1 ? '1 captured request' : `${prompts.length} captured requests`;
if (!prompts.length) {
const empty = makeElement('div', 'hop-prompts__empty');
empty.append(
makeElement('strong', '', 'The published prompt record set is empty.'),
document.createTextNode('Capture work through Hop and create a proposal to publish its prompt records.'),
);
page.append(empty);
return;
}
const list = makeElement('div', 'hop-prompt-list');
prompts.forEach((prompt) => list.append(promptRow(prompt)));
page.append(list);
} catch (error) {
status.textContent = error.message || 'Could not load prompt records.';
}
};
refresh.addEventListener('click', load);
load();
}
async function copyToClipboard(value) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(value);
return true;
} catch {
// Fall back for browsers that block clipboard access outside a secure context.
}
}
const input = document.createElement('textarea');
input.value = value;
input.setAttribute('readonly', '');
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.append(input);
input.select();
input.setSelectionRange(0, value.length);
const copied = document.execCommand('copy');
input.remove();
return copied;
}
function bindInstallationSwitchers() {
for (const switcher of document.querySelectorAll('.hop-agent-install-shell')) {
if (switcher.dataset.hopInstallBound === 'true') continue;
switcher.dataset.hopInstallBound = 'true';
const buttons = [...switcher.querySelectorAll('[data-hop-install-command]')];
const output = switcher.querySelector('[data-hop-install-command-output]');
const copyButton = switcher.querySelector('[data-hop-copy]');
const status = copyButton ? document.getElementById(copyButton.getAttribute('aria-describedby')) : null;
for (const button of buttons) {
button.addEventListener('click', () => {
for (const option of buttons) option.setAttribute('aria-pressed', String(option === button));
if (output) output.textContent = button.dataset.hopInstallCommand;
if (copyButton) copyButton.setAttribute('aria-label', `Copy ${button.dataset.hopInstallPlatform} installation command`);
if (status) status.textContent = '';
});
}
}
}
function bindInstallationCopyButtons() {
for (const button of document.querySelectorAll('[data-hop-copy]')) {
if (button.dataset.hopCopyBound === 'true') continue;
button.dataset.hopCopyBound = 'true';
button.addEventListener('click', async () => {
const command = button.closest('.hop-agent-install-command')?.querySelector('code')?.textContent?.trim();
const status = document.getElementById(button.getAttribute('aria-describedby'));
if (!command) return;
button.disabled = true;
button.textContent = 'Copying…';
const copied = await copyToClipboard(command);
button.disabled = false;
button.textContent = copied ? 'Copied' : 'Retry';
if (status) status.textContent = copied ? 'Command copied to clipboard.' : 'Could not copy automatically. Select the command and copy it.';
window.setTimeout(() => {
button.textContent = 'Copy';
if (status) status.textContent = '';
}, 2400);
});
}
}
function replaceBrandImages() {
const upstreamBrandAsset = /\/img\/(?:avatar_default\.png|logo\.png|logo\.svg)(?:\?.*)?$/;
for (const image of document.querySelectorAll('img[src]')) {
if (!upstreamBrandAsset.test(image.src)) continue;
image.src = image.src.replace(upstreamBrandAsset, '/img/hop.svg?v=4');
}
}
function removeHelpNavigation() {
for (const link of document.querySelectorAll('#navbar a[href*="docs.gitea.com"]')) {
if (link.textContent.trim() === 'Help') link.remove();
}
}
function applyHopSemantics() {
const brandLogo = document.querySelector('#navbar-logo img');
if (brandLogo) brandLogo.src = brandLogo.src.replace(/\/img\/(?:logo|hop)\.svg(?:\?.*)?$/, '/img/hop.svg?v=4');
replaceBrandImages();
removeHelpNavigation();
for (const root of document.querySelectorAll(semanticRoots)) replaceExactText(root);
replaceAccessibleLabels(document);
addPromptsNavigation();
addWorkflowSummary();
renderPromptPage();
bindInstallationSwitchers();
bindInstallationCopyButtons();
const title = replacePhrases(document.title);
if (title !== document.title) document.title = title;
document.documentElement.dataset.hopNative = 'true';
}
let queued = false;
function queueApply() {
if (queued) return;
queued = true;
window.requestAnimationFrame(() => {
queued = false;
applyHopSemantics();
});
}
applyHopSemantics();
document.addEventListener('DOMContentLoaded', queueApply);
document.addEventListener('htmx:afterSwap', queueApply);
new MutationObserver(queueApply).observe(document.documentElement, {childList: true, subtree: true});
})();
+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
<meta name="keywords" content="{{MetaKeywords}}">
<meta name="referrer" content="same-origin">{{/* required by: 1. "redirect_to" cookie; 2. cross-origin protection */}}
{{if .GoGetImport}}
<meta name="go-import" content="{{.GoGetImport}} git {{.RepoCloneLink.HTTPS}}">
<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}">
{{end}}
{{if and .EnableFeed .FeedURL}}
<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
{{end}}
<link rel="icon" href="{{AssetUrlPrefix}}/img/hop.svg?v=5" type="image/svg+xml">
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png?v=5" type="image/png">
{{template "base/head_opengraph" .}}
{{template "base/head_style" .}}
{{template "base/head_script" .}}
{{template "custom/header" .}}
</head>
<body hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
{{template "custom/body_outer_pre" .}}
<div class="full height">
<noscript>{{ctx.Locale.Tr "enable_javascript"}}</noscript>
{{template "custom/body_inner_pre" .}}
{{if not .PageIsInstall}}
{{template "base/head_navbar" .}}
{{end}}
{{if false}}
{{/* to make html structure "likely" complete to prevent IDE warnings */}}
</div>
</body>
</html>
{{end}}
@@ -0,0 +1,3 @@
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/hop-home.css?v=4">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/hop-prompts.css?v=3">
<script src="{{AssetUrlPrefix}}/js/hop-native-v14.js?v=1"></script>
+15
View File
@@ -0,0 +1,15 @@
<link rel="preload" href="{{AssetUrlPrefix}}/img/hop.svg?v=4" as="image" type="image/svg+xml">
<style>
html:not([data-hop-native="true"]) {
background: var(--color-body);
}
html:not([data-hop-native="true"]) body {
visibility: hidden;
}
#navbar-logo img {
content: url("{{AssetUrlPrefix}}/img/hop.svg?v=4");
}
</style>
<noscript><style>html body { visibility: visible !important; }</style></noscript>
+628
View File
@@ -0,0 +1,628 @@
{{template "base/head" .}}
<style>
.hop-agent-home {
--hop-agent-accent: color-mix(in srgb, var(--color-primary) 62%, var(--color-text));
color: var(--color-text);
margin: 0 auto;
max-width: 1120px;
padding: clamp(4.5rem, 8vw, 7.5rem) clamp(1.25rem, 4vw, 2.5rem) clamp(5rem, 9vw, 8rem);
width: 100%;
}
.hop-agent-home *,
.hop-agent-home *::before,
.hop-agent-home *::after {
box-sizing: border-box;
}
.hop-agent-home > :first-child {
margin-top: 0 !important;
}
.hop-agent-home h1,
.hop-agent-home h2,
.hop-agent-home h3,
.hop-agent-home p,
.hop-agent-home ol,
.hop-agent-home table {
margin: 0;
}
.hop-agent-home h1,
.hop-agent-home h2,
.hop-agent-home h3 {
color: var(--color-text);
text-wrap: balance;
}
.hop-agent-label {
color: var(--color-text-light-2) !important;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.12em;
line-height: 1.35;
text-transform: uppercase;
}
.hop-agent-hero {
align-items: center;
display: grid;
gap: clamp(2.5rem, 5vw, 5rem);
grid-template-columns: minmax(0, 1fr) clamp(280px, 29vw, 400px);
}
.hop-agent-hero-copy {
min-width: 0;
}
.hop-agent-hero h1 {
font-size: clamp(3.2rem, 6.1vw, 5.5rem);
font-weight: 700;
letter-spacing: -0.06em;
line-height: 0.96;
margin-top: 1.35rem;
max-width: 16ch;
}
.hop-agent-hero h1 span {
display: block;
}
.hop-agent-hero h1 span:last-child {
color: var(--hop-agent-accent);
}
.hop-agent-lede {
color: var(--color-text-light-2) !important;
font-size: clamp(1.12rem, 1.7vw, 1.32rem);
line-height: 1.6;
margin-top: 2rem !important;
max-width: 68ch;
}
.hop-agent-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 2rem;
}
.hop-agent-actions .button {
margin: 0;
}
.hop-agent-note {
color: var(--color-text-light-2) !important;
font-size: 0.9rem;
line-height: 1.55;
margin-top: 1.2rem !important;
}
.hop-agent-mascot {
align-items: center;
display: flex;
justify-content: center;
margin: 0;
min-width: 0;
pointer-events: none;
}
.hop-agent-mascot img {
display: block;
filter: drop-shadow(0 1.5rem 2.5rem color-mix(in srgb, var(--color-primary) 18%, transparent));
height: auto;
max-width: 100%;
width: 400px;
}
.hop-agent-flow {
border-top: 1px solid var(--color-secondary);
display: grid;
gap: clamp(3rem, 7vw, 6rem);
grid-template-columns: minmax(220px, 0.7fr) minmax(0, 1.3fr);
margin-top: clamp(3.5rem, 5vw, 5rem);
padding-top: clamp(2.25rem, 3.5vw, 3.5rem);
}
.hop-agent-section-heading h2 {
font-size: clamp(2.2rem, 4vw, 3.6rem);
font-weight: 650;
letter-spacing: -0.045em;
line-height: 1.04;
margin-top: 0.9rem;
}
.hop-agent-section-heading > p:last-child {
color: var(--color-text-light-2) !important;
font-size: 1rem;
line-height: 1.6;
margin-top: 1.2rem !important;
max-width: 48ch;
}
.hop-agent-steps {
border-top: 1px solid var(--color-secondary);
list-style: none;
padding: 0;
}
.hop-agent-steps li {
align-items: start;
border-bottom: 1px solid var(--color-secondary);
display: grid;
gap: 1rem;
grid-template-columns: 2rem minmax(7rem, 0.42fr) minmax(0, 1fr);
padding: 1.45rem 0;
}
.hop-agent-step-number {
color: var(--color-text-light-2);
font-family: var(--fonts-monospace);
font-size: 0.75rem;
font-weight: 700;
line-height: 1.7;
}
.hop-agent-steps h3 {
font-size: 1.08rem;
font-weight: 650;
letter-spacing: -0.015em;
line-height: 1.35;
}
.hop-agent-steps p {
color: var(--color-text-light-2) !important;
font-size: 0.96rem;
line-height: 1.58;
}
.hop-agent-why {
border-top: 1px solid var(--color-secondary);
margin-top: clamp(3.5rem, 5vw, 5rem);
padding-top: clamp(2.25rem, 3.5vw, 3.5rem);
}
.hop-agent-why .hop-agent-section-heading {
max-width: 760px;
}
.hop-agent-table-wrap {
margin-top: clamp(2.75rem, 5vw, 4.5rem);
overflow-x: auto;
}
.hop-agent-compare {
border-collapse: collapse;
min-width: 700px;
width: 100%;
}
.hop-agent-compare caption {
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.hop-agent-compare th,
.hop-agent-compare td {
border-bottom: 1px solid var(--color-secondary);
padding: 1.1rem 1rem 1.1rem 0;
text-align: left;
vertical-align: top;
}
.hop-agent-compare thead th {
color: var(--color-text-light-2);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.hop-agent-compare tbody th {
color: var(--color-text);
font-size: 0.94rem;
font-weight: 650;
width: 19%;
}
.hop-agent-compare td {
color: var(--color-text-light-2);
font-size: 0.94rem;
line-height: 1.5;
width: 40.5%;
}
.hop-agent-compare td:last-child {
color: var(--color-text);
font-weight: 550;
}
.hop-agent-installs {
border-top: 1px solid var(--color-secondary);
margin-top: 1.5rem;
max-width: 100%;
padding-top: 1rem;
}
.hop-agent-install-heading {
align-items: baseline;
display: flex;
gap: 1rem;
justify-content: space-between;
}
.hop-agent-install-shell {
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius);
margin-top: 0.7rem;
min-width: 0;
overflow: hidden;
}
.hop-agent-install-tabs {
align-items: center;
display: flex;
gap: 0.25rem;
padding: 0.25rem;
}
.hop-agent-install-tab {
background: transparent;
border: 0;
border-radius: calc(var(--border-radius) - 2px);
color: var(--color-text-light-2);
cursor: pointer;
font: inherit;
font-size: 0.72rem;
font-weight: 650;
line-height: 1;
padding: 0.55rem 0.7rem;
}
.hop-agent-install-tab[aria-pressed="true"] {
background: color-mix(in srgb, var(--color-primary) 18%, transparent);
color: var(--color-text);
}
.hop-agent-install-tab:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 1px;
}
.hop-agent-install-command {
align-items: center;
background: color-mix(in srgb, var(--color-secondary) 42%, transparent);
border-top: 1px solid var(--color-secondary);
display: grid;
gap: 0.75rem;
grid-template-columns: minmax(0, 1fr) auto;
min-width: 0;
padding: 0.75rem 0.8rem;
}
.hop-agent-copy-button {
background: transparent;
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius);
color: var(--color-text);
cursor: pointer;
font: inherit;
font-size: 0.78rem;
font-weight: 650;
line-height: 1;
padding: 0.5rem 0.65rem;
}
.hop-agent-copy-button:hover {
background: color-mix(in srgb, var(--color-primary) 16%, transparent);
}
.hop-agent-copy-button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.hop-agent-install-command code {
color: var(--color-text);
display: block;
font-family: var(--fonts-monospace);
font-size: clamp(0.72rem, 1.12vw, 0.84rem);
line-height: 1.45;
min-width: 0;
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-word;
}
.hop-agent-copy-status {
color: var(--color-text-light-2);
display: block;
font-size: 0.78rem;
line-height: 1.3;
margin-top: 0.45rem;
}
.hop-agent-copy-status:empty {
display: none;
}
.hop-agent-repo-link {
align-items: center;
display: inline-flex;
font-size: 0.84rem;
font-weight: 600;
gap: 0.45rem;
}
.hop-agent-repo-link::after {
content: '↗';
font-size: 0.9em;
}
.hop-agent-finish {
align-items: end;
border-top: 1px solid var(--color-secondary);
display: grid;
gap: 2rem;
grid-template-columns: minmax(0, 1fr) auto;
margin-top: clamp(3rem, 4.5vw, 4.5rem);
padding-top: clamp(2.25rem, 3.5vw, 3.5rem);
}
.hop-agent-finish h2 {
font-size: clamp(2.3rem, 4.8vw, 4.4rem);
font-weight: 650;
letter-spacing: -0.05em;
line-height: 1;
}
.hop-agent-finish p {
color: var(--color-text-light-2) !important;
font-size: 1rem;
line-height: 1.6;
margin-top: 1rem !important;
max-width: 60ch;
}
.hop-agent-finish .button {
margin: 0;
}
@media (max-width: 800px) {
.hop-agent-hero {
grid-template-columns: 1fr;
}
.hop-agent-mascot img {
width: min(70vw, 320px);
}
.hop-agent-flow {
grid-template-columns: 1fr;
}
.hop-agent-flow .hop-agent-section-heading {
max-width: 620px;
}
}
@media (max-width: 620px) {
.hop-agent-home {
padding-top: 3.5rem;
}
.hop-agent-hero h1 {
font-size: clamp(2.75rem, 13vw, 4rem);
max-width: none;
}
.hop-agent-mascot img {
width: min(72vw, 260px);
}
.hop-agent-actions {
align-items: stretch;
flex-direction: column;
}
.hop-agent-actions .button,
.hop-agent-finish .button {
width: 100%;
}
.hop-agent-steps li {
gap: 0.4rem 0.8rem;
grid-template-columns: 1.6rem minmax(0, 1fr);
}
.hop-agent-steps p {
grid-column: 2;
}
.hop-agent-table-wrap {
overflow: visible;
}
.hop-agent-compare {
display: block;
min-width: 0;
}
.hop-agent-compare thead {
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
position: absolute;
width: 1px;
}
.hop-agent-compare tbody,
.hop-agent-compare tr,
.hop-agent-compare th,
.hop-agent-compare td {
display: block;
width: 100%;
}
.hop-agent-compare tbody th,
.hop-agent-compare tbody td {
width: 100%;
}
.hop-agent-compare tbody tr {
border-bottom: 1px solid var(--color-secondary);
padding: 1.35rem 0;
}
.hop-agent-compare tbody th,
.hop-agent-compare td {
border: 0;
padding: 0;
}
.hop-agent-compare td {
margin-top: 0.8rem;
}
.hop-agent-compare td::before {
color: var(--color-text-light-2);
content: 'Standard Git';
display: block;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.09em;
margin-bottom: 0.2rem;
text-transform: uppercase;
}
.hop-agent-compare td:last-child::before {
color: var(--color-text-light-2);
content: 'Hop';
}
.hop-agent-finish {
align-items: start;
grid-template-columns: 1fr;
}
.hop-agent-install-heading {
align-items: center;
}
.hop-agent-install-command {
align-items: start;
}
}
</style>
<main id="hop-main" tabindex="-1" role="main" aria-label="Hop, agent-first version control" class="page-content hop-agent-home">
<section class="hop-agent-hero" aria-labelledby="hop-agent-title">
<div class="hop-agent-hero-copy">
<p class="hop-agent-label">Agent-first version control</p>
<h1 id="hop-agent-title"><span>Tell agents what to build.</span><span>Hop handles the Git.</span></h1>
<p class="hop-agent-lede">Run one coding agent or many. Hop captures the request, isolates every attempt, combines concurrent edits, and routes real conflicts through an agent reconciliation loop. Once checks pass, the agent lands the result automatically by default. You do not have to manage a branch or write a commit.</p>
<div class="hop-agent-actions">
{{if .IsSigned}}
<a class="ui primary button" href="{{AppSubUrl}}/repo/create">Create a repository</a>
<a class="ui basic button" href="{{AppSubUrl}}/explore/repos">Explore repositories</a>
{{else}}
<a class="ui primary button" href="{{AppSubUrl}}/user/sign_up">Create a Hop account</a>
<a class="ui basic button" href="#why-hop">Why use Hop?</a>
{{end}}
</div>
<div class="hop-agent-installs" aria-label="Install Hop">
<div class="hop-agent-install-heading">
<p class="hop-agent-label">Install Hop</p>
<a class="hop-agent-repo-link" href="https://githop.xyz/GnosysLabs/Hop" target="_blank" rel="noreferrer">Repository</a>
</div>
<div class="hop-agent-install-shell">
<div class="hop-agent-install-tabs" role="group" aria-label="Installation platform">
<button class="hop-agent-install-tab" type="button" aria-pressed="true" data-hop-install-command="curl -fsSL https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.sh | sh" data-hop-install-platform="macOS and Linux">macOS + Linux</button>
<button class="hop-agent-install-tab" type="button" aria-pressed="false" data-hop-install-command="irm https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.ps1 | iex" data-hop-install-platform="Windows PowerShell">Windows</button>
</div>
<div class="hop-agent-install-command">
<code data-hop-install-command-output>curl -fsSL https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.sh | sh</code>
<button class="hop-agent-copy-button" type="button" data-hop-copy aria-label="Copy macOS and Linux installation command" aria-describedby="hop-install-status">Copy</button>
</div>
</div>
<span class="hop-agent-copy-status" id="hop-install-status" role="status" aria-live="polite"></span>
</div>
<p class="hop-agent-note">Git stays underneath for compatibility. The mechanics disappear from your workflow.</p>
</div>
<figure class="hop-agent-mascot">
<img src="{{AssetUrlPrefix}}/img/hop.svg?v=4" alt="Hop robot mascot" width="512" height="512" decoding="async" fetchpriority="high">
</figure>
</section>
<section class="hop-agent-flow" aria-labelledby="hop-agent-flow-title">
<div class="hop-agent-section-heading">
<p class="hop-agent-label">One instruction in. Working code out.</p>
<h2 id="hop-agent-flow-title">From prompt to landed code, without Git ceremony.</h2>
<p>Hop runs the version-control workflow around your agents. You stay focused on the outcome.</p>
</div>
<ol class="hop-agent-steps">
<li>
<span class="hop-agent-step-number">01</span>
<h3>Prompt captured</h3>
<p>Your instruction becomes durable state before any agent touches the project.</p>
</li>
<li>
<span class="hop-agent-step-number">02</span>
<h3>Agents work in parallel</h3>
<p>Each agent runs in its own Hop workspace. Several can edit the same files at the same time.</p>
</li>
<li>
<span class="hop-agent-step-number">03</span>
<h3>Hop reconciles</h3>
<p>Compatible hunks merge automatically. A real conflict starts an agent reconciliation loop, then runs the checks again. Ordinary code conflicts never get handed back to you.</p>
</li>
<li>
<span class="hop-agent-step-number">04</span>
<h3>Code lands</h3>
<p>After required checks pass, the agent lands by default and Hop synchronizes the project. No manual commit, rebase, or merge.</p>
</li>
</ol>
</section>
<section class="hop-agent-why" id="why-hop" aria-labelledby="hop-agent-why-title">
<div class="hop-agent-section-heading">
<p class="hop-agent-label">Why not just Git?</p>
<h2 id="hop-agent-why-title">Git stores code. Hop runs the work.</h2>
<p>Git was designed to track and exchange source history, not to coordinate autonomous coding agents. Hop keeps Git underneath, then adds an agent workflow with state, isolation, evidence, reconciliation, and automatic landing after checks.</p>
</div>
<div class="hop-agent-table-wrap">
<table class="hop-agent-compare">
<caption>Comparison of standard Git workflows and Hop</caption>
<thead>
<tr><th scope="col">Work</th><th scope="col">Standard Git</th><th scope="col">Hop</th></tr>
</thead>
<tbody>
<tr><th scope="row">Start</th><td>You create a branch or worktree.</td><td>Starting a task creates an isolated agent attempt.</td></tr>
<tr><th scope="row">Save progress</th><td>You decide when and how to commit.</td><td>Checks and follow-ups capture immutable checkpoints.</td></tr>
<tr><th scope="row">Parallel edits</th><td>Incompatible overlapping edits require manual conflict handling.</td><td>Hop three-way-merges compatible edits and sends unresolved conflicts through agent reconciliation.</td></tr>
<tr><th scope="row">Verify</th><td>Validation is conventionally attached to commits.</td><td>Hop records evidence for the exact checkpoint and requires checked reconciliation.</td></tr>
<tr><th scope="row">Finish</th><td>You merge, rebase, and clean up.</td><td>The agent lands checked work automatically by default.</td></tr>
</tbody>
</table>
</div>
</section>
<section class="hop-agent-finish" aria-labelledby="hop-agent-finish-title">
<div>
<h2 id="hop-agent-finish-title">Git compatibility without Git busywork.</h2>
<p>Keep Git-compatible hosting and tooling underneath. The day-to-day branch, commit, rebase, and conflict choreography disappears.</p>
</div>
{{if .IsSigned}}
<a class="ui primary button" href="{{AppSubUrl}}/repo/create">Create a repository</a>
{{else}}
<a class="ui primary button" href="{{AppSubUrl}}/user/sign_up">Create a Hop account</a>
{{end}}
</section>
</main>
{{template "base/footer" .}}