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
|
||||
- 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
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