(() => { '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', '', 'Agent 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.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 was requested, which agent acted, and the result it reported. Every entry is tied to the task, attempt, and state that produced it.'), ); 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}); })();