diff --git a/README.md b/README.md index 576f8ec..a4e89d5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/chrome-extension/README.md b/chrome-extension/README.md new file mode 100644 index 0000000..af3a525 --- /dev/null +++ b/chrome-extension/README.md @@ -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 \ No newline at end of file diff --git a/chrome-extension/background.js b/chrome-extension/background.js new file mode 100644 index 0000000..76ecda5 --- /dev/null +++ b/chrome-extension/background.js @@ -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; +} \ No newline at end of file diff --git a/chrome-extension/icons/icon-128.png b/chrome-extension/icons/icon-128.png new file mode 100644 index 0000000..f3c5d13 --- /dev/null +++ b/chrome-extension/icons/icon-128.png @@ -0,0 +1,2 @@ +Placeholder for 128x128 icon. +Please generate from icon.svg or create your own 128x128 PNG icon. \ No newline at end of file diff --git a/chrome-extension/icons/icon-16.png b/chrome-extension/icons/icon-16.png new file mode 100644 index 0000000..e904f01 --- /dev/null +++ b/chrome-extension/icons/icon-16.png @@ -0,0 +1,2 @@ +Placeholder for 16x16 icon. +Please generate from icon.svg or create your own 16x16 PNG icon. \ No newline at end of file diff --git a/chrome-extension/icons/icon-48.png b/chrome-extension/icons/icon-48.png new file mode 100644 index 0000000..10d6b33 --- /dev/null +++ b/chrome-extension/icons/icon-48.png @@ -0,0 +1,2 @@ +Placeholder for 48x48 icon. +Please generate from icon.svg or create your own 48x48 PNG icon. \ No newline at end of file diff --git a/chrome-extension/icons/icon.svg b/chrome-extension/icons/icon.svg new file mode 100644 index 0000000..6c5eb1f --- /dev/null +++ b/chrome-extension/icons/icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json new file mode 100644 index 0000000..e26cb2a --- /dev/null +++ b/chrome-extension/manifest.json @@ -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" + } + } +} \ No newline at end of file diff --git a/chrome-extension/popup.html b/chrome-extension/popup.html new file mode 100644 index 0000000..8b8c5da --- /dev/null +++ b/chrome-extension/popup.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/chrome-extension/popup.js b/chrome-extension/popup.js new file mode 100644 index 0000000..b2326ff --- /dev/null +++ b/chrome-extension/popup.js @@ -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 = ` + ${owner}/${repo} + ${commitSha.substring(0, 7)} + `; + + // 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 = ` +
+

${refName}

+ ${formatNote(notes[refName])} +
+ `; + } else { + // Multiple refs - create tabs + html = '
'; + html += '
'; + + noteRefs.forEach((ref, index) => { + const isActive = index === 0; + html += ` + + `; + }); + + html += '
'; + html += '
'; + + noteRefs.forEach((ref, index) => { + const isActive = index === 0; + html += ` +
+ ${formatNote(notes[ref])} +
+ `; + }); + + html += '
'; + } + + 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 `
${JSON.stringify(noteData, null, 2)}
`; + } + + // Plain text + const content = noteData.content || noteData; + return `
${escapeHtml(content)}
`; +} + +function formatCnotesNote(data) { + let html = '
'; + + // Metadata + html += '
'; + html += `Session: ${data.session_id.substring(0, 8)}...`; + if (data.claude_version) { + html += `${data.claude_version}`; + } + html += `${new Date(data.timestamp).toLocaleString()}`; + html += '
'; + + // Tools used + if (data.tools_used && data.tools_used.length > 0) { + html += '
'; + html += 'Tools used: '; + data.tools_used.forEach(tool => { + html += `${tool}`; + }); + html += '
'; + } + + // Conversation excerpt + if (data.conversation_excerpt) { + html += '
'; + html += 'Conversation:'; + html += '
'; + + let formatted = escapeHtml(data.conversation_excerpt); + formatted = formatted.replace(/👤 User:|🧑 User:/g, '$&'); + formatted = formatted.replace(/🤖 Claude:/g, '$&'); + formatted = formatted.replace(/Tool \(([^)]+)\):/g, 'Tool ($1):'); + formatted = formatted.replace(/\n/g, '
'); + + html += formatted; + html += '
'; + } + + html += '
'; + 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 = ` +
+

${message}

+
+ `; +} + +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 += '
Limit: 60 requests/hour for unauthenticated access'; + } + + const contentEl = document.getElementById('content'); + contentEl.innerHTML = ` +
+

${message}

+
+ `; +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} \ No newline at end of file diff --git a/chrome-extension/styles.css b/chrome-extension/styles.css new file mode 100644 index 0000000..ddb1a60 --- /dev/null +++ b/chrome-extension/styles.css @@ -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; +} \ No newline at end of file diff --git a/chrome-extension/utils.js b/chrome-extension/utils.js new file mode 100644 index 0000000..c11ef3b --- /dev/null +++ b/chrome-extension/utils.js @@ -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 \ No newline at end of file