Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Failing after 2m26s
CI/CD / test-sqlite (pull_request) Successful in 2m41s
CI/CD / build-and-deploy (pull_request) Has been skipped
Stop auto-seeding 6 chat-centric extensions on startup. Move them to standard packages with js/ layout and "requires": ["chat"] metadata. Delete SeedBuiltinPackages, seed tests, Dockerfile COPY, and extensions/builtin/ directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
6.8 KiB
JavaScript
167 lines
6.8 KiB
JavaScript
// ==========================================
|
|
// Diff Viewer — Browser Extension
|
|
// ==========================================
|
|
// Renders ```diff code blocks with syntax-highlighted
|
|
// additions, deletions, hunk headers, and context lines.
|
|
// No external dependencies.
|
|
// ==========================================
|
|
|
|
Extensions.register({
|
|
id: 'diff-viewer',
|
|
|
|
async init(ctx) {
|
|
const self = this;
|
|
|
|
// ── Inject styles ──
|
|
this._injectStyles();
|
|
|
|
ctx.renderers.register('diff-block', {
|
|
type: 'block',
|
|
priority: 10,
|
|
pattern: 'diff',
|
|
render(lang, code, container) {
|
|
const lines = code.split('\n');
|
|
let stats = { added: 0, removed: 0 };
|
|
let currentFile = '';
|
|
|
|
const rendered = lines.map(line => {
|
|
const escaped = self._escapeHtml(line);
|
|
|
|
// File headers
|
|
if (line.startsWith('--- ') || line.startsWith('+++ ')) {
|
|
if (line.startsWith('+++ ')) {
|
|
currentFile = line.slice(4).trim();
|
|
}
|
|
return `<div class="diff-line diff-file-header">${escaped}</div>`;
|
|
}
|
|
|
|
// Hunk headers
|
|
if (line.startsWith('@@')) {
|
|
const hunkMatch = line.match(/^@@\s*-(\d+)(?:,\d+)?\s*\+(\d+)(?:,\d+)?\s*@@\s*(.*)/);
|
|
const context = hunkMatch ? hunkMatch[3] : '';
|
|
return `<div class="diff-line diff-hunk">${escaped}</div>`;
|
|
}
|
|
|
|
// Additions
|
|
if (line.startsWith('+')) {
|
|
stats.added++;
|
|
return `<div class="diff-line diff-add"><span class="diff-indicator">+</span>${self._escapeHtml(line.slice(1))}</div>`;
|
|
}
|
|
|
|
// Deletions
|
|
if (line.startsWith('-')) {
|
|
stats.removed++;
|
|
return `<div class="diff-line diff-del"><span class="diff-indicator">-</span>${self._escapeHtml(line.slice(1))}</div>`;
|
|
}
|
|
|
|
// Context (lines starting with space or no prefix)
|
|
if (line.startsWith(' ')) {
|
|
return `<div class="diff-line diff-ctx"><span class="diff-indicator"> </span>${self._escapeHtml(line.slice(1))}</div>`;
|
|
}
|
|
|
|
// Other (commit info, etc)
|
|
return `<div class="diff-line diff-meta">${escaped}</div>`;
|
|
}).join('');
|
|
|
|
const fileLabel = currentFile ? `<span class="diff-filename" title="${self._escapeHtml(currentFile)}">${self._escapeHtml(currentFile)}</span>` : '';
|
|
|
|
container.innerHTML = `
|
|
<div class="diff-block ext-rendered">
|
|
<div class="diff-toolbar">
|
|
${fileLabel}
|
|
<span class="diff-stats">
|
|
<span class="diff-stat-add">+${stats.added}</span>
|
|
<span class="diff-stat-del">-${stats.removed}</span>
|
|
</span>
|
|
<button class="diff-copy-btn" title="Copy diff">📋 Copy</button>
|
|
</div>
|
|
<div class="diff-content">${rendered}</div>
|
|
<details class="diff-source-toggle">
|
|
<summary>📝 View raw</summary>
|
|
<pre><code>${self._escapeHtml(code)}</code></pre>
|
|
</details>
|
|
</div>
|
|
`;
|
|
|
|
// Copy handler
|
|
const copyBtn = container.querySelector('.diff-copy-btn');
|
|
if (copyBtn) {
|
|
copyBtn.addEventListener('click', () => {
|
|
navigator.clipboard.writeText(code).then(() => {
|
|
copyBtn.textContent = '✓ Copied';
|
|
setTimeout(() => { copyBtn.textContent = '📋 Copy'; }, 1500);
|
|
}).catch(() => {});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
_escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
},
|
|
|
|
_injectStyles() {
|
|
if (document.getElementById('ext-style-diff-viewer')) return;
|
|
const style = document.createElement('style');
|
|
style.id = 'ext-style-diff-viewer';
|
|
style.textContent = `
|
|
.diff-block {
|
|
background: var(--bg-2); border: 1px solid var(--border);
|
|
border-radius: 8px; overflow: hidden; margin: 12px 0;
|
|
}
|
|
.diff-toolbar {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 6px 12px; border-bottom: 1px solid var(--border);
|
|
font-size: 12px; color: var(--text-3);
|
|
}
|
|
.diff-filename {
|
|
flex: 1; font-family: var(--mono); font-weight: 600;
|
|
color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.diff-stats { display: flex; gap: 8px; font-family: var(--mono); font-weight: 600; }
|
|
.diff-stat-add { color: var(--success, #27ae60); }
|
|
.diff-stat-del { color: var(--error, #e74c3c); }
|
|
.diff-copy-btn {
|
|
background: none; border: 1px solid var(--border); border-radius: 4px;
|
|
padding: 2px 8px; font-size: 12px; cursor: pointer; color: var(--text-3);
|
|
}
|
|
.diff-copy-btn:hover { color: var(--text-2); border-color: var(--text-3); }
|
|
.diff-content {
|
|
font-family: var(--mono); font-size: 13px; line-height: 1.5;
|
|
overflow-x: auto;
|
|
}
|
|
.diff-line { padding: 1px 12px; white-space: pre; min-height: 1.5em; }
|
|
.diff-indicator {
|
|
display: inline-block; width: 1.2em; text-align: center;
|
|
opacity: 0.7; user-select: none;
|
|
}
|
|
.diff-add { background: rgba(39,174,96,0.12); color: var(--success, #27ae60); }
|
|
.diff-del { background: rgba(231,76,60,0.12); color: var(--error, #e74c3c); }
|
|
.diff-hunk {
|
|
background: rgba(52,152,219,0.08); color: var(--info, #3498db);
|
|
font-style: italic; border-top: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border); margin: 2px 0;
|
|
}
|
|
.diff-file-header { background: var(--bg-3); color: var(--text-2); font-weight: 600; }
|
|
.diff-ctx { color: var(--text-3); }
|
|
.diff-meta { color: var(--text-3); font-style: italic; }
|
|
.diff-source-toggle { border-top: 1px solid var(--border); font-size: 12px; }
|
|
.diff-source-toggle summary {
|
|
padding: 6px 12px; cursor: pointer; color: var(--text-3); user-select: none;
|
|
}
|
|
.diff-source-toggle summary:hover { color: var(--text-2); }
|
|
.diff-source-toggle pre {
|
|
margin: 0; border-radius: 0; border: none;
|
|
max-height: 200px; overflow: auto;
|
|
}`;
|
|
document.head.appendChild(style);
|
|
},
|
|
|
|
destroy() {
|
|
document.getElementById('ext-style-diff-viewer')?.remove();
|
|
}
|
|
});
|