1
0
Fork 0
mirror of https://github.com/imjasonh/cnotes synced 2026-07-08 00:55:37 +00:00

Add Chrome extension for viewing git notes on GitHub

- Popup-based extension that works on GitHub commit pages
- Shows badge with note count when commits have notes
- Displays all available git notes refs in tabbed interface
- Supports cnotes format with special formatting
- Works with any public GitHub repository (no auth required)
- Includes caching to minimize API calls (60 req/hour limit)
This commit is contained in:
Jason Hall 2025-07-21 22:22:32 -04:00
parent ce24ed507f
commit 32be89b76d
Failed to extract signature
12 changed files with 1016 additions and 0 deletions

View file

@ -226,6 +226,21 @@ This hook will:
- Show success/failure messages
- Continue with the regular push even if notes fail to push
## Chrome Extension
A Chrome extension is available to view git notes directly on GitHub commit pages. See [`chrome-extension/`](chrome-extension/) for details.
### Features
- 🔢 Shows badge indicator when commits have notes
- 📝 Displays notes in a popup when clicked
- 🏷️ Supports multiple notes refs with tabs
- ⚡ Works with any public repository
### Installation
1. Load the extension from `chrome-extension/` directory
2. Navigate to any GitHub commit page
3. Click the extension icon to view notes
---
**⚠️ Security Notice**: cnotes executes as a Claude Code hook handler. Ensure your installation is secure and trusted.

128
chrome-extension/README.md Normal file
View file

@ -0,0 +1,128 @@
# GitHub Git Notes Viewer
A Chrome extension that displays git notes on GitHub commit pages via a popup interface. Click the extension icon while viewing a GitHub commit to see all available git notes.
## Features
- 🔍 Works on any GitHub commit page - just click the extension icon
- 🔢 Shows badge with note count on commits that have notes
- 📝 Displays all available git notes refs for a commit
- 🏷️ Supports multiple notes refs with tabbed interface
- 🎨 Clean popup UI with GitHub-style design
- ⚡ Caches results to minimize API calls
- 🚫 No authentication required (public repos only)
## Installation
### From Source (Development)
1. Clone this repository:
```bash
git clone https://github.com/imjasonh/chrome-github-notes.git
cd chrome-github-notes
```
2. Generate icon files from the SVG (optional):
```bash
# Using ImageMagick or similar tool
convert icons/icon.svg -resize 16x16 icons/icon-16.png
convert icons/icon.svg -resize 48x48 icons/icon-48.png
convert icons/icon.svg -resize 128x128 icons/icon-128.png
```
3. Load the extension in Chrome:
- Open Chrome and navigate to `chrome://extensions`
- Enable "Developer mode" in the top right
- Click "Load unpacked"
- Select the `chrome-github-notes` directory
## Usage
1. Navigate to any GitHub commit page (e.g., `https://github.com/owner/repo/commit/abc123`)
2. If the commit has notes, you'll see a blue badge on the extension icon showing the count
3. Click the extension icon to view the notes
4. Click on tabs to switch between different notes refs if multiple exist
The extension automatically checks for notes when you visit GitHub commit pages and displays a badge indicator when notes are found.
## How It Works
The extension uses the GitHub API to fetch git notes:
1. Detects when you're on a GitHub commit page
2. Extracts the repository and commit information from the URL
3. Makes API calls to fetch available notes refs
4. For each ref, fetches the notes content for the specific commit
5. Displays the notes in a formatted view
## API Rate Limits
- **Unauthenticated**: 60 requests per hour per IP address
- The extension caches results for 5 minutes to minimize API usage
- Rate limit errors are displayed clearly to the user
## Supported Note Formats
The extension recognizes and formats several note types:
### cnotes Format
If notes were created by [cnotes](https://github.com/imjasonh/cnotes), they're displayed with:
- Session information
- Claude version
- Tools used
- Formatted conversation excerpts
### JSON Format
JSON notes are pretty-printed with syntax highlighting
### Plain Text
Plain text notes are displayed in a monospace font
## Development
### Project Structure
```
chrome-github-notes/
├── manifest.json # Extension manifest (V3)
├── background.js # Service worker for API calls
├── popup.html # Extension popup interface
├── popup.js # Popup logic and UI handling
├── styles.css # Popup styles
├── utils.js # Utility functions (placeholder)
└── icons/ # Extension icons
```
### Key Components
- **popup.js**: Detects current tab URL and displays notes in popup
- **background.js**: Handles GitHub API calls to bypass CORS restrictions
- **styles.css**: Clean popup UI with GitHub-inspired design
## Limitations
- Only works with public repositories (no authentication)
- Limited to 60 API requests per hour
- Notes must be pushed to GitHub (`git push origin refs/notes/*`)
- Only displays notes on individual commit pages
## Future Enhancements
- [ ] OAuth authentication for private repos and higher rate limits
- [ ] Display notes on commit list pages
- [ ] User preferences and configuration
- [ ] Export functionality
- [ ] Support for more note formats
- [ ] Integration with git notes management tools
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
## License
MIT License - see LICENSE file for details
## Related Projects
- [cnotes](https://github.com/imjasonh/cnotes) - Git notes for Claude conversations
- [git-notes](https://git-scm.com/docs/git-notes) - Git notes documentation

View file

@ -0,0 +1,279 @@
// Background service worker for GitHub Git Notes Viewer
// Handles API calls to bypass CORS restrictions and monitors tabs for notes
// Cache for API responses (5 minutes TTL)
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
// Track tab states
const tabStates = new Map();
// Listen for tab updates
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
checkTabForNotes(tabId, tab.url);
}
});
// Listen for tab activation
chrome.tabs.onActivated.addListener(async (activeInfo) => {
const tab = await chrome.tabs.get(activeInfo.tabId);
if (tab.url) {
checkTabForNotes(activeInfo.tabId, tab.url);
}
});
// Check if a tab is on a GitHub commit page and has notes
async function checkTabForNotes(tabId, url) {
const commitMatch = url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/commit\/([0-9a-f]+)/i);
if (!commitMatch) {
// Not a commit page, clear badge
chrome.action.setBadgeText({ tabId, text: '' });
tabStates.delete(tabId);
return;
}
const [, owner, repo, commitSha] = commitMatch;
const cacheKey = `${owner}/${repo}/${commitSha}`;
// Check if we already know about this tab
const tabState = tabStates.get(tabId);
if (tabState && tabState.cacheKey === cacheKey && tabState.checked) {
return;
}
try {
// Check cache first
const cached = cache.get(cacheKey);
let noteCount = 0;
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
noteCount = Object.keys(cached.data).length;
} else {
// Fetch notes to check if any exist
const notesRefs = await fetchNotesRefs(owner, repo);
for (const ref of notesRefs) {
const refName = ref.replace('refs/notes/', '');
const hasNotes = await checkCommitHasNotes(owner, repo, commitSha, refName);
if (hasNotes) {
noteCount++;
}
}
}
// Update badge
if (noteCount > 0) {
chrome.action.setBadgeText({ tabId, text: noteCount.toString() });
chrome.action.setBadgeBackgroundColor({ tabId, color: '#0366d6' });
} else {
chrome.action.setBadgeText({ tabId, text: '' });
}
// Save state
tabStates.set(tabId, {
cacheKey,
noteCount,
checked: true
});
} catch (error) {
console.error('Error checking for notes:', error);
// Don't show badge on error
chrome.action.setBadgeText({ tabId, text: '' });
}
}
// Quick check if a commit has notes without fetching content
async function checkCommitHasNotes(owner, repo, commitSha, notesRef) {
try {
const refResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/refs/notes/${notesRef}`
);
if (!refResponse.ok) {
return false;
}
const refData = await refResponse.json();
const notesSha = refData.object.sha;
// Get the tree
const commitResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/commits/${notesSha}`
);
if (!commitResponse.ok) {
return false;
}
const commitData = await commitResponse.json();
const treeSha = commitData.tree.sha;
const treeResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/trees/${treeSha}`
);
if (!treeResponse.ok) {
return false;
}
const treeData = await treeResponse.json();
// Check if this commit exists in the tree
return treeData.tree.some(item =>
item.path.startsWith(commitSha) || commitSha.startsWith(item.path.substring(0, 7))
);
} catch (error) {
console.error(`Error checking notes for ref ${notesRef}:`, error);
return false;
}
}
// Listen for messages from content script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'fetchGitNotes') {
handleFetchGitNotes(request, sendResponse);
return true; // Will respond asynchronously
}
});
async function handleFetchGitNotes(request, sendResponse) {
const { owner, repo, commitSha } = request;
const cacheKey = `${owner}/${repo}/${commitSha}`;
// Check cache first
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
sendResponse({ success: true, data: cached.data });
return;
}
try {
// Fetch all available notes refs
const notesRefs = await fetchNotesRefs(owner, repo);
const allNotes = {};
for (const ref of notesRefs) {
const refName = ref.replace('refs/notes/', '');
const notes = await fetchNotesForCommit(owner, repo, commitSha, refName);
if (notes) {
allNotes[refName] = notes;
}
}
// Cache the result
cache.set(cacheKey, {
data: allNotes,
timestamp: Date.now()
});
sendResponse({ success: true, data: allNotes });
} catch (error) {
console.error('Error fetching git notes:', error);
sendResponse({
success: false,
error: error.message,
rateLimitRemaining: error.rateLimitRemaining
});
}
}
async function fetchNotesRefs(owner, repo) {
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs`);
if (!response.ok) {
throw createError(response);
}
const refs = await response.json();
return refs
.filter(ref => ref.ref.startsWith('refs/notes/'))
.map(ref => ref.ref);
}
async function fetchNotesForCommit(owner, repo, commitSha, notesRef) {
try {
// Step 1: Get the notes reference
const refResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/refs/notes/${notesRef}`
);
if (!refResponse.ok) {
if (refResponse.status === 404) {
return null; // No notes in this ref
}
throw createError(refResponse);
}
const refData = await refResponse.json();
const notesSha = refData.object.sha;
// Step 2: Get the commit object
const commitResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/commits/${notesSha}`
);
if (!commitResponse.ok) {
throw createError(commitResponse);
}
const commitData = await commitResponse.json();
const treeSha = commitData.tree.sha;
// Step 3: Get the tree
const treeResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/trees/${treeSha}`
);
if (!treeResponse.ok) {
throw createError(treeResponse);
}
const treeData = await treeResponse.json();
// Find the blob for this commit (handle short SHAs)
const blob = treeData.tree.find(item =>
item.path.startsWith(commitSha) || commitSha.startsWith(item.path.substring(0, 7))
);
if (!blob) {
return null; // No notes for this commit
}
// Step 4: Get the blob content
const blobResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`
);
if (!blobResponse.ok) {
throw createError(blobResponse);
}
const blobData = await blobResponse.json();
// Decode base64 content
const content = atob(blobData.content);
// Try to parse as JSON
try {
return JSON.parse(content);
} catch {
// Return as plain text if not JSON
return { content };
}
} catch (error) {
console.error(`Error fetching notes for ref ${notesRef}:`, error);
throw error;
}
}
function createError(response) {
const error = new Error(`GitHub API error: ${response.status} ${response.statusText}`);
error.status = response.status;
error.rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
return error;
}

View file

@ -0,0 +1,2 @@
Placeholder for 128x128 icon.
Please generate from icon.svg or create your own 128x128 PNG icon.

View file

@ -0,0 +1,2 @@
Placeholder for 16x16 icon.
Please generate from icon.svg or create your own 16x16 PNG icon.

View file

@ -0,0 +1,2 @@
Placeholder for 48x48 icon.
Please generate from icon.svg or create your own 48x48 PNG icon.

View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<!-- Background circle -->
<circle cx="64" cy="64" r="60" fill="#0366d6"/>
<!-- Git branch symbol -->
<g fill="white">
<!-- Main branch -->
<rect x="30" y="60" width="50" height="8" rx="4"/>
<!-- Branch -->
<rect x="70" y="40" width="8" height="28" rx="4"/>
<rect x="70" y="40" width="28" height="8" rx="4"/>
<!-- Commit dots -->
<circle cx="34" cy="64" r="6"/>
<circle cx="54" cy="64" r="6"/>
<circle cx="74" cy="64" r="6"/>
<circle cx="94" cy="44" r="6"/>
<!-- Note symbol (document) -->
<path d="M 45 75 L 45 95 L 65 95 L 65 85 L 55 85 L 55 75 Z" />
<path d="M 55 85 L 55 91 L 61 91 Z" fill="#0366d6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View file

@ -0,0 +1,29 @@
{
"manifest_version": 3,
"name": "GitHub Git Notes Viewer",
"version": "1.0.0",
"description": "Display git notes on GitHub commit pages",
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"permissions": [
"activeTab",
"tabs"
],
"host_permissions": [
"https://api.github.com/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
}

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="popup-container">
<header class="popup-header">
<h1>Git Notes Viewer</h1>
<div id="status" class="status"></div>
</header>
<main id="content" class="popup-content">
<!-- Content will be inserted here by popup.js -->
<div class="loading">
<div class="spinner"></div>
<span>Loading...</span>
</div>
</main>
</div>
<script src="popup.js"></script>
</body>
</html>

219
chrome-extension/popup.js Normal file
View file

@ -0,0 +1,219 @@
// Popup script for GitHub Git Notes Viewer
document.addEventListener('DOMContentLoaded', async () => {
const statusEl = document.getElementById('status');
const contentEl = document.getElementById('content');
try {
// Get the current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
// Check if we're on a GitHub commit page
const commitMatch = tab.url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/commit\/([0-9a-f]+)/i);
if (!commitMatch) {
showMessage('Not a GitHub commit page', 'warning');
return;
}
const [, owner, repo, commitSha] = commitMatch;
// Update status
statusEl.innerHTML = `
<span class="repo-info">${owner}/${repo}</span>
<span class="commit-sha">${commitSha.substring(0, 7)}</span>
`;
// Fetch notes
const response = await new Promise((resolve) => {
chrome.runtime.sendMessage({
action: 'fetchGitNotes',
owner,
repo,
commitSha
}, resolve);
});
if (!response.success) {
showError(response.error, response.rateLimitRemaining);
return;
}
displayNotes(response.data);
} catch (error) {
console.error('Error:', error);
showError(error.message);
}
});
function displayNotes(notes) {
const contentEl = document.getElementById('content');
const noteRefs = Object.keys(notes);
if (noteRefs.length === 0) {
showMessage('No git notes found for this commit', 'info');
return;
}
let html = '';
if (noteRefs.length === 1) {
// Single ref - show directly
const refName = noteRefs[0];
html = `
<div class="note-ref">
<h2>${refName}</h2>
${formatNote(notes[refName])}
</div>
`;
} else {
// Multiple refs - create tabs
html = '<div class="tabs">';
html += '<div class="tab-buttons">';
noteRefs.forEach((ref, index) => {
const isActive = index === 0;
html += `
<button class="tab-button ${isActive ? 'active' : ''}"
data-ref="${ref}">
${ref}
</button>
`;
});
html += '</div>';
html += '<div class="tab-content">';
noteRefs.forEach((ref, index) => {
const isActive = index === 0;
html += `
<div class="tab-panel ${isActive ? 'active' : ''}" data-ref="${ref}">
${formatNote(notes[ref])}
</div>
`;
});
html += '</div></div>';
}
contentEl.innerHTML = html;
// Set up tab switching
if (noteRefs.length > 1) {
setupTabs();
}
}
function formatNote(noteData) {
// Check if it's a cnotes-style note
if (noteData.session_id && noteData.conversation_excerpt) {
return formatCnotesNote(noteData);
}
// Check if it's JSON
if (typeof noteData === 'object' && !noteData.content) {
return `<pre class="json-content">${JSON.stringify(noteData, null, 2)}</pre>`;
}
// Plain text
const content = noteData.content || noteData;
return `<pre class="text-content">${escapeHtml(content)}</pre>`;
}
function formatCnotesNote(data) {
let html = '<div class="cnotes-content">';
// Metadata
html += '<div class="cnotes-meta">';
html += `<span class="badge">Session: ${data.session_id.substring(0, 8)}...</span>`;
if (data.claude_version) {
html += `<span class="badge badge-primary">${data.claude_version}</span>`;
}
html += `<span class="timestamp">${new Date(data.timestamp).toLocaleString()}</span>`;
html += '</div>';
// Tools used
if (data.tools_used && data.tools_used.length > 0) {
html += '<div class="tools-section">';
html += '<strong>Tools used:</strong> ';
data.tools_used.forEach(tool => {
html += `<span class="badge badge-secondary">${tool}</span>`;
});
html += '</div>';
}
// Conversation excerpt
if (data.conversation_excerpt) {
html += '<div class="conversation-section">';
html += '<strong>Conversation:</strong>';
html += '<div class="conversation-excerpt">';
let formatted = escapeHtml(data.conversation_excerpt);
formatted = formatted.replace(/👤 User:|🧑 User:/g, '<strong class="user">$&</strong>');
formatted = formatted.replace(/🤖 Claude:/g, '<strong class="assistant">$&</strong>');
formatted = formatted.replace(/Tool \(([^)]+)\):/g, '<em class="tool">Tool ($1):</em>');
formatted = formatted.replace(/\n/g, '<br>');
html += formatted;
html += '</div></div>';
}
html += '</div>';
return html;
}
function setupTabs() {
const buttons = document.querySelectorAll('.tab-button');
const panels = document.querySelectorAll('.tab-panel');
buttons.forEach(button => {
button.addEventListener('click', () => {
const ref = button.dataset.ref;
// Update buttons
buttons.forEach(btn => {
btn.classList.toggle('active', btn === button);
});
// Update panels
panels.forEach(panel => {
panel.classList.toggle('active', panel.dataset.ref === ref);
});
});
});
}
function showMessage(message, type = 'info') {
const contentEl = document.getElementById('content');
contentEl.innerHTML = `
<div class="message message-${type}">
<p>${message}</p>
</div>
`;
}
function showError(error, rateLimitRemaining) {
let message = error;
if (error.includes('403') || error.includes('rate limit')) {
message = 'GitHub API rate limit exceeded.';
if (rateLimitRemaining !== undefined) {
message += ` (${rateLimitRemaining} requests remaining)`;
}
message += '<br><small>Limit: 60 requests/hour for unauthenticated access</small>';
}
const contentEl = document.getElementById('content');
contentEl.innerHTML = `
<div class="message message-error">
<p>${message}</p>
</div>
`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

281
chrome-extension/styles.css Normal file
View file

@ -0,0 +1,281 @@
/* Styles for GitHub Git Notes Viewer Popup */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #24292e;
background: #ffffff;
min-width: 400px;
max-width: 600px;
min-height: 200px;
max-height: 600px;
}
.popup-container {
display: flex;
flex-direction: column;
height: 100%;
}
/* Header */
.popup-header {
background: #f6f8fa;
border-bottom: 1px solid #e1e4e8;
padding: 12px 16px;
}
.popup-header h1 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.status {
margin-top: 4px;
font-size: 12px;
color: #586069;
}
.repo-info {
font-weight: 600;
color: #0366d6;
}
.commit-sha {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
background: #e1e4e8;
padding: 2px 4px;
border-radius: 3px;
margin-left: 4px;
}
/* Content */
.popup-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 32px;
color: #586069;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid #e1e4e8;
border-top-color: #0366d6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Messages */
.message {
padding: 16px;
border-radius: 6px;
margin: 16px 0;
}
.message p {
margin: 0;
}
.message-info {
background: #f1f8ff;
border: 1px solid #c8e1ff;
color: #0366d6;
}
.message-warning {
background: #fffbdd;
border: 1px solid #fffbdd;
color: #735c0f;
}
.message-error {
background: #ffeef0;
border: 1px solid #ffdce0;
color: #d73a49;
}
/* Tabs */
.tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.tab-buttons {
display: flex;
gap: 8px;
margin-bottom: 16px;
border-bottom: 1px solid #e1e4e8;
padding-bottom: 8px;
}
.tab-button {
background: none;
border: none;
padding: 6px 12px;
font-size: 14px;
color: #586069;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
}
.tab-button:hover {
background: #f6f8fa;
color: #24292e;
}
.tab-button.active {
background: #0366d6;
color: white;
}
.tab-content {
flex: 1;
overflow-y: auto;
}
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
/* Note content */
.note-ref h2 {
font-size: 14px;
margin: 0 0 12px 0;
color: #24292e;
}
.json-content,
.text-content {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 12px;
overflow-x: auto;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 12px;
line-height: 1.45;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
/* cnotes specific */
.cnotes-content {
font-size: 13px;
}
.cnotes-meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.badge {
display: inline-block;
padding: 2px 8px;
font-size: 11px;
font-weight: 500;
line-height: 18px;
border-radius: 12px;
background: #e1e4e8;
color: #24292e;
}
.badge-primary {
background: #0366d6;
color: white;
}
.badge-secondary {
background: #6a737d;
color: white;
}
.timestamp {
font-size: 12px;
color: #586069;
}
.tools-section {
margin-bottom: 12px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
.tools-section strong {
margin-right: 4px;
}
.conversation-section {
margin-top: 12px;
}
.conversation-excerpt {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 12px;
margin-top: 8px;
font-size: 12px;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
}
.user {
color: #0366d6;
}
.assistant {
color: #6f42c1;
}
.tool {
color: #586069;
font-style: italic;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f3f4;
}
::-webkit-scrollbar-thumb {
background: #dadce0;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #bdc1c6;
}

10
chrome-extension/utils.js Normal file
View file

@ -0,0 +1,10 @@
// Utility functions for GitHub Git Notes Viewer
// Currently empty as the main functionality is in content.js and background.js
// This file is included in the manifest for future utility functions
// Placeholder for future utilities:
// - SHA validation
// - Markdown rendering
// - Enhanced error handling
// - User preferences management