12c806c77f
Hop-State: A_06FN8NFDRFAFN5VCJTHJAAG Hop-Proposal: R_06FN8NEGJ054CPMJF9ZAN70 Hop-Task: T_06FN8HT6XG332C3XY6PYCCG Hop-Attempt: AT_06FN8HT6XGRXW0B4WPFFBJG
410 lines
16 KiB
JavaScript
410 lines
16 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
function replaceActivityAvatars() {
|
|
if (!/\/activity\/?$/.test(window.location.pathname)) return;
|
|
const main = document.querySelector('[role="main"]');
|
|
if (!main) return;
|
|
|
|
for (const image of main.querySelectorAll('img.ui.avatar, .activity-bar-graph img, .activity-bar-graph-alt img')) {
|
|
image.src = '/assets/img/hop.svg?v=5';
|
|
}
|
|
}
|
|
|
|
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'), 'Issues'),
|
|
workflowLink(navigationLink('/pulls'), 'Pull Requests'),
|
|
workflowLink(navigationLink('/actions'), 'Actions'),
|
|
].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;
|
|
summary.append(request);
|
|
details.append(summary, promptDetails(prompt));
|
|
return details;
|
|
}
|
|
|
|
async function personalPromptRecords(path) {
|
|
const [owner, repo] = path.slice(1).split('/');
|
|
const query = new URLSearchParams({owner, repo});
|
|
const response = await fetch(`/hop/api/v1/prompts?${query}`, {credentials: 'same-origin'});
|
|
if (response.status === 401) throw new Error('Sign in to view your prompt history.');
|
|
if (!response.ok) throw new Error('Could not load your prompt history.');
|
|
const payload = await response.json();
|
|
return (Array.isArray(payload.prompts) ? payload.prompts : [])
|
|
.filter((prompt) => {
|
|
const request = String(prompt.prompt || '');
|
|
const outcome = String(prompt.response_summary || '');
|
|
return prompt.status !== 'active'
|
|
&& prompt.status !== 'running'
|
|
&& !request.startsWith('Resolve proposal ')
|
|
&& !outcome.startsWith('hop-reconciliation-conflicts:');
|
|
})
|
|
.sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)));
|
|
}
|
|
|
|
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 your requests for this repository. Prompt history is private to your signed-in account.'),
|
|
);
|
|
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 personalPromptRecords(path);
|
|
if (!prompts.length) {
|
|
status.textContent = 'No prompts for your account';
|
|
const empty = makeElement('div', 'hop-prompts__empty');
|
|
empty.append(
|
|
makeElement('strong', '', 'You have no prompt history for this repository.'),
|
|
document.createTextNode('Only requests attributed to your signed-in Gitea account appear here.'),
|
|
);
|
|
page.append(empty);
|
|
return;
|
|
}
|
|
status.textContent = prompts.length === 1 ? '1 captured request' : `${prompts.length} captured requests`;
|
|
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 publicHomepageRequested() {
|
|
return new URLSearchParams(window.location.search).get('hop') === 'home';
|
|
}
|
|
|
|
async function loadPublicHomepage() {
|
|
const link = document.querySelector('[data-hop-public-home-link]');
|
|
if (link) link.classList.toggle('active', publicHomepageRequested());
|
|
if (!link || !publicHomepageRequested() || document.documentElement.dataset.hopPublicHome) return;
|
|
|
|
document.documentElement.dataset.hopPublicHome = 'loading';
|
|
try {
|
|
const publicURL = new URL(link.href, window.location.origin);
|
|
publicURL.search = '';
|
|
const response = await fetch(publicURL, {credentials: 'omit'});
|
|
if (!response.ok) throw new Error(`Could not load the Hop homepage (${response.status})`);
|
|
|
|
const publicDocument = new DOMParser().parseFromString(await response.text(), 'text/html');
|
|
const publicMain = publicDocument.querySelector('#hop-main');
|
|
const currentMain = document.querySelector('[role="main"]');
|
|
if (!publicMain || !currentMain) throw new Error('The Hop homepage was not present in the response');
|
|
|
|
for (const style of publicMain.parentElement?.querySelectorAll(':scope > style') || []) {
|
|
style.dataset.hopPublicHomeStyle = 'true';
|
|
document.head.append(style);
|
|
}
|
|
currentMain.replaceWith(publicMain);
|
|
document.title = publicDocument.title;
|
|
document.documentElement.dataset.hopPublicHome = 'rendered';
|
|
bindInstallationSwitchers();
|
|
bindInstallationCopyButtons();
|
|
} catch (error) {
|
|
delete document.documentElement.dataset.hopPublicHome;
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
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();
|
|
replaceActivityAvatars();
|
|
removeHelpNavigation();
|
|
addPromptsNavigation();
|
|
addWorkflowSummary();
|
|
renderPromptPage();
|
|
bindInstallationSwitchers();
|
|
bindInstallationCopyButtons();
|
|
void loadPublicHomepage();
|
|
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});
|
|
})();
|