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:
parent
ce24ed507f
commit
32be89b76d
12 changed files with 1016 additions and 0 deletions
15
README.md
15
README.md
|
|
@ -226,6 +226,21 @@ This hook will:
|
||||||
- Show success/failure messages
|
- Show success/failure messages
|
||||||
- Continue with the regular push even if notes fail to push
|
- 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.
|
**⚠️ Security Notice**: cnotes executes as a Claude Code hook handler. Ensure your installation is secure and trusted.
|
||||||
|
|
|
||||||
128
chrome-extension/README.md
Normal file
128
chrome-extension/README.md
Normal 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
|
||||||
279
chrome-extension/background.js
Normal file
279
chrome-extension/background.js
Normal 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;
|
||||||
|
}
|
||||||
2
chrome-extension/icons/icon-128.png
Normal file
2
chrome-extension/icons/icon-128.png
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Placeholder for 128x128 icon.
|
||||||
|
Please generate from icon.svg or create your own 128x128 PNG icon.
|
||||||
2
chrome-extension/icons/icon-16.png
Normal file
2
chrome-extension/icons/icon-16.png
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Placeholder for 16x16 icon.
|
||||||
|
Please generate from icon.svg or create your own 16x16 PNG icon.
|
||||||
2
chrome-extension/icons/icon-48.png
Normal file
2
chrome-extension/icons/icon-48.png
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Placeholder for 48x48 icon.
|
||||||
|
Please generate from icon.svg or create your own 48x48 PNG icon.
|
||||||
24
chrome-extension/icons/icon.svg
Normal file
24
chrome-extension/icons/icon.svg
Normal 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 |
29
chrome-extension/manifest.json
Normal file
29
chrome-extension/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
chrome-extension/popup.html
Normal file
25
chrome-extension/popup.html
Normal 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
219
chrome-extension/popup.js
Normal 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
281
chrome-extension/styles.css
Normal 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
10
chrome-extension/utils.js
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue