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
@@ -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});
})();