diff --git a/codex-rs/tui/src/security_report_assets/script.js b/codex-rs/tui/src/security_report_assets/script.js index 1a28a3b4e6..d9bb70f915 100644 --- a/codex-rs/tui/src/security_report_assets/script.js +++ b/codex-rs/tui/src/security_report_assets/script.js @@ -1 +1,1715 @@ -document.addEventListener("DOMContentLoaded", () => {}); +(function () { + // CSS.escape polyfill for older browsers + if (!window.CSS || typeof window.CSS.escape !== 'function') { + window.CSS = window.CSS || {}; + window.CSS.escape = function (value) { + return String(value).replace(/[^a-zA-Z0-9_\-]/g, function (ch) { + const hex = ch.charCodeAt(0).toString(16); + return '\\' + hex + ' '; + }); + }; + } + const contentEl = document.getElementById('content'); + const navList = document.getElementById('navList'); + const tocList = document.getElementById('tocList'); + const clearTocBtn = document.getElementById('clearToc'); + const jobProgressHost = document.getElementById('jobProgressHost'); + const fileInput = document.getElementById('fileInput'); + const dropOverlay = document.getElementById('dropOverlay'); + const themeToggle = document.getElementById('themeToggle'); + const shareBtn = document.getElementById('shareBtn'); + const editToggle = document.getElementById('editToggle'); + const sitePath = document.getElementById('site-path'); + const navToggle = document.getElementById('navToggle'); + const sectionSearch = document.getElementById('sectionSearch'); + + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + function setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + try { localStorage.setItem('theme', theme); } catch {} + updateThemeUI(theme); + } + function getSavedTheme() { + try { return localStorage.getItem('theme'); } catch { return null; } + } + function currentTheme() { + const saved = getSavedTheme(); + return saved || (prefersDark.matches ? 'dark' : 'light'); + } + function initTheme() { + setTheme(currentTheme()); + } + function updateThemeUI(theme) { + // Update toggle button state and icon + if (!themeToggle) return; + const isDark = theme === 'dark'; + themeToggle.setAttribute('aria-pressed', String(isDark)); + themeToggle.setAttribute('title', isDark ? 'Switch to light mode' : 'Switch to dark mode'); + themeToggle.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode'); + try { + const icon = document.getElementById('themeIcon'); + if (icon) { + // Simple sun/moon paths; keep present default if any issue + // Sun icon when light mode, moon icon when dark mode + const sun = 'M6.76 4.84l-1.8-1.79-1.42 1.41 1.79 1.8 1.43-1.42zm10.48 0l1.8-1.79 1.42 1.41-1.79 1.8-1.43-1.42zM12 4V1h-2v3h2zm0 19v-3h-2v3h2zm7-9h3v-2h-3v2zM1 12H4v-2H1v2zm14.24 7.16l1.8 1.79 1.42-1.41-1.79-1.8-1.43 1.42zM4.84 17.24l-1.79 1.8 1.41 1.41 1.8-1.79-1.42-1.42zM12 6a6 6 0 100 12 6 6 0 000-12z'; + const moon = 'M21.64 13a1 1 0 0 0-1.11-.27 8 8 0 0 1-10.26-10.26 1 1 0 0 0-1.38-1.26 10 10 0 1 0 13 13 1 1 0 0 0-.25-1.21Z'; + icon.setAttribute('d', isDark ? sun : moon); + } + } catch {} + } + // Toggle theme and re-render theme-sensitive elements + themeToggle && themeToggle.addEventListener('click', () => { + const next = currentTheme() === 'dark' ? 'light' : 'dark'; + setTheme(next); + try { enhanceMermaid(); } catch {} + }); + // If user hasn't manually chosen a theme (no saved), follow OS changes + try { + prefersDark.addEventListener('change', (e) => { + if (!getSavedTheme()) { + setTheme(e.matches ? 'dark' : 'light'); + try { enhanceMermaid(); } catch {} + } + }); + } catch {} + + fileInput.addEventListener('change', async (e) => { + const t = e.target; + const file = t && t.files && t.files[0]; + if (file) await loadFile(file); + }); + + ;['dragenter', 'dragover'].forEach(evt => { + window.addEventListener(evt, (e) => { + e.preventDefault(); + e.stopPropagation(); + dropOverlay.classList.add('show'); + dropOverlay.setAttribute('aria-hidden', 'false'); + }); + }); + ;['dragleave', 'drop'].forEach(evt => { + window.addEventListener(evt, (e) => { + e.preventDefault(); + e.stopPropagation(); + dropOverlay.classList.remove('show'); + dropOverlay.setAttribute('aria-hidden', 'true'); + }); + }); + window.addEventListener('drop', async (e) => { + const dt = e.dataTransfer; + const file = dt && dt.files && dt.files[0]; + if (file && file.name.match(/\.(md|markdown|txt)$/i)) { + await loadFile(file); + } + }); + + // Sidebar collapse/expand + function setSidebarCollapsed(collapsed) { + const cls = 'sidebar-collapsed'; + document.body.classList.toggle(cls, !!collapsed); + if (navToggle) { + navToggle.setAttribute('aria-pressed', collapsed ? 'true' : 'false'); + navToggle.setAttribute('aria-label', collapsed ? 'Expand sidebar' : 'Collapse sidebar'); + navToggle.setAttribute('title', collapsed ? 'Expand sidebar' : 'Collapse sidebar'); + try { + const icon = document.getElementById('navIcon'); + // Chevron-right when collapsed (to expand), chevron-left when expanded + const right = 'M9 6l6 6-6 6'; + const left = 'M15 6l-6 6 6 6'; + if (icon) icon.setAttribute('d', collapsed ? right : left); + } catch {} + } + try { localStorage.setItem('sidebar:collapsed', collapsed ? '1' : '0'); } catch {} + } + if (navToggle) { + navToggle.addEventListener('click', () => { + const collapsed = document.body.classList.contains('sidebar-collapsed'); + setSidebarCollapsed(!collapsed); + }); + } + + // Section search events + if (sectionSearch) { + sectionSearch.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSectionSearch(sectionSearch.value); + } + }); + // Render TOC with filtered results on input; also jump on exact match + sectionSearch.addEventListener('input', () => { + const v = (sectionSearch.value || '').trim(); + renderFilteredNavigation(v); + const low = v.toLowerCase(); + if (!low) return; + for (const i of headingIndex) { + if ((i.text || '').trim().toLowerCase() === low) { + handleSectionSearch(v); + break; + } + } + }); + } + + shareBtn.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(location.href); + shareBtn.textContent = 'Link copied!'; + setTimeout(() => (shareBtn.textContent = 'Share'), 1200); + } catch (e) { + alert('Copy failed. You can manually copy the URL.'); + } + }); + + // Clear selection in sidebar (Outline) + if (clearTocBtn) { + clearTocBtn.addEventListener('click', () => { + // Remove active highlight + tocList && tocList.querySelectorAll('.active').forEach(el => el.classList.remove('active')); + // Clear hash and scroll to top + try { + const url = new URL(location.href); + url.hash = ''; + history.replaceState(null, '', url.toString()); + } catch {} + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + } + + // Sidebar Job Progress integration: subscribes to current job via SSE + let jp = null; let jpES = null; + function initJobProgress() { + if (!jobProgressHost) return; + jobProgressHost.innerHTML = ''; + const box = document.createElement('div'); + box.className = 'job-progress'; + box.innerHTML = ` +
+ Job progress + +
+
No active job
+
+
+ `; + jobProgressHost.appendChild(box); + jp = { + box, + status: box.querySelector('#jpStatus'), + bar: box.querySelector('#jpBar'), + logs: box.querySelector('#jpLogs'), + clearBtn: box.querySelector('#jpClearBtn'), + }; + jp.clearBtn.addEventListener('click', () => { + unsubscribeJobProgress(); + jp.status.textContent = 'No active job'; + jp.bar.style.width = '0%'; + jp.logs.textContent = ''; + }); + // Attach to existing job if present + try { + const current = localStorage.getItem('job:current'); + if (current) subscribeJobProgress(current); + } catch {} + } + + function subscribeJobProgress(id) { + unsubscribeJobProgress(); + if (!id || !jp) return; + jp.status.textContent = 'Running (Job ' + id + ')'; + try { + jpES = new EventSource('/api/events/' + id); + jpES.onmessage = (e) => { + try { var d = JSON.parse(e.data); } catch { var d = e.data; } + if (typeof d === 'object' && d) { + if (d.event === 'close') { + jp.status.textContent = 'Finished (code ' + d.code + ')'; + jp.bar.style.width = '100%'; + try { localStorage.removeItem('job:current'); } catch {} + setTimeout(unsubscribeJobProgress, 3000); + return; + } + if (d.line) { + const line = d.line; + if (/progress\s+(\d+)%/i.test(line)) { + const m = line.match(/(\d+)%/); if (m) jp.bar.style.width = m[1] + '%'; + } + const atBottom = jp.logs.scrollTop + jp.logs.clientHeight >= jp.logs.scrollHeight - 5; + jp.logs.textContent += (d.source === 'stderr' ? '[ERR] ' : '') + line + '\n'; + if (atBottom) jp.logs.scrollTop = jp.logs.scrollHeight; + } + } + }; + jpES.onerror = () => { jp.status.textContent = 'Disconnected'; }; + } catch {} + } + + function unsubscribeJobProgress() { + if (jpES) { try { jpES.close(); } catch {} jpES = null; } + } + + // Listen for job start from job.html + window.addEventListener('storage', (e) => { + if (e.key === 'job:current' && e.newValue && jp) { + subscribeJobProgress(e.newValue); + } + }); + + // Edit mode toggle + if (editToggle) { + editToggle.addEventListener('click', () => { + const isEditing = contentEl.isContentEditable; + if (!isEditing) { + contentEl.contentEditable = 'true'; + contentEl.classList.add('editing'); + editToggle.setAttribute('aria-pressed', 'true'); + editToggle.textContent = 'Done'; + } else { + contentEl.contentEditable = 'false'; + contentEl.classList.remove('editing'); + editToggle.setAttribute('aria-pressed', 'false'); + editToggle.textContent = 'Edit'; + // Postprocess edited HTML to refresh TOC, IDs, diagrams, etc. + postprocessEditedContent(); + } + }); + } + + if (window.marked) { + marked.setOptions({ + mangle: false, + headerIds: true, + headerPrefix: '', + breaks: false, + gfm: true, + highlight: function (code, lang) { + if (window.hljs) { + if (lang && hljs.getLanguage(lang)) { + return hljs.highlight(code, { language: lang }).value; + } + return hljs.highlightAuto(code).value; + } + return code; + }, + }); + } + + async function loadFile(file) { + const text = await file.text(); + renderMarkdown(text); + sitePath.textContent = `/ Docs / ${file.name}`; + try { + sessionStorage.setItem('doc:' + file.name, text); + } catch {} + const url = new URL(location.href); + url.searchParams.set('file', encodeURIComponent(file.name)); + history.replaceState(null, '', url.toString()); + } + + function slugify(str) { + return str + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); + } + + function renderMarkdown(text) { + // Keep the raw Markdown around so we can copy exact sections (including code fences) + window.__originalMarkdown = text; + const usedMarked = !!window.marked; + const rawHtml = usedMarked ? marked.parse(text) : basicMarkdown(text); + contentEl.innerHTML = rawHtml; + + const headings = Array.from(contentEl.querySelectorAll('h1, h2, h3, h4, h5, h6')); + const used = new Set(); + headings.forEach(h => { + if (!h.id) { + let id = slugify(h.textContent || ''); + let i = 2; + while (used.has(id) || document.getElementById(id)) { + id = `${id}-${i++}`; + } + h.id = id; + used.add(id); + } + }); + + enhanceCodeBlocks(); + + // Syntax highlighting for fallback renderer + if (!usedMarked && window.hljs) { + try { + contentEl.querySelectorAll('pre code').forEach(function (el) { hljs.highlightElement(el); }); + } catch {} + } + + // Mark inline code for color styling only in safe contexts + applyInlineCodeColor(); + + // Remove empty bullets (list items without content) + removeEmptyBullets(); + + enhanceCallouts(); + + // Mermaid diagrams + enhanceMermaid(); + + // Severity badges in tables + enhanceSeverityBadges(); + + // Link bug table titles to detail sections + enhanceBugTableLinks(); + + buildNavigation(headings); + updateSectionSearch(); + + if (location.hash && location.hash.length > 1) { + const target = document.getElementById(decodeURIComponent(location.hash.slice(1))); + if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + initScrollSpy(); + + // Add per-bug floating actions + enhanceBugTicketUI(); + } + + // no cleanup — we rely on correct Markdown + + function applyInlineCodeColor() { + // Reset previous marks + contentEl.querySelectorAll('code.inline-code').forEach(function (c) { c.classList.remove('inline-code'); }); + // Mark inline code in safe, readable contexts (paragraphs and blockquotes) + const candidates = contentEl.querySelectorAll('p code, blockquote p code'); + candidates.forEach(function (codeEl) { + if (codeEl.closest('pre')) return; + if (codeEl.closest('h1,h2,h3,h4,h5,h6')) return; + if (codeEl.closest('summary')) return; + if (codeEl.closest('.callout-header')) return; + codeEl.classList.add('inline-code'); + }); + } + + async function fetchAndRender(url, fallbackText) { + try { + const res = await fetch(url, { cache: 'no-cache' }); + if (!res.ok) throw new Error('HTTP ' + res.status); + const txt = await res.text(); + renderMarkdown(txt); + sitePath.textContent = `/ Docs / ${decodeURIComponent(url.split('/').pop() || url)}`; + return true; + } catch (e) { + if (fallbackText) renderMarkdown(fallbackText); + return false; + } + } + + function enhanceCodeBlocks() { + const blocks = contentEl.querySelectorAll('pre > code'); + blocks.forEach(code => { + const pre = code.parentElement; + if (!pre) return; + // Remove any existing copy buttons to avoid duplicates + pre.querySelectorAll('.copy-btn').forEach(btn => btn.remove()); + const btn = document.createElement('button'); + btn.className = 'copy-btn'; + btn.type = 'button'; + btn.textContent = 'Copy'; + btn.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(code.innerText); + btn.textContent = 'Copied'; + setTimeout(() => (btn.textContent = 'Copy'), 1200); + } catch {} + }); + pre.appendChild(btn); + }); + } + + function postprocessEditedContent() { + // Ensure heading IDs and rebuild ToC + const headings = Array.from(contentEl.querySelectorAll('h1, h2, h3, h4, h5, h6')); + const used = new Set(); + headings.forEach(h => { + if (!h.id || used.has(h.id)) { + let id = slugify(h.textContent || ''); + let i = 2; + while (used.has(id) || document.getElementById(id)) { + id = `${id}-${i++}`; + } + h.id = id; + } + used.add(h.id); + }); + // Re-run enhancers + enhanceCodeBlocks(); + applyInlineCodeColor(); + removeEmptyBullets(); + enhanceCallouts(); + enhanceMermaid(); + enhanceSeverityBadges(); + buildNavigation(headings); + updateSectionSearch(); + initScrollSpy(); + } + + function removeEmptyBullets() { + const items = contentEl.querySelectorAll('li'); + items.forEach(function (li) { + // If there are no element children and no text content, remove the list item + const hasElements = li.children && li.children.length > 0; + const text = (li.textContent || '').trim(); + if (!hasElements && text === '') { + const parent = li.parentElement; + li.remove(); + // If the parent list becomes empty, remove it as well + if (parent && parent.children.length === 0) parent.remove(); + } + }); + } + + function enhanceSeverityBadges() { + const tables = contentEl.querySelectorAll('table'); + tables.forEach(function (table) { + // First: if a Severity column exists, badge that column + const headerCells = table.querySelectorAll('thead tr th'); + let sevIndex = -1; + headerCells.forEach(function (th, idx) { + const t = (th.textContent || '').trim().toLowerCase(); + if (t === 'severity') sevIndex = idx; + }); + if (sevIndex !== -1) { + table.querySelectorAll('tbody tr').forEach(function (tr) { + const cells = tr.querySelectorAll('td'); + if (cells.length <= sevIndex) return; + badgeCellIfSeverity(cells[sevIndex]); + }); + } + // Then: badge any other cells that contain plain severity words + table.querySelectorAll('tbody td').forEach(function (td) { + badgeCellIfSeverity(td); + }); + }); + } + + function badgeCellIfSeverity(td) { + const raw = (td.textContent || '').trim().toLowerCase(); + let cls = ''; + let label = ''; + if (raw === 'high') { cls = 'sev-high'; label = 'High'; } + else if (raw === 'medium' || raw === 'med') { cls = 'sev-med'; label = 'Medium'; } + else if (raw === 'low') { cls = 'sev-low'; label = 'Low'; } + else if (raw === 'ignore' || raw === 'ignored') { cls = 'sev-ign'; label = 'Ignore'; } + if (!cls) return; + td.innerHTML = ''; + const span = document.createElement('span'); + span.className = 'badge ' + cls; + span.textContent = label; + td.appendChild(span); + } + + function enhanceBugTableLinks() { + // Build a map from bug numeric ID -> heading id + const bugHeadMap = new Map(); + Array.from(contentEl.querySelectorAll('h1, h2, h3, h4, h5, h6')).forEach(h => { + const txt = (h.textContent || '').trim(); + const m = txt.match(/^\s*\[(\d+)\]/); + if (m && h.id) bugHeadMap.set(m[1], h.id); + }); + if (bugHeadMap.size === 0) return; + + const tables = contentEl.querySelectorAll('table'); + tables.forEach(table => { + const headerCells = table.querySelectorAll('thead tr th'); + if (!headerCells.length) return; + let idIdx = -1, titleIdx = -1; + headerCells.forEach((th, i) => { + const t = (th.textContent || '').trim().toLowerCase(); + if (t === 'bug id' || t === 'id') idIdx = i; + // Support various column names that contain the bug title/description + if (t === 'title' || t === 'component' || t === 'description') titleIdx = i; + }); + if (idIdx === -1 || titleIdx === -1) return; + + table.querySelectorAll('tbody tr').forEach(tr => { + const cells = tr.querySelectorAll('td'); + if (cells.length <= Math.max(idIdx, titleIdx)) return; + const idCell = cells[idIdx]; + const titleCell = cells[titleIdx]; + const bugIdText = (idCell.textContent || '').trim(); + const bugNumMatch = bugIdText.match(/\d+/); + if (!bugNumMatch) return; + const bugNum = bugNumMatch[0]; + let targetId = bugHeadMap.get(bugNum); + if (!targetId) { + // Fallback: find a heading whose text (without leading [id]) matches the title text + const norm = (s) => (s || '').toLowerCase().replace(/^\s*\[[0-9]+\]\s*/, '').replace(/[^a-z0-9]+/g, ' ').trim(); + const want = norm(titleText); + if (want) { + const heads = Array.from(contentEl.querySelectorAll('h1, h2, h3, h4, h5, h6')); + for (const h of heads) { + const ht = norm(h.textContent || ''); + if (ht && (ht === want || ht.startsWith(want) || want.startsWith(ht))) { + targetId = h.id || ''; + if (targetId) break; + } + } + } + if (!targetId) return; + } + + const titleText = (titleCell.textContent || '').trim(); + titleCell.innerHTML = ''; + const a = document.createElement('a'); + a.href = `#${targetId}`; + a.textContent = titleText; + a.addEventListener('click', (e) => { + e.preventDefault(); + const el = document.getElementById(targetId); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.replaceState(null, '', `#${targetId}`); + }); + titleCell.appendChild(a); + }); + }); + } + + function enhanceBugTicketUI() { + // Find bug headings like "[1] ..." typically h2/h3 + const bugHeads = Array.from(contentEl.querySelectorAll('h2, h3')).filter(h => /^\s*\[\d+\]/.test((h.textContent || '').trim())); + bugHeads.forEach(h => { + const full = (h.textContent || '').trim(); + const idMatch = full.match(/^\s*\[(\d+)\]\s*(.*)$/); + const bugId = idMatch ? idMatch[1] : ''; + const bugTitle = idMatch ? idMatch[2] : full; + + // Prevent duplicates if reprocessing + if (h.nextElementSibling && h.nextElementSibling.classList && h.nextElementSibling.classList.contains('ticket-box')) return; + + // Find assignee/author in subsequent paragraphs until next heading + let author = ''; + let authorId = ''; + let node = h.nextElementSibling; + while (node) { + if (/^H[1-6]$/.test(node.tagName)) break; + if (node.tagName === 'P') { + const t = (node.textContent || '').trim(); + const m = t.match(/(?:^|\b)(?:Author|Assignee|Owner)\s*:\s*([^,(\n]+)(?:\s*\(([^)]+)\))?/i); + if (m) { + author = (m[1] || '').trim(); + authorId = (m[2] || '').trim(); + break; + } + } + node = node.nextElementSibling; + } + if (!author) author = 'Unknown'; + + // Build UI box + const box = document.createElement('div'); + box.className = 'ticket-box'; + box.setAttribute('role', 'region'); + box.setAttribute('aria-label', 'Create ticket'); + + const labelAssignee = document.createElement('label'); + labelAssignee.textContent = 'Assignee'; + const input = document.createElement('input'); + input.type = 'text'; + input.value = author; + if (author && author !== 'Unknown') input.dataset.assigneeName = author; + if (authorId && /^usr_[A-Za-z0-9]+$/.test(authorId)) input.dataset.assigneeId = authorId; + input.setAttribute('aria-label', 'Assignee'); + labelAssignee.appendChild(input); + + const labelTeam = document.createElement('label'); + labelTeam.textContent = 'Team/Project'; + const select = document.createElement('select'); + const projects = [ + { label: 'PSEC', value: 'https://linear.new?team=PSEC' }, + { label: 'SEC', value: 'https://linear.new?team=SEC' }, + { label: 'ENG', value: 'https://linear.new?team=ENG' }, + { label: 'SRE', value: 'https://linear.new?team=SRE' }, + { label: 'PROD', value: 'https://linear.new?team=PROD' }, + { label: 'DESIGN', value: 'https://linear.new?team=DESIGN' }, + ]; + projects.forEach(p => { + const o = document.createElement('option'); + o.value = p.value; o.textContent = p.label; + if (p.label === 'PSEC') o.selected = true; + select.appendChild(o); + }); + labelTeam.appendChild(select); + + const btn = document.createElement('button'); + btn.className = 'ticket-btn'; + btn.type = 'button'; + btn.textContent = 'Create ticket'; + + const status = document.createElement('span'); + status.className = 'ticket-status'; + + // Copy Markdown button + const copyBtn = document.createElement('button'); + copyBtn.className = 'btn small'; + copyBtn.type = 'button'; + copyBtn.textContent = 'Copy markdown'; + copyBtn.addEventListener('click', async () => { + try { + const md = getBugMarkdownSection(window.__originalMarkdown || '', h, bugId); + if (!md) throw new Error('Section not found'); + await navigator.clipboard.writeText(md); + copyBtn.textContent = 'Copied!'; + setTimeout(() => { copyBtn.textContent = 'Copy markdown'; }, 1200); + } catch (e) { + // Fallback: copy rendered text content + try { + const txt = getRenderedSectionText(h); + await navigator.clipboard.writeText(txt); + copyBtn.textContent = 'Copied!'; + setTimeout(() => { copyBtn.textContent = 'Copy markdown'; }, 1200); + } catch {} + } + }); + + btn.addEventListener('click', () => { + const title = (bugTitle || '').trim() || 'New issue'; + let description = getBugMarkdownSection(window.__originalMarkdown || '', h, bugId); + if (!description) description = getRenderedSectionText(h); + // Remove the heading/title from the ticket body + (function stripHeadingFromBody(){ + const stripHeading = (text) => { + if (!text) return text; + const lines = String(text).split(/\r?\n/); + while (lines.length && lines[0].trim() === '') lines.shift(); + if (!lines.length) return ''; + const first = lines[0].trim(); + const headTxt = (h.textContent || '').trim(); + if (/^#{1,6}\s+/.test(first)) { + lines.shift(); + } else if (headTxt && first === headTxt) { + lines.shift(); + } else if (bugId && first.startsWith(`[${bugId}]`)) { + lines.shift(); + } + while (lines.length && lines[0].trim() === '') lines.shift(); + return lines.join('\n').trim(); + }; + description = stripHeading(description); + if (!description) description = ''; + })(); + const rawInput = (input.value || '').trim(); + const sanitized = rawInput.replace(/^`|`$/g, '').trim(); + const assigneeVal = sanitized || author || ''; + const enc = (s) => encodeURIComponent(String(s)).replace(/%20/g, '+'); + let assigneeParam = ''; + if (assigneeVal) { + const initialName = input.dataset.assigneeName || ''; + const initialId = input.dataset.assigneeId || ''; + if (initialId && sanitized === initialName) { + assigneeParam = `&assigneeId=${enc(initialId)}`; + } else if (/^usr_[A-Za-z0-9]+$/.test(assigneeVal)) { + assigneeParam = `&assigneeId=${enc(assigneeVal)}`; + } else { + assigneeParam = `&assignee=${enc(assigneeVal)}`; + } + } + // Determine base from the Team/Project dropdown value. If it's a URL, use it; + // otherwise, fall back to Linear with a team hint. + let base = (select && select.value) || 'https://linear.new'; + if (!/^https?:\/\//i.test(base)) { + base = `https://linear.new?team=${enc(base)}`; + } + // If team is PSEC, default project to Vulnerabilities + const teamLabel = (select && select.options && select.options[select.selectedIndex] + && select.options[select.selectedIndex].textContent) || ''; + const projectParam = teamLabel === 'PSEC' ? `&project=${enc('Vulnerabilities')}` : ''; + // Always include a default label to help triage + const labelParam = `&label=${enc('appsec-agent-bugs')}`; + const params = `title=${enc(title)}&description=${enc(description)}${assigneeParam}${labelParam}${projectParam}`; + const joiner = base.includes('?') ? (/[?&]$/.test(base) ? '' : '&') : '?'; + const url = base + joiner + params; + // Open in a new tab reliably using an anchor element within the click handler gesture + try { + const a = document.createElement('a'); + a.href = url; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + setTimeout(() => { try { a.remove(); } catch {} }, 0); + } catch (e) { + // Fallback to window.open (still attempts a new tab) + const win = window.open(url, '_blank', 'noopener,noreferrer'); + if (!win) location.href = url; + } + }); + + box.appendChild(labelAssignee); + box.appendChild(labelTeam); + box.appendChild(copyBtn); + box.appendChild(btn); + box.appendChild(status); + + // Insert after heading + h.insertAdjacentElement('afterend', box); + }); + } + + // Extract the full Markdown for a bug section by matching the bug heading and scanning until next heading + function getBugMarkdownSection(md, headingEl, bugId) { + if (!md || !headingEl) return ''; + const level = (function(tag){ try { return parseInt(tag.replace(/[^0-9]/g,'') || '0', 10); } catch { return 0; } })(headingEl.tagName || ''); + if (!level) return ''; + // Scan the markdown line by line to locate the heading that contains [bugId] + let pos = 0; + let start = -1; + let end = md.length; + let insideFence = false; + let fenceChar = null; + let fenceLen = 0; + const lines = md.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineStartPos = pos; + pos += line.length + 1; // +1 for the split newline + // Detect fenced code blocks + const fenceOpen = line.match(/^([`~]{3,})/); + if (!insideFence && fenceOpen) { + insideFence = true; fenceChar = fenceOpen[1][0]; fenceLen = fenceOpen[1].length; + continue; + } + if (insideFence) { + const fenceClose = new RegExp('^' + (fenceChar ? fenceChar : '`') + '{' + fenceLen + ',}\s*$'); + if (fenceClose.test(line)) { insideFence = false; fenceChar = null; fenceLen = 0; } + continue; + } + // Headings only if not inside a fence + const hm = line.match(/^(#{1,6})\s*(.*?)\s*#*\s*$/); + if (!hm) continue; + const hLevel = hm[1].length; + const title = hm[2] || ''; + // Match [N] at start of the visible heading title + const mId = title.match(/^\s*\[(\d+)\]/); + if (mId && mId[1] === String(bugId) && hLevel === level) { + start = lineStartPos; + // Now find end: next heading with level <= current + for (let j = i + 1, p = pos; j < lines.length; j++) { + const ln = lines[j]; + const lnStart = p; p += ln.length + 1; + const open2 = ln.match(/^([`~]{3,})/); + if (!insideFence && open2) { // enter fence + insideFence = true; fenceChar = open2[1][0]; fenceLen = open2[1].length; continue; } + if (insideFence) { + const fenceClose2 = new RegExp('^' + (fenceChar ? fenceChar : '`') + '{' + fenceLen + ',}\s*$'); + if (fenceClose2.test(ln)) { insideFence = false; fenceChar = null; fenceLen = 0; } + continue; + } + const hm2 = ln.match(/^(#{1,6})\s*(.*?)\s*#*\s*$/); + if (hm2) { + const lvl2 = hm2[1].length; + if (lvl2 <= level) { end = lnStart; break; } + } + } + break; + } + } + if (start === -1) return ''; + return md.slice(start, end).replace(/\s+$/, '') + '\n'; + } + + function getRenderedSectionText(headingEl) { + if (!headingEl) return ''; + const parts = []; + parts.push(headingEl.textContent || ''); + let node = headingEl.nextSibling; + while (node) { + if (node.nodeType === 1) { + const tag = node.tagName ? node.tagName.toLowerCase() : ''; + if (/^h[1-6]$/.test(tag)) break; + parts.push(node.innerText || node.textContent || ''); + } else if (node.nodeType === 3) { + const t = (node.textContent || '').trim(); + if (t) parts.push(t); + } + node = node.nextSibling; + } + return parts.join('\n\n').trim(); + } + + function enhanceCallouts() { + const quotes = contentEl.querySelectorAll('blockquote'); + quotes.forEach(q => { + const first = q.querySelector('p'); + const titleText = (first && first.textContent ? first.textContent.trim() : '') || ''; + if (/^(sources|relevant|note|tip|info|warning)/i.test(titleText)) { + q.classList.add('callout'); + const header = document.createElement('div'); + header.className = 'callout-header'; + const icon = document.createElement('span'); + icon.setAttribute('aria-hidden', 'true'); + icon.textContent = '▸'; + const title = document.createElement('span'); + title.textContent = titleText.replace(/^\w+\s*:\s*/i, (m) => m.replace(/:\s*$/,'')).replace(/:$/, ''); + header.appendChild(icon); + header.appendChild(title); + + const body = document.createElement('div'); + body.className = 'callout-body'; + const rest = Array.from(q.childNodes); + q.innerHTML = ''; + q.appendChild(header); + rest.forEach((node, idx) => { + if (idx === 0 && node.nodeName === 'P') return; // skip first p (we used as header) + body.appendChild(node); + }); + q.appendChild(body); + q.classList.add('open'); + header.addEventListener('click', () => { + const isOpen = q.classList.toggle('open'); + icon.textContent = isOpen ? '▾' : '▸'; + }); + icon.textContent = '▾'; + } + }); + } + + function enhanceMermaid() { + if (!window.mermaid) return; + // Convert fenced code blocks into .mermaid containers + contentEl.querySelectorAll('pre > code.language-mermaid').forEach(code => { + const parent = code.closest('pre'); + if (!parent) return; + const div = document.createElement('div'); + div.className = 'mermaid'; + div.textContent = code.textContent || ''; + parent.replaceWith(div); + }); + + const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default'; + const rs = getComputedStyle(document.documentElement); + const sevHigh = (rs.getPropertyValue('--sev-high') || '#dc2626').trim(); + const sevMed = (rs.getPropertyValue('--sev-med') || '#eab308').trim(); + const sevLow = (rs.getPropertyValue('--sev-low') || '#059669').trim(); + const sevIgn = (rs.getPropertyValue('--sev-ign') || '#64748b').trim(); + // For pie charts we want 'ignore' to be white regardless of badge color + const sevIgnPie = '#ffffff'; + const borderCol = (rs.getPropertyValue('--border') || '#e5e7eb').trim(); + // Neutral fill for unknown/unscored slices (use grey tone) + const neutralFill = (rs.getPropertyValue('--sev-ign') || '#94a3b8').trim(); + const accent = (rs.getPropertyValue('--accent') || '#2563eb').trim(); + const textCol = (rs.getPropertyValue('--text') || '#1f2937').trim(); + try { + // Reset previously processed diagrams to allow re-render + contentEl.querySelectorAll('.mermaid').forEach(el => { + const element = el; + const code = element.getAttribute('data-code') || element.textContent || ''; + const norm = getNormalizedPie(code); + element.setAttribute('data-code', norm.code); + if (norm.labels && norm.labels.length) { + element.setAttribute('data-slices', JSON.stringify(norm.labels)); + } else { + element.removeAttribute('data-slices'); + } + element.removeAttribute('data-processed'); + element.textContent = norm.code; + }); + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'loose', + theme, + themeVariables: { + pie1: sevHigh, + pie2: sevMed, + pie3: sevLow, + pie4: sevIgnPie, + pie5: accent, + pie6: textCol, + }, + }); + mermaid.run({ querySelector: '.mermaid' }).then(() => { + // After render, enforce severity palette on pie charts by label + document.querySelectorAll('#content .mermaid svg').forEach(svg => { + const container = svg.closest('.mermaid'); + let labels = []; + try { + const attr = container && container.getAttribute('data-slices'); + if (attr) labels = JSON.parse(attr); + } catch {} + applySeverityColorsToPie(svg, { sevHigh, sevMed, sevLow, sevIgn: sevIgnPie, accent, textCol, borderCol, neutralFill }, labels); + }); + }); + } catch (e) { + // ignore + } + } + + function getNormalizedPie(code) { + try { + const lines = (code || '').split(/\r?\n/); + // find first non-empty meaningful line + const firstNonEmpty = lines.findIndex(l => l.trim().length > 0); + if (firstNonEmpty === -1) return { code }; + if (!/^\s*pie\b/i.test(lines[firstNonEmpty])) return { code }; // not a pie chart + + const severityOrder = ['high', 'medium', 'low', 'ignore']; + const titleLines = []; + const sliceLines = []; + const otherLines = []; + for (let i = firstNonEmpty + 1; i < lines.length; i++) { + const raw = lines[i]; + const t = raw.trim(); + if (t.length === 0) { otherLines.push(raw); continue; } + if (/^title\b/i.test(t)) { titleLines.push(raw); continue; } + // Match slice: label : value + const m = t.match(/^((?:"[^"]+"|'[^']+'|[^:]+))\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*$/); + if (m) { + // Extract label without quotes + let label = m[1].trim(); + if ((label.startsWith('"') && label.endsWith('"')) || (label.startsWith("'") && label.endsWith("'"))) { + label = label.slice(1, -1); + } + const value = m[2]; + sliceLines.push({ raw: raw, label, value }); + } else { + otherLines.push(raw); + } + } + if (sliceLines.length === 0) return { code }; + + function sevFromLabel(lbl) { + const L = (lbl || '').toLowerCase(); + if (L.includes('high')) return 'high'; + if (L.includes('medium') || L.includes('med')) return 'medium'; + if (L.includes('low')) return 'low'; + if (L.includes('ignore') || L.includes('ignored')) return 'ignore'; + return null; + } + + const sevGroups = { high: [], medium: [], low: [], ignore: [] }; + const others = []; + sliceLines.forEach(s => { + const sev = sevFromLabel(s.label); + if (sev && sevGroups[sev]) sevGroups[sev].push(s); + else others.push(s); + }); + + // Rebuild code with pie line, title lines, then slices in severity order, then others + const indent = ' '; + const out = []; + out.push(lines[firstNonEmpty]); + titleLines.forEach(l => out.push(l)); + const labels = []; + severityOrder.forEach(sev => { + sevGroups[sev].forEach(s => { + const label = /\s/.test(s.label) ? '"' + s.label + '"' : s.label; + out.push(`${indent}${label}: ${s.value}`); + labels.push(s.label); + }); + }); + others.forEach(s => { + const label = /\s/.test(s.label) ? '"' + s.label + '"' : s.label; + out.push(`${indent}${label}: ${s.value}`); + labels.push(s.label); + }); + return { code: out.join('\n'), labels }; + } catch (e) { + return { code }; + } + } + + function applySeverityColorsToPie(svg, palette, labelsOrder) { + try { + // Locate legend items across Mermaid variants + const legendItems = Array.from( + svg.querySelectorAll('g.legend, g.legend > g, g[class*="legend"] > g, g[class*="legend"] g.legend-item') + ); + // Try to find arcs (paths using arc command 'A') + let arcPaths = Array.from(svg.querySelectorAll('path.pieCircle, path')).filter(p => { + const d = (p.getAttribute('d') || '').toUpperCase(); + return d.includes('A') && /M\s*[-\d.]+\s*[,\s]\s*[-\d.]+/i.test(d); + }); + + const normalize = (s) => (s || '').trim().toLowerCase(); + const mapColor = (label) => { + const l = normalize(label); + // Match severity at the start of the label (case-insensitive), + // allowing counts or extra text after, e.g., "High (12)". + if (/^high\b/.test(l)) return palette.sevHigh; + if (/^(medium|med)\b/.test(l)) return palette.sevMed; + if (/^low\b/.test(l)) return palette.sevLow; + if (/^ign(?:ore|ored)?\b/.test(l)) return palette.sevIgn; + if (l.includes('unknown') || l.includes('unscored') || l.includes('unrated') || l.includes('unassigned')) return (palette.neutralFill || null); + return null; + }; + + // Primary: recolor arcs by their own slice label (text within same group), excluding legend labels. + (function colorArcsBySliceLabel(){ + try { + const sliceGroups = Array.from(svg.querySelectorAll('g')) + .filter(g => g.querySelector('path') && g.querySelector('text') && !g.closest('g[class*="legend"]')); + let colored = 0; + sliceGroups.forEach(g => { + const labelEl = g.querySelector('text'); + const path = g.querySelector('path'); + if (!labelEl || !path) return; + const label = labelEl.textContent || ''; + const color = mapColor(label); + if (!color) return; + path.setAttribute('fill', color); + const l = normalize(label); + if (/^ign(?:ore|ored)\b/.test(l)) { + path.setAttribute('stroke', palette.borderCol || '#e5e7eb'); + path.setAttribute('stroke-width', '1'); + } else { + path.removeAttribute('stroke'); + path.removeAttribute('stroke-width'); + } + colored++; + }); + if (colored) return; // stop if arcs colored via slice labels + } catch {} + // Fallback: recolor arcs by legend order (assumes legend order follows arc order) + if (legendItems.length && arcPaths.length) { + const legendLabels = legendItems.map((item) => { + const t = item.querySelector('text'); + return t ? (t.textContent || '') : ''; + }); + legendLabels.forEach((label, idx) => { + const color = mapColor(label); + const path = arcPaths[idx]; + if (!path || !color) return; + path.setAttribute('fill', color); + const l = normalize(label); + if (/^ign(?:ore|ored)\b/.test(l)) { + path.setAttribute('stroke', palette.borderCol || '#e5e7eb'); + path.setAttribute('stroke-width', '1'); + } else { + path.removeAttribute('stroke'); + path.removeAttribute('stroke-width'); + } + }); + } + })(); + + // Color legend markers by their text labels (robust to DOM ordering) + if (legendItems.length) { + legendItems.forEach((item) => { + const textEl = item.querySelector('text'); + const label = textEl ? textEl.textContent : ''; + const color = mapColor(label); + const marker = item.querySelector('rect, path, circle'); + if (marker) { + const l = normalize(label); + if (color) { + // Update inline style if present (takes precedence over attributes) + const style = (marker.getAttribute('style') || '').trim(); + if (style) { + // Replace existing fill and stroke entries; preserve others + let next = style + .replace(/fill\s*:\s*[^;]+/i, '') + .replace(/stroke\s*:\s*[^;]+/i, '') + .replace(/;;+/g, ';') + .replace(/^;|;$/g, ''); + next = (next ? next + '; ' : '') + `fill: ${color}`; + if (/^ign(?:ore|ored)\b/.test(l)) { + next += `; stroke: ${palette.borderCol || '#e5e7eb'}`; + } + marker.setAttribute('style', next); + } else { + marker.setAttribute('fill', color); + if (/^ign(?:ore|ored)\b/.test(l)) { + marker.setAttribute('stroke', palette.borderCol || '#e5e7eb'); + marker.setAttribute('stroke-width', '1'); + } else { + marker.removeAttribute('stroke'); + marker.removeAttribute('stroke-width'); + } + } + } else if (palette.neutralFill && (l.includes('unknown') || l.includes('unscored') || l.includes('unrated') || l.includes('unassigned'))) { + const style = (marker.getAttribute('style') || '').trim(); + if (style) { + let next = style + .replace(/fill\s*:\s*[^;]+/i, '') + .replace(/stroke\s*:\s*[^;]+/i, '') + .replace(/;;+/g, ';') + .replace(/^;|;$/g, ''); + next = (next ? next + '; ' : '') + `fill: ${palette.neutralFill}` + `; stroke: ${palette.borderCol || '#e5e7eb'}`; + marker.setAttribute('style', next); + } else { + marker.setAttribute('fill', palette.neutralFill); + marker.setAttribute('stroke', palette.borderCol || '#e5e7eb'); + marker.setAttribute('stroke-width', '1'); + } + } + } + }); + + // Reorder legend visually and in DOM as High -> Medium -> Low -> Ignore -> Others + try { + const sevKey = (label) => { + const l = normalize(label); + if (/^high\b/.test(l)) return 'high'; + if (/^(medium|med)\b/.test(l)) return 'medium'; + if (/^low\b/.test(l)) return 'low'; + if (/^ign(?:ore|ored)\b/.test(l)) return 'ignore'; + return 'other'; + }; + const pri = { high: 0, medium: 1, low: 2, ignore: 3, other: 4 }; + const items = legendItems.map((g) => { + const tx = (g.getAttribute('transform') || '').match(/translate\(([^,]+),\s*([^\)]+)\)/i); + const x = tx ? parseFloat(tx[1]) : 0; + const y = tx ? parseFloat(tx[2]) : 0; + const textEl = g.querySelector('text'); + const label = textEl ? (textEl.textContent || '') : ''; + return { g, x, y, label, key: sevKey(label) }; + }); + if (items.length) { + const parent = items[0].g.parentNode; + const ys = items.map(i => i.y).sort((a,b) => a - b); + const step = ys.length >= 2 ? Math.max(18, Math.round(ys[1] - ys[0])) : 22; + const baseX = items.reduce((acc, i) => isNaN(i.x) ? acc : i.x, items[0].x || 0); + const baseY = Math.min.apply(null, ys); + const sorted = items.slice().sort((a,b) => { + const da = pri[a.key] ?? 4; + const db = pri[b.key] ?? 4; + if (da !== db) return da - db; + // Stable secondary by original Y to keep consistent spacing within same group + return a.y - b.y; + }); + sorted.forEach((it, idx) => { + const y = baseY + idx * step; + it.g.setAttribute('transform', `translate(${baseX},${y})`); + // Move node to reflect sorted order in DOM + try { parent.appendChild(it.g); } catch {} + }); + } + } catch {} + } + } catch (e) { + // no-op + } + } + + // Determine if a heading's section has any substantive content until the next heading of same or higher level +function isSectionEmpty(headingEl, depth) { + if (!headingEl) return true; + const stopDepth = depth; // stop at next heading with depth <= this + let node = headingEl.nextSibling; + while (node) { + if (node.nodeType === 1) { // element + const el = node; + const tag = el.tagName ? el.tagName.toLowerCase() : ''; + if (/^h[1-6]$/.test(tag)) { + const d = (function(t){ try { return parseInt(t.replace(/[^0-9]/g,'') || '0', 10); } catch { return 0; } })(tag); + if (d <= stopDepth) return true; // reached next section without content + } else { + const text = (el.textContent || '').trim(); + const isMermaid = el.classList && el.classList.contains('mermaid'); + if (isMermaid) return false; + if (el.tagName === 'UL' || el.tagName === 'OL') { + if (el.querySelector('li')) return false; + } + if (el.tagName === 'TABLE' || el.tagName === 'PRE' || el.tagName === 'BLOCKQUOTE' || el.tagName === 'P' || el.tagName === 'IMG' || el.tagName === 'FIGURE' || el.tagName === 'DETAILS' || el.tagName === 'DIV') { + if (text.length > 0) return false; + } + } + } else if (node.nodeType === 3) { // text node + if ((node.textContent || '').trim().length > 0) return false; + } + node = node.nextSibling; + } + return true; + } + + let includedSectionIds = new Set(); + let headingIndex = []; + function buildNavigation(headings) { + const items = headings + .map(h => ({ + id: h.id, + text: h.textContent || '', + depth: (function(tag){ try { return parseInt(tag.replace(/[^0-9]/g,'') || '0', 10); } catch { return 0; } })(h.tagName || ''), + })) + .filter(i => (i.text || '').trim().length > 0) + .filter(i => !isSectionEmpty(document.getElementById(i.id), i.depth)); + // Attach section content snapshot for search scoring + headingIndex = items.map(i => ({ + ...i, + content: (getSectionTextById(i.id, i.depth, 4000) || ''), + })); + // Initial render (will show full list if search is empty) + renderFilteredNavigation(sectionSearch ? sectionSearch.value : ''); + } + +function getSectionTextById(id, depth, maxChars) { + const el = document.getElementById(id); + if (!el) return ''; + const stopDepth = depth; + let text = ''; + let node = el.nextSibling; + while (node && text.length < (maxChars || 4000)) { + if (node.nodeType === 1) { + const tag = node.tagName ? node.tagName.toLowerCase() : ''; + if (/^h[1-6]$/.test(tag)) { + const d = (function(t){ try { return parseInt(t.replace(/[^0-9]/g,'') || '0', 10); } catch { return 0; } })(tag); + if (d <= stopDepth) break; + } else { + text += ' ' + (node.innerText || node.textContent || ''); + } + } else if (node.nodeType === 3) { + text += ' ' + (node.textContent || ''); + } + node = node.nextSibling; + } + return text.trim(); + } + + function updateSectionSearch() { + // Re-render the TOC according to the current query + renderFilteredNavigation(sectionSearch ? sectionSearch.value : ''); + } + + function handleSectionSearch(q) { + const query = (q || '').trim(); + if (!query) return; + const tokens = query.toLowerCase().split(/\s+/).filter(Boolean); + if (headingIndex.length === 0) return; + + let best = null; + let bestScore = -Infinity; + headingIndex.forEach((item, idx) => { + const text = (item.text || '').toLowerCase(); + const content = (item.content || '').toLowerCase(); + let score = 0; + if (text === query.toLowerCase()) score += 100; + if (text.startsWith(query.toLowerCase())) score += 50; + if (tokens.length && tokens.every(t => text.includes(t))) score += 10 * tokens.length; + // Content-based scoring + if (tokens.length && tokens.every(t => content.includes(t))) score += 6 * tokens.length; + // prefer shallower depth slightly + score += (7 - item.depth); + // slight bias to earlier items + score -= idx * 0.001; + if (score > bestScore) { bestScore = score; best = item; } + }); + if (best) { + const el = document.getElementById(best.id); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + try { history.replaceState(null, '', `#${best.id}`); } catch {} + try { sectionSearch.value = ''; } catch {} + } + } + } + + // Render the TOC list filtered by the query; empty query shows all + function renderFilteredNavigation(query) { + const q = (query || '').trim().toLowerCase(); + let items = headingIndex.slice(); + if (q) { + const tokens = q.split(/\s+/).filter(Boolean); + const scored = items.map((item, idx) => { + const text = (item.text || '').toLowerCase(); + const content = (item.content || '').toLowerCase(); + let score = 0; + if (text === q) score += 100; + if (text.startsWith(q)) score += 50; + if (tokens.length && tokens.every(t => text.includes(t))) score += 10 * tokens.length; + if (tokens.length && tokens.every(t => content.includes(t))) score += 6 * tokens.length; + score += (7 - item.depth); + score -= idx * 0.001; + return { item, score }; + }); + items = scored.filter(s => s.score > 0).sort((a,b) => b.score - a.score).map(s => s.item); + } + + tocList.innerHTML = ''; + const tocUl = document.createElement('ul'); + items.forEach(i => { + const li = document.createElement('li'); + li.className = `toc-item depth-${i.depth}`; + const a = document.createElement('a'); + a.href = `#${i.id}`; + a.textContent = i.text; + a.addEventListener('click', (e) => { + e.preventDefault(); + var target = document.getElementById(i.id); + if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.replaceState(null, '', `#${i.id}`); + }); + li.appendChild(a); + tocUl.appendChild(li); + }); + tocList.appendChild(tocUl); + includedSectionIds = new Set(items.map(i => i.id)); + initScrollSpy(); + } + + let spyObserver; + function initScrollSpy() { + if (spyObserver) spyObserver.disconnect(); + + const sections = Array.from(contentEl.querySelectorAll('h1, h2, h3, h4, h5, h6')).filter(sec => includedSectionIds.has(sec.id)); + const map = new Map(); + sections.forEach(sec => { + const id = sec.id; + if (!id) return; + map.set(id, { + toc: (function(){ + const a = tocList.querySelector('a[href="#' + CSS.escape(id) + '"]'); + return a ? a.parentElement : null; + })(), + }); + }); + + const setActive = (id) => { + tocList.querySelectorAll('.active').forEach(el => el.classList.remove('active')); + const pair = map.get(id); + if (pair && pair.toc) { + pair.toc.classList.add('active'); + const container = tocList.closest('.toc-inner') || tocList.parentElement; + scrollIntoViewIfNeeded(pair.toc, container); + } + }; + + spyObserver = new IntersectionObserver((entries) => { + let topMost = null; + entries.forEach(ent => { + if (!ent.isIntersecting) return; + if (!topMost || ent.boundingClientRect.top < topMost.boundingClientRect.top) { + topMost = ent; + } + }); + if (topMost && topMost.target && topMost.target.id) setActive(topMost.target.id); + }, { rootMargin: '-56px 0px -70% 0px', threshold: [0, 1] }); + + sections.forEach(sec => spyObserver.observe(sec)); + } + + function scrollIntoViewIfNeeded(target, container) { + if (!target || !container) return; + try { + const cRect = container.getBoundingClientRect(); + const tRect = target.getBoundingClientRect(); + const margin = 12; + const overTop = tRect.top < cRect.top + margin; + const overBottom = tRect.bottom > cRect.bottom - margin; + if (overTop) { + container.scrollTop -= (cRect.top + margin - tRect.top); + } else if (overBottom) { + container.scrollTop += (tRect.bottom - (cRect.bottom - margin)); + } + } catch (e) { /* noop */ } + } + + function basicMarkdown(md) { + const esc = (s) => s.replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); + const formatInline = (s) => { + let t = esc(s); + // 1) Inline code first to protect spans from other regexes + // Require whitespace/punctuation boundaries to avoid mid-word false positives + t = t.replace(/(^|\s)`([^`]+)`(?=\s|$|[.,;:!\?\)\]\}])/g, '$1$2'); + // 2) Links [text](url) + t = t.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1'); + // 3) Autolinks (avoid matching inside tags or code/backticks); don't eat trailing backticks or ')' + t = t.replace(/(?)/g, '$1'); + // 4) Strikethrough ~~text~~ + t = t.replace(/~~([^~]+)~~/g, '$1'); + // 5) Bold **text** + t = t.replace(/\*\*([^*]+)\*\*/g, '$1<\/strong>'); + // 6) Italics *text* + t = t.replace(/(^|\W)\*([^*]+)\*(?=\W|$)/g, '$1$2<\/em>'); + return t; + }; + const lines = md.split(/\r?\n/); + let html = ''; + let inCode = false; + let codeLang = ''; + let fenceType = '```'; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const fence = line.match(/^\s*(```|~~~)\s*(.*)$/); + if (fence) { + if (!inCode) { + fenceType = fence[1]; + const info = (fence[2] || '').trim(); + codeLang = (info.split(/\s+/)[0] || '').toLowerCase(); + const safeLang = codeLang.replace(/[^a-z0-9_+\-]/g, ''); + if (codeLang === 'mermaid') { + html += '
'; + } else { + const cls = safeLang ? ` class="language-${safeLang}"` : ''; + html += `
`;
+          }
+          inCode = true;
+        } else {
+          if (codeLang === 'mermaid') {
+            html += '
'; + } else { + html += ''; + } + inCode = false; + codeLang = ''; + } + continue; + } + if (inCode) { html += esc(line) + '\n'; continue; } + + // Setext headings + if (i + 1 < lines.length && (/^===+\s*$/.test(lines[i + 1]) || /^---+\s*$/.test(lines[i + 1]))) { + const lvl = /^===+\s*$/.test(lines[i + 1]) ? 1 : 2; + html += `${esc(line.trim())}`; + i += 1; // skip underline + continue; + } + + if (/^#\s+/.test(line)) { html += `

${formatInline(line.replace(/^#\s+/, ''))}

`; continue; } + if (/^##\s+/.test(line)) { html += `

${formatInline(line.replace(/^##\s+/, ''))}

`; continue; } + if (/^###\s+/.test(line)) { html += `

${formatInline(line.replace(/^###\s+/, ''))}

`; continue; } + if (/^####\s+/.test(line)) { html += `

${formatInline(line.replace(/^####\s+/, ''))}

`; continue; } + if (/^#####\s+/.test(line)) { html += `
${formatInline(line.replace(/^#####\s+/, ''))}
`; continue; } + if (/^######\s+/.test(line)) { html += `
${formatInline(line.replace(/^######\s+/, ''))}
`; continue; } + if (/^>\s?/.test(line)) { html += `

${formatInline(line.replace(/^>\s?/, ''))}

`; continue; } + if (line.trim() === '') { html += ''; continue; } + + // Simple tables (GFM) + if (line.includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?[-\s]+:?\s*\|/.test(lines[i + 1])) { + // collect block until a non-row line + const header = line.trim(); + const divider = lines[i + 1].trim(); + i += 2; + const rows = []; + while (i < lines.length && lines[i].includes('|') && !/^\s*$/.test(lines[i])) { + rows.push(lines[i].trim()); + i++; + } + i--; // step back one because loop will i++ + const cells = (row) => row.replace(/^\|?|\|?$/g, '').split('|').map(s => s.trim()); + const ths = cells(header).map(h => `${formatInline(h)}`).join(''); + const tds = rows.map(r => `${cells(r).map(c => `${formatInline(c)}`).join('')}`).join(''); + html += `${ths}${tds}
`; + continue; + } + + // Lists (unordered/ordered + task lists) + const ulMatch = line.match(/^\s*([*+-])\s+(.+)/); + const olMatch = line.match(/^\s*(\d+)\.\s+(.+)/); + if (ulMatch || olMatch) { + const isOrdered = !!olMatch; + const tag = isOrdered ? 'ol' : 'ul'; + const items = []; + let isTaskList = false; + // collect contiguous list lines + while (i < lines.length) { + const l = lines[i]; + const m = isOrdered ? l.match(/^\s*\d+\.\s+(.+)/) : l.match(/^\s*([*+-])\s+(.+)/); + if (!m) break; + let text = isOrdered ? m[1] : m[2]; + const task = text.match(/^\[( |x|X)\]\s+(.+)/); + if (task) { + isTaskList = true; + const checked = /x/i.test(task[1]); + items.push({ task: true, checked, text: task[2] }); + } else { + items.push({ task: false, text }); + } + i++; + } + i--; // for loop increment + if (isTaskList && !isOrdered) { + html += ``; + } else { + html += `<${tag}>`; + items.forEach(it => { + html += `
  • ${formatInline(it.text)}
  • `; + }); + html += ``; + } + continue; + } + + // Paragraph with inline formatting + html += `

    ${formatInline(line)}

    `; + } + return html; + } + + (async function bootstrap() { + // Initialize theme before rendering anything + try { initTheme(); } catch {} + // Initialize sidebar collapsed state + try { + const saved = localStorage.getItem('sidebar:collapsed'); + if (saved === '1') setSidebarCollapsed(true); else setSidebarCollapsed(false); + } catch { setSidebarCollapsed(false); } + // If ?file= is present, try to load from sessionStorage first, else fetch relative URL + const urlObj = new URL(location.href); + // Fullscreen viewer mode + const full = urlObj.searchParams.get('full'); + if (full && /^(1|true)$/i.test(full)) { + document.body.classList.add('fullviewer'); + } + const fileParam = urlObj.searchParams.get('file'); + const remoteParam = urlObj.searchParams.get('remote'); + if (remoteParam) { + try { + const resp = await fetch('/api/file?path=' + encodeURIComponent(remoteParam)); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const txt = await resp.text(); + renderMarkdown(txt); + sitePath.textContent = `/ Remote / ${remoteParam}`; + return; + } catch (e) {} + } + if (fileParam) { + const fileName = decodeURIComponent(fileParam); + try { + const cached = sessionStorage.getItem('doc:' + fileName); + if (cached) { + renderMarkdown(cached); + sitePath.textContent = `/ Docs / ${fileName}`; + return; + } + } catch {} + const loaded = await fetchAndRender(fileName, null); + if (loaded) return; + } + + // Prefer pre-bundled JS payload if available (works over file:// without fetch) + if (typeof window.REPORT_MD === 'string' && window.REPORT_MD.length) { + renderMarkdown(window.REPORT_MD); + sitePath.textContent = '/ Report / report.md'; + return; + } + // Try to auto-load report.md if present via fetch (served over http) + const loadedReport = await fetchAndRender('report.md', null); + if (loadedReport) return; + + // Fallback demo content from prompt.txt + try { + const demo = await fetch('prompt.txt').then(r => r.text()); + const md = `# Markdown Viewer\n\nUpload a Markdown file (drag & drop or use Open) to see it rendered with navigation and a table of contents.\n\n> Note: This page uses your local file only; nothing is uploaded.\n\n## Getting started\n\n- Click Open or drag a .md file into the window.\n- Navigate using the left sidebar or right outline.\n\n## Features\n\n${'```'}js\nconsole.log('Code blocks support copy button and syntax highlighting');\n${'```'}\n\n${'```'}mermaid\nflowchart LR\n A[Drag file] --> B{Parse Markdown}\n B -->|Build| C[Nav/ToC]\n B -->|Render| D[Content]\n C --> E[Scroll Spy]\n${'```'}\n\n| Feature | Status |\n|---|---|\n| Drag & Drop | ✅ |\n| Scroll Spy | ✅ |\n| Dark Mode | ✅ |\n\n> Sources: \n> styles.css\n> script.js\n> index.html\n`; + renderMarkdown(md); + } catch { + renderMarkdown('# Markdown Viewer\n\nOpen a local Markdown file to begin.'); + } + })(); + + window.addEventListener('hashchange', () => { + const id = decodeURIComponent(location.hash.replace('#', '')); + const el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + + // Auto-refresh viewer if a job updates the report in another tab + window.addEventListener('storage', (e) => { + if (e.key === 'report:updated') { + // Re-fetch report.md and render + fetchAndRender('report.md', null); + } + }); + + // Floating ChatGPT widget behavior + (function initChatWidget(){ + const toggle = document.getElementById('chatToggle'); + const panel = document.getElementById('chatPanel'); + const close = document.getElementById('chatClose'); + const copyBtn = document.getElementById('copyPromptBtn'); + const openBtn = document.getElementById('openChatBtn'); + const input = document.getElementById('chatInput'); + const modelSel = document.getElementById('chatModel'); + const includeCbx = document.getElementById('includeContext'); + const status = document.getElementById('chatStatus'); + if (!toggle || !panel) return; + + function setOpen(open){ + panel.hidden = !open; + toggle.setAttribute('aria-expanded', String(open)); + if (open) { + // Prefill prompt with current section context if empty + if (input && !input.value.trim()) { + const ctx = currentSectionContext(); + input.value = ctx ? ctx + '\n\n' : ''; + if (includeCbx) includeCbx.checked = false; // avoid duplicating context + } + if (input) { + try { input.focus(); input.setSelectionRange(input.value.length, input.value.length); } catch {} + } + } + } + toggle.addEventListener('click', (e)=> { e.stopPropagation(); setOpen(panel.hidden); }); + close && close.addEventListener('click', ()=> setOpen(false)); + + // Close on outside click / ESC + document.addEventListener('click', (e)=>{ + if (panel.hidden) return; + if (!panel.contains(e.target) && !toggle.contains(e.target)) setOpen(false); + }); + document.addEventListener('keydown', (e)=>{ + if (e.key === 'Escape') setOpen(false); + }); + + function currentSectionContext(maxChars=2000){ + // find active heading (h2/h3 preferred, else h1) + const active = (function(){ + const a = document.querySelector('#tocList .active a'); + if (a) return document.getElementById(decodeURIComponent(a.getAttribute('href').slice(1))); + return contentEl.querySelector('h2, h3, h1'); + })(); + if (!active) return ''; + let ctx = []; + let n = active.nextSibling; + while (n && ctx.join('\n').length < maxChars){ + if (n.nodeType === 1){ + const tag = n.tagName.toLowerCase(); + if (/^h[1-6]$/.test(tag)) break; + ctx.push(n.innerText || ''); + } else if (n.nodeType === 3) { + ctx.push(n.textContent || ''); + } + n = n.nextSibling; + } + const heading = (active.textContent || '').trim(); + return `Context: ${heading}\n\n` + ctx.join('\n').trim().slice(0, maxChars); + } + + function buildPrompt(){ + const q = (input && input.value.trim()) || ''; + const model = (modelSel && modelSel.value) || 'gpt-4o'; + const include = includeCbx && includeCbx.checked; + const parts = []; + parts.push(`Question: ${q}`); + parts.push(`Model: ${model}`); + parts.push(`Source: ${location.href}`); + if (include) parts.push(currentSectionContext()); + return parts.join('\n\n'); + } + + copyBtn && copyBtn.addEventListener('click', async ()=>{ + const prompt = buildPrompt(); + try { + await navigator.clipboard.writeText(prompt); + status.textContent = 'Prompt copied to clipboard'; + setTimeout(()=> status.textContent = '', 1500); + } catch(e) { + alert(prompt); + } + }); + + openBtn && openBtn.addEventListener('click', async ()=>{ + const model = (modelSel && modelSel.value) || 'gpt-4o'; + const url = `https://chat.openai.com/?model=${encodeURIComponent(model)}`; + const prompt = buildPrompt(); + try { await navigator.clipboard.writeText(prompt); } catch {} + window.open(url, '_blank', 'noopener'); + }); + })(); +})(); diff --git a/codex-rs/tui/src/security_report_assets/styles.css b/codex-rs/tui/src/security_report_assets/styles.css index 180b527fd4..d7db8d761c 100644 --- a/codex-rs/tui/src/security_report_assets/styles.css +++ b/codex-rs/tui/src/security_report_assets/styles.css @@ -1 +1,439 @@ -body { font-family: sans-serif; margin: 0; padding: 1rem; } +:root { + --bg: #ffffff; + --bg-elev: #fafafa; + --bg-muted: #f5f5f7; + --text: #1f2937; + --text-muted: #4b5563; + --border: #e5e7eb; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --shadow: 0 4px 10px rgba(0, 0, 0, 0.06); + --inline-code-bg: rgba(2, 6, 23, 0.06); + --inline-code-color: #d73a49; /* GitHub-like reddish for inline code (light) */ + /* Scrollbar */ + --scroll-thumb: rgba(100, 116, 139, 0.45); + --scroll-thumb-hover: rgba(100, 116, 139, 0.7); + --scroll-track: transparent; + /* Severity palette (light) */ + --sev-high: #dc2626; /* red-600 */ + --sev-high-bg: #fee2e2; /* red-100 */ + --sev-med: #eab308; /* yellow-500 */ + --sev-med-bg: #fef9c3; /* yellow-100 */ + --sev-low: #059669; /* emerald-600 */ + --sev-low-bg: #d1fae5; /* emerald-100 */ + --sev-ign: #64748b; /* slate-500 */ + --sev-ign-bg: #e5e7eb; /* gray-200 */ +} + +[data-theme="dark"] { + --bg: #0b0f14; + --bg-elev: #0f141b; + --bg-muted: #121923; + --text: #e5e7eb; + --text-muted: #cbd5e1; + --border: #202a36; + --accent: #3b82f6; + --accent-hover: #60a5fa; + --shadow: 0 4px 10px rgba(0, 0, 0, 0.35); + --inline-code-bg: rgba(148, 163, 184, 0.18); + --inline-code-color: #ffa657; /* GitHub dark theme orange for inline code */ + /* Scrollbar */ + --scroll-thumb: rgba(148, 163, 184, 0.4); + --scroll-thumb-hover: rgba(148, 163, 184, 0.65); + --scroll-track: transparent; + /* Severity palette (dark) */ + --sev-high: #f87171; /* red-400 */ + --sev-high-bg: rgba(248, 113, 113, 0.12); + --sev-med: #facc15; /* yellow-400 */ + --sev-med-bg: rgba(250, 204, 21, 0.12); + --sev-low: #34d399; /* emerald-400 */ + --sev-low-bg: rgba(52, 211, 153, 0.12); + --sev-ign: #94a3b8; /* slate-400 */ + --sev-ign-bg: rgba(148, 163, 184, 0.12); +} + +* { box-sizing: border-box; } +html, body { height: 100%; } +body { + margin: 0; + color: var(--text); + background: var(--bg); + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Noto Sans, "Apple Color Emoji", "Segoe UI Emoji"; + line-height: 1.6; +} + +/* Minimal, pretty scrollbars (Firefox + WebKit) */ +html { + scrollbar-width: thin; /* Firefox */ + scrollbar-color: var(--scroll-thumb) var(--scroll-track); +} +.toc-inner, pre, .chat-panel, .job-progress .jp-logs, .content, body { + scrollbar-width: thin; /* Firefox for scrollable containers */ + scrollbar-color: var(--scroll-thumb) var(--scroll-track); +} +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} +*::-webkit-scrollbar-track { + background: var(--scroll-track); +} +*::-webkit-scrollbar-thumb { + background-color: var(--scroll-thumb); + border-radius: 999px; + border: 2px solid transparent; + background-clip: padding-box; +} +*::-webkit-scrollbar-thumb:hover { + background-color: var(--scroll-thumb-hover); +} + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* Global nav links */ +.nav-links { display:flex; gap:10px; } +.nav-links a { padding:6px 10px; border-radius:8px; color:var(--text); border:1px solid var(--border); background:var(--bg); } +.nav-links a.active, .nav-links a:hover { background: var(--bg-elev); text-decoration: none; } + +.topbar { + position: sticky; + top: 0; + z-index: 30; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid var(--border); + background: var(--bg-elev); + backdrop-filter: saturate(180%) blur(8px); +} +.brand { display: flex; align-items: center; gap: 10px; } +.site-path { font-weight: 600; color: var(--text); } + +.nav-toggle { + display: inline-flex; + border: 1px solid var(--border); + background: transparent; + border-radius: 8px; + padding: 4px 6px; + align-items: center; + justify-content: center; +} + +.top-actions { display: flex; align-items: center; gap: 6px; } + +.search-input { + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); + padding: 8px 10px; + border-radius: 10px; + outline: none; + width: min(260px, 40vw); +} +.search-input:focus { box-shadow: var(--shadow); } + +/* Sidebar placement of the search */ +.toc-inner .toc-search { padding: 6px 2px 8px; } +.toc-inner .search-input { + width: 100%; +} + +.btn { + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); + padding: 6px 10px; + border-radius: 8px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + line-height: 1.2; + font: inherit; + margin: 0; +} +.btn:hover { box-shadow: var(--shadow); } +.btn.primary { + background: var(--accent); + color: white; + border-color: transparent; +} +.btn.primary:hover { background: var(--accent-hover); } +.icon-btn { border: none; background: transparent; color: var(--text); cursor: pointer; border-radius: 8px; padding: 4px; } +.icon-btn:hover { background: var(--bg-muted); } +.btn.small { padding: 3px 8px; font-size: 12px; border-radius: 8px; } + +.layout { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 0; + width: 100%; + max-width: 100%; + margin: 0; +} + +/* Collapsible left sidebar: keep a thin rail with the toggle */ +body.sidebar-collapsed .layout { grid-template-columns: 44px minmax(0, 1fr); } +body.sidebar-collapsed .sidebar { display: block; } +body.sidebar-collapsed .toc-inner { + padding: 8px 6px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} +body.sidebar-collapsed .toc-inner .nav-title { width: 100%; display: flex; align-items: center; justify-content: center !important; } +body.sidebar-collapsed .toc-inner .nav-title span { display: none; } +body.sidebar-collapsed .toc-inner .toc-search, +body.sidebar-collapsed .toc-inner #jobProgressHost, +body.sidebar-collapsed .toc-inner #tocList { display: none; } +body.sidebar-collapsed .nav-toggle { padding: 6px; } + +.sidebar { + min-height: calc(100vh - 56px); + position: relative; +} +.sidebar.right { border-right: 1px solid var(--border); border-left: none; grid-column: 1; grid-row: 1; } + +.nav-inner, .toc-inner { + position: sticky; + top: 56px; + padding: 10px 8px; + height: calc(100vh - 56px); + overflow: auto; + background: var(--bg-muted); +} + +.nav-title { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + margin-bottom: 6px; +} + +.content { + padding: 24px 40px; + background: var(--bg); + grid-column: 2; + grid-row: 1; +} +.content.editing { outline: 2px dashed var(--border); outline-offset: -2px; background: var(--bg-elev); } +.content > *:first-child { margin-top: 0; } + +.content h1 { font-size: 2.2rem; margin: 0.6em 0 0.4em; line-height: 1.2; } +.content h2 { font-size: 1.6rem; margin: 1.6em 0 0.5em; border-top: 1px solid var(--border); padding-top: 1em; } +.content h3 { font-size: 1.25rem; margin: 1.2em 0 0.4em; } +.content h4 { font-size: 1.05rem; margin: 1em 0 0.3em; } +.content h5 { font-size: 0.95rem; margin: 0.9em 0 0.25em; } +.content h6 { font-size: 0.9rem; margin: 0.8em 0 0.2em; } +.content p { color: var(--text); margin: 0.7em 0; } +.content :not(pre) > code { background: transparent; color: inherit; padding: 0.15em 0.35em; border-radius: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } +/* Highlight only explicit inline code marks */ +.content code.inline-code { background: var(--inline-code-bg); } +/* Only color inline code in typical body contexts (not headings or bold text) */ +/* Inline code color applied only when JS marks it explicitly */ +.content code.inline-code { color: var(--inline-code-color); } +.content code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } +.content pre { background: var(--bg-muted); border: 1px solid var(--border); border-radius: 10px; padding: 14px; overflow: auto; position: relative; } +.content pre code { background: transparent; padding: 0; } +.content .mermaid { display: block; margin: 1em auto; background: var(--bg-elev); border: 1px solid var(--border); border-radius: 10px; padding: 12px; } +.content ul, .content ol { padding-left: 1.2em; } +.content ul.task-list { list-style: none; padding-left: 0; } +.content .task-list-item { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; } +.content .task-list-item input[type="checkbox"] { margin-top: 3px; } +.content del { opacity: 0.7; } +.copy-btn { + position: absolute; top: 8px; right: 8px; + font-size: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--text); + border-radius: 8px; padding: 4px 8px; cursor: pointer; +} +.copy-btn:hover { box-shadow: var(--shadow); } + +/* Bug ticket widget */ +.ticket-box { + margin: 12px 0 8px; + padding: 10px 12px; + border: 1px solid var(--border); + background: var(--bg-elev); + border-radius: 10px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px 12px; +} +.ticket-box label { font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; } +.ticket-box input[type="text"], .ticket-box select { + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); + border-radius: 8px; + padding: 6px 8px; +} +.ticket-box .ticket-btn { border: 1px solid transparent; background: var(--accent); color: #fff; border-radius: 8px; padding: 8px 10px; cursor: pointer; } +.ticket-box .ticket-btn:hover { background: var(--accent-hover); } +.ticket-box .ticket-status { font-size: 12px; color: var(--text-muted); } + +/* (Reverted) per-bug floating FAB styles removed */ + +/* Severity badges */ +.badge { + display: inline-block; + font-size: 12px; + line-height: 1; + padding: 4px 8px; + border-radius: 999px; + font-weight: 600; + border: 1px solid transparent; +} +.sev-high { color: var(--sev-high); background: var(--sev-high-bg); border-color: transparent; } +.sev-med { color: var(--sev-med); background: var(--sev-med-bg); border-color: transparent; } +.sev-low { color: var(--sev-low); background: var(--sev-low-bg); border-color: transparent; } +.sev-ign { color: var(--sev-ign); background: var(--sev-ign-bg); border-color: transparent; } + +#navList, #tocList { list-style: none; padding: 0; margin: 0; } +#tocList ul { list-style: none; padding-left: 0; margin: 0; } +#navList a, #tocList a { + display: block; padding: 4px 6px; border-radius: 6px; color: var(--text); +} +#navList a:hover, #tocList a:hover { background: var(--bg-elev); text-decoration: none; } +.nav-item { font-size: 14px; } +.nav-item.depth-2 { padding-left: 12px; } +.nav-item.depth-3 { padding-left: 24px; } +.nav-item.depth-4 { padding-left: 36px; } +.nav-item.depth-5 { padding-left: 48px; } +.nav-item.depth-6 { padding-left: 60px; } +.nav-item.active > a, .toc-item.active > a { font-weight: 700; color: var(--accent); } +.nav-item.active > a { border-left: 3px solid var(--accent); margin-left: -3px; } + +/* Right TOC enhanced selection visuals */ +.toc-item > a { position: relative; transition: background 160ms ease, color 160ms ease, padding-left 160ms ease; font-size: 13px; line-height: 1.3; } +.toc-item.depth-1 > a { font-weight: 700; } +.toc-item.depth-2 { padding-left: 14px; } +.toc-item.depth-3 { padding-left: 28px; } +.toc-item.depth-4 { padding-left: 42px; } +.toc-item.depth-5 { padding-left: 56px; } +.toc-item.depth-6 { padding-left: 70px; } +.toc-item.active > a { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, transparent); + padding-left: 10px; +} +.toc-item.active > a::before { + content: ''; + position: absolute; + left: 0; + top: 4px; + bottom: 4px; + width: 3px; + border-radius: 2px; + background: var(--accent); +} +.toc-item > a:focus-visible { + outline: 2px solid color-mix(in srgb, var(--accent) 50%, transparent); + outline-offset: 2px; +} + +.content table { width: 100%; border-collapse: collapse; margin: 1em 0; } +.content th, .content td { border: 1px solid var(--border); padding: 8px 10px; } +.content thead th { background: var(--bg-muted); font-weight: 700; } +.content tbody tr:nth-child(even) { background: var(--bg-elev); } + +.content blockquote { border-left: 4px solid var(--border); margin: 1em 0; padding: 0.6em 1em; background: var(--bg-elev); border-radius: 6px; } +.callout { border: 1px solid var(--border); border-left: 4px solid var(--accent); background: var(--bg-elev); border-radius: 10px; overflow: hidden; } +.callout .callout-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; user-select: none; font-weight: 600; } +.callout .callout-body { padding: 0 12px 12px; display: none; } +.callout.open .callout-body { display: block; animation: fadeIn 160ms ease-in; } +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + +.footer { border-top: 1px solid var(--border); color: var(--text-muted); font-size: 14px; padding: 16px; background: var(--bg-elev); } + +.drop-overlay { position: fixed; inset: 0; background: rgba(37, 99, 235, 0.08); border: 2px dashed var(--accent); display: none; align-items: center; justify-content: center; z-index: 50; } +.drop-overlay .drop-message { background: var(--bg); color: var(--text); padding: 16px 22px; border-radius: 12px; border: 1px dashed var(--accent); box-shadow: var(--shadow); } +.drop-overlay.show { display: flex; } + +@media (max-width: 1100px) { + .layout { grid-template-columns: 220px minmax(0, 1fr); } + .sidebar.right { display: block; } + .content { grid-column: 2; } +} + +@media (max-width: 800px) { + .layout { grid-template-columns: 200px minmax(0, 1fr); } + .sidebar { display: block; } + .content { padding: 14px; grid-column: 2; } +} + +/* Fullscreen viewer mode */ +body.fullviewer .topbar { display: none; } +body.fullviewer .layout { grid-template-columns: minmax(0, 1fr) !important; } +body.fullviewer .sidebar { display: none !important; } +body.fullviewer .content { grid-column: 1; padding: 20px 28px; } + +/* Floating ChatGPT widget */ +.chat-widget { position: fixed; left: 50%; transform: translateX(-50%); bottom: 20px; z-index: 70; } +.chat-toggle { + border: 1px solid transparent; + background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 75%, #fff)); + color: #fff; border-radius: 999px; padding: 12px 18px; cursor: pointer; box-shadow: var(--shadow); + font-weight: 700; letter-spacing: .02em; + transition: transform .12s ease, box-shadow .12s ease, filter .2s ease; +} +.chat-toggle:hover { transform: translateY(-1px); filter: brightness(1.04); } +.chat-panel { + position: absolute; left: 50%; transform: translate(-50%, -12px); + bottom: 52px; width: min(680px, 94vw); + background: color-mix(in srgb, var(--bg-elev) 85%, transparent); + color: var(--text); + border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); + padding: 12px; display: grid; gap: 10px; grid-template-rows: auto auto 1fr auto auto; + backdrop-filter: blur(6px) saturate(120%); +} +.chat-panel[hidden] { display: none !important; } +.chat-panel:not([hidden]) { animation: popIn 140ms ease-out; } +@keyframes popIn { from { opacity: 0; transform: translate(-50%, 0) scale(.98); } to { opacity: 1; transform: translate(-50%, -12px) scale(1); } } +.chat-header { display: flex; align-items: center; justify-content: space-between; padding: 4px 2px; } +.chat-title { font-weight: 800; font-size: 14px; color: var(--text); letter-spacing: 0.02em; } +.chat-close { border: none; background: transparent; color: var(--text); font-size: 18px; cursor: pointer; } +.chat-controls { display: flex; gap: 10px; align-items: center; } +.chat-controls label { display: flex; gap: 8px; align-items: center; font-size: 12px; color: var(--text-muted); } +#chatModel { border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 10px; padding: 6px 10px; } +#chatInput { width: 100%; min-height: 90px; border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 12px; padding: 10px 12px; resize: vertical; font-family: inherit; box-shadow: inset 0 1px 0 rgba(0,0,0,.02); } +.chat-actions { display: flex; align-items: center; gap: 8px; } +.chat-actions .spacer { flex: 1 1 auto; } +.include-context { font-size: 12px; color: var(--text-muted); } +.chat-status { font-size: 12px; color: var(--text-muted); min-height: 1em; } + +/* Sidebar job progress card */ +.job-progress { border: 1px solid var(--border); background: var(--bg-elev); border-radius: 10px; padding: 10px; margin: 6px 0 10px; } +.job-progress .jp-title { display:flex; align-items:center; justify-content:space-between; gap:8px; font-weight:700; font-size:13px; } +.job-progress .jp-status { font-size: 12px; color: var(--text-muted); margin-top: 4px; } +.job-progress .jp-bar { height: 6px; background: var(--bg); border:1px solid var(--border); border-radius: 999px; overflow:hidden; margin-top: 8px; } +.job-progress .jp-bar > span { display:block; height:100%; width:0%; background: var(--accent); transition: width .2s ease; } +.job-progress .jp-logs { font-size: 12px; max-height: 120px; overflow:auto; margin-top:8px; } + +/* Modal dialog */ +.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.35); backdrop-filter: blur(2px); display: none; align-items: center; justify-content: center; z-index: 80; } +.modal-overlay.show { display: flex; } +.modal { width: min(700px, 94vw); background: var(--bg-elev); color: var(--text); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); padding: 12px; } +.modal-header { display:flex; align-items:center; justify-content:space-between; padding: 6px 4px 10px; } +.modal-title { font-weight: 800; letter-spacing: .02em; } +.modal-body { display:grid; gap: 10px; } +.modal-row { display:flex; gap: 10px; align-items:center; } +.modal-row label { min-width: 160px; font-size: 12px; color: var(--text-muted); } +.modal-row input, .modal-row select { flex: 1; border:1px solid var(--border); background:var(--bg); color:var(--text); border-radius:10px; padding:8px 10px; } +.modal-actions { display:flex; gap:8px; justify-content:flex-end; margin-top: 12px; } + +/* highlight.js GitHub theme */ +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0} \ No newline at end of file diff --git a/codex-rs/tui/src/security_report_viewer.rs b/codex-rs/tui/src/security_report_viewer.rs index c1c4ba0fcc..addd0c353d 100644 --- a/codex-rs/tui/src/security_report_viewer.rs +++ b/codex-rs/tui/src/security_report_viewer.rs @@ -1,5 +1,8 @@ const REPORT_STYLES: &str = include_str!("security_report_assets/styles.css"); const REPORT_SCRIPT: &str = include_str!("security_report_assets/script.js"); +const MARKED_JS: &str = include_str!("security_report_assets/marked.min.js"); +const HIGHLIGHT_JS: &str = include_str!("security_report_assets/highlight.min.js"); +const MERMAID_JS: &str = include_str!("security_report_assets/mermaid.min.js"); fn escape_html(input: &str) -> String { let mut out = String::with_capacity(input.len()); @@ -81,9 +84,9 @@ pub(crate) fn build_report_html(title: &str, markdown: &str) -> String { - - - + + + diff --git a/codex-rs/tui/src/security_review.rs b/codex-rs/tui/src/security_review.rs index 48cb71cb2e..121b797902 100644 --- a/codex-rs/tui/src/security_review.rs +++ b/codex-rs/tui/src/security_review.rs @@ -89,6 +89,8 @@ Examples: - Static analysis high alert that only touches dead code ⇒ risk_score 10, severity "Informational", reason "dead code path". - High-severity SQL injection finding that uses fully parameterized queries ⇒ risk_score 20, severity "Low", reason "parameterized queries". - SSRF flagged as critical but the target requires internal metadata access tokens ⇒ risk_score 24, severity "Low", reason "internal metadata token". +- Critical-looking command injection in an internal-only CLI guarded by SSO and audited logging ⇒ risk_score 22, severity "Low", reason "internal CLI". +- Reported secret leak found in sample dev config with rotate-on-startup hook ⇒ risk_score 12, severity "Informational", reason "sample config only". Instructions: - Output severity **only** from ["High","Medium","Low","Informational"]. Map "critical"/"p0" to "High". @@ -96,6 +98,7 @@ Instructions: - Review the repository summary, spec excerpt, blame metadata, and file locations before requesting anything new; reuse existing specs or context attachments when possible. - If you still lack certainty, request concrete follow-up (e.g., repo_search, read_file, git blame) in the reason and cite the spec section you need. - Reference concrete evidence (spec section, tool name, log line) in the reason when you confirm mitigations or reclassify a finding. +- Prefer reusing existing tool outputs and cached specs before launching new expensive calls; only request fresh tooling when the supplied artifacts truly lack the needed context. - Down-rank issues when mitigations or limited blast radius materially reduce customer risk, even if the initial triage labeled them "High". - Upgrade issues when exploitability or exposure was understated, or when multiple components amplify the blast radius. - Respond with one JSON object per finding, **in the same order**, formatted exactly as: @@ -778,67 +781,15 @@ pub(crate) async fn run_security_review( selection_summaries.push((display_path, reason)); } + include_paths = resolved_paths; if let Some(tx) = request.progress_sender.as_ref() { - let (confirm_tx, confirm_rx) = oneshot::channel(); - let selections_for_ui: Vec = - selection_summaries - .iter() - .map(|(path, reason)| SecurityReviewAutoScopeSelection { - display_path: path.clone(), - reason: reason.clone(), - }) - .collect(); - tx.send(AppEvent::SecurityReviewAutoScopeConfirm { - mode: request.mode, - prompt: prompt.to_string(), - selections: selections_for_ui, - responder: confirm_tx, + let display_paths: Vec = selection_summaries + .iter() + .map(|(path, _)| path.clone()) + .collect(); + tx.send(AppEvent::SecurityReviewScopeResolved { + paths: display_paths, }); - - record( - "Waiting for user confirmation of auto-detected scope...".to_string(), - ); - - match confirm_rx.await { - Ok(true) => { - record("Auto scope confirmed by user.".to_string()); - include_paths = resolved_paths; - let display_paths: Vec = selection_summaries - .iter() - .map(|(path, _)| path.clone()) - .collect(); - tx.send(AppEvent::SecurityReviewScopeResolved { - paths: display_paths, - }); - } - Ok(false) => { - record( - "Auto scope selection rejected by user; cancelling review." - .to_string(), - ); - tx.send(AppEvent::OpenSecurityReviewPathPrompt(request.mode)); - return Err(SecurityReviewFailure { - message: - "Security review cancelled after auto scope rejection." - .to_string(), - logs, - }); - } - Err(_) => { - record( - "Auto scope confirmation interrupted; cancelling review." - .to_string(), - ); - return Err(SecurityReviewFailure { - message: - "Auto scope confirmation interrupted; review cancelled." - .to_string(), - logs, - }); - } - } - } else { - include_paths = resolved_paths; } } }