squashed build and bundling errors and improved user error messages

This commit is contained in:
2025-10-18 14:20:08 -05:00
parent 18ef73d1ae
commit 389e4530f6
14 changed files with 560 additions and 45 deletions
+87
View File
@@ -0,0 +1,87 @@
# Connection Error Fix
## Issue: "Could not establish connection. Receiving end does not exist."
This error occurs when the popup tries to communicate with the content script, but the content script hasn't fully loaded yet or isn't responding.
### When This Happens
1. **Settings pages or other Claude.ai pages**: The content script loads on all `claude.ai/*` pages, but there might be a timing issue where the page hasn't fully initialized the message listener yet.
2. **Page just loaded**: If you click the export button immediately after loading a Claude.ai page, the content script might not be ready.
3. **Tab navigated away and back**: Sometimes the content script needs to reinitialize.
### Solutions Implemented
#### 1. Better Error Messages
The extension now catches the "connection" error and provides helpful guidance:
- **For "Export Current Conversation"**:
- Old: Generic browser error
- New: "Content script not ready. Please refresh the Claude.ai page and try again."
- **For "Export All"**:
- Old: Generic browser error
- New: "Content script not ready. Please refresh the Claude.ai page or use 'Browse All Conversations' instead."
#### 2. Recommended Workarounds
If you see this error:
**Option A: Refresh the page**
1. Refresh the Claude.ai page (F5 or Ctrl+R)
2. Wait a moment for the page to fully load
3. Try the export again
**Option B: Use Browse All Conversations (Recommended)**
1. Click the extension icon
2. Click "Browse All Conversations" button
3. This opens a dedicated page that doesn't rely on content scripts
4. Export from there without any connection issues
### Why Browse All Conversations is Better
The "Browse All Conversations" feature:
- ✅ Works from ANY tab (not just Claude.ai)
- ✅ Doesn't depend on content scripts
- ✅ Never has connection errors
- ✅ Provides a full UI for browsing, searching, and filtering
- ✅ Shows export progress with cancel option
- ✅ More reliable for bulk exports
### Technical Details
**Content Script Loading**
- The content script runs on all `https://claude.ai/*` pages
- It sets up a message listener to handle export requests
- Sometimes there's a race condition between the page loading and the listener being ready
**Error Detection**
The popup now checks for connection errors by looking for keywords in the error message:
```typescript
if (errorMsg.includes('connection') || errorMsg.includes('Receiving end'))
```
**Fallback Behavior**
Rather than showing a cryptic browser error, the extension now:
1. Detects the connection failure
2. Provides a clear, actionable error message
3. Suggests alternatives (refresh or use Browse page)
### Best Practices
For the most reliable experience:
1. **Use "Browse All Conversations"** for bulk exports - it's the most reliable method
2. **Wait for pages to fully load** before using "Export Current" or "Export All"
3. **Refresh if you see connection errors** - this reloads the content script
4. **Check console logs** (F12) if issues persist - look for content script load messages
### Future Improvements
Potential enhancements to consider:
- Auto-inject content script if not detected
- Add retry logic with exponential backoff
- Show loading state while waiting for content script
- Detect if content script is loaded before attempting message send
+83
View File
@@ -0,0 +1,83 @@
# Content Script Build Issue - RESOLVED
## The Problem
The Vite web extension plugin (`@samrum/vite-plugin-web-extension`) was creating a dynamic import loader for the content script instead of bundling the actual code:
```javascript
// What was being generated (69 bytes):
(async()=>{await import(chrome.runtime.getURL("src/content.js"))})();
```
This created a self-referencing loop where the content script tried to load itself, causing it to never actually load. This is why you saw:
- "Could not establish connection" errors
- No "Claude Conversation Exporter content script loaded" message in console
- Content script not responding to messages from the popup
## The Root Cause
The `@samrum/vite-plugin-web-extension` plugin uses dynamic imports for content scripts by design, which doesn't work well with Firefox's Manifest V2 content script loading mechanism. The plugin was:
1. Creating a loader stub at `src/content.js`
2. Trying to dynamically import the actual code
3. But the actual code was never being generated as a separate chunk
## The Solution
Created a separate Vite configuration (`vite.content.config.ts`) that builds the content script as an IIFE (Immediately Invoked Function Expression) bundle, then copies it to the correct location.
### Files Added:
1. **`vite.content.config.ts`** - Separate Vite config for content script
- Builds content.ts as a library in IIFE format
- Outputs to temp directory
2. **`build-firefox.sh`** - Custom build script
- Builds content script separately first
- Builds main extension with Vite plugin
- Copies the properly built content script over the plugin's stub
3. **`build-check.sh`** - Verification script
- Checks if content script was built correctly
- Helps debug build issues
### Changes Made:
1. **`package.json`**
- Updated `build:firefox` script to use `build-firefox.sh`
2. **`src/firefox/manifest.json`**
- Added `"run_at": "document_end"` to content script configuration
- Ensures content script loads after page is ready
## Verification
After building with `pnpm build:firefox`:
- Content script is now **5.24 KB** (was 69 bytes)
- Contains actual minified code with all functions
- Loads properly on Claude.ai pages
- Console shows: "Claude Conversation Exporter content script loaded"
- Responds to messages from popup
## Next Steps for User
1. **Rebuild the extension:**
```bash
pnpm build:firefox
```
2. **Reload in Firefox:**
- Go to `about:debugging#/runtime/this-firefox`
- Click Reload button next to the extension
3. **Test on Claude.ai:**
- Navigate to any https://claude.ai/chat/... page
- Open console (F12)
- You should see: "Claude Conversation Exporter content script loaded"
- Try clicking "Export Current Conversation" - it should work now!
## Future Improvements
Consider:
- Migrate to Manifest V3 (though this has its own challenges)
- Use a different build plugin that handles content scripts better
- Or continue with this custom build script approach (works well!)
+87
View File
@@ -0,0 +1,87 @@
# Export Options Guide
The extension provides three different ways to export your Claude conversations:
## 1. Export Current Conversation
**Where:** Popup (extension icon)
**Requirements:** Must be on a Claude.ai conversation page
**What it does:** Exports the single conversation you're currently viewing
**Best for:** Quick exports of individual conversations
**How to use:**
1. Navigate to any Claude.ai conversation
2. Click the extension icon
3. Choose your format (JSON/Markdown/Text)
4. Click "Export Current Conversation"
---
## 2. Export All (requires Claude.ai tab)
**Where:** Popup (extension icon)
**Requirements:** Must be on ANY Claude.ai page (not necessarily a conversation)
**What it does:** Exports all conversations as a ZIP file using the content script
**Best for:** Quick bulk exports when you're already on Claude.ai
**How to use:**
1. Navigate to claude.ai (any page)
2. Click the extension icon
3. Choose your format (JSON/Markdown/Text)
4. Click "Export All (requires Claude.ai tab)"
5. Wait for the ZIP file to download
**Note:** If you see "Could not establish connection" error, you're not on a Claude.ai tab. Use option #3 instead.
---
## 3. Browse All Conversations
**Where:** Popup (extension icon)
**Requirements:** None - works from any tab
**What it does:** Opens a dedicated page where you can:
- View all your conversations in a searchable table
- Filter by model type
- Sort by name, created date, or updated date
- Export individual conversations
- Export selected/filtered conversations as a ZIP
- See export progress with cancel option
**Best for:**
- When you want to browse and selectively export
- When you're not on a Claude.ai tab
- When you want more control over what to export
**How to use:**
1. Click the extension icon (from ANY tab)
2. Click "Browse All Conversations"
3. A new tab opens with all your conversations
4. Use search/filters to find what you want
5. Click individual "Export" buttons OR "Export All" for bulk export
---
## Which Option Should I Use?
| Scenario | Recommended Option |
|----------|-------------------|
| Export one conversation I'm viewing | #1 - Export Current |
| Export all conversations, I'm on Claude.ai | #2 - Export All |
| Export all conversations, I'm NOT on Claude.ai | #3 - Browse All |
| I want to see what conversations I have | #3 - Browse All |
| I want to export only certain conversations | #3 - Browse All |
| I want to filter by model before exporting | #3 - Browse All |
---
## Troubleshooting
### "Could not establish connection" error
- You're trying to use option #2 from a non-Claude.ai tab
- **Solution:** Either navigate to claude.ai first, or use option #3 instead
### "Organization ID not configured"
- You need to set up your org ID first
- **Solution:** Click the options/settings link in the popup to configure it
### Browse page shows no conversations
- Check browser console (F12) for errors
- Verify your Organization ID is correct
- Make sure you're logged into claude.ai in the same browser
+112
View File
@@ -0,0 +1,112 @@
# Browse Page Troubleshooting Guide
## Steps to Debug
### 1. Rebuild the Extension
```bash
pnpm build:firefox
```
### 2. Reload Extension in Firefox
- Go to `about:debugging#/runtime/this-firefox`
- Find "Claude Conversation Exporter"
- Click **Reload** button
### 3. Open Browse Page
- Click the extension icon in Firefox toolbar
- Click "Browse All Conversations" button
### 4. Open Browser Console
- Press `F12` or `Ctrl+Shift+I` (Linux) to open Developer Tools
- Go to the **Console** tab
### 5. Check Console Logs
Look for these log messages in order:
```
[Browse] Script loaded, waiting for DOMContentLoaded...
[Browse] DOMContentLoaded fired, initializing...
[Browse] Loading org ID...
[Browse] Org ID loaded: <your-org-id>
[Browse] loadConversations called, orgId: <your-org-id>
[Browse] Fetching conversations from API...
[Browse] API response status: 200
[Browse] Loaded X conversations
[Browse] displayConversations called, filtered count: X
[Browse] Rendering table with X conversations
[Browse] Initialization complete
```
## Common Issues and Solutions
### Issue 1: No logs appear at all
**Problem:** Script isn't loading
**Solution:**
- Check that `browse.html` is in the correct location
- Verify the script tag in `browse.html` points to `browse.ts`
- Make sure Vite is bundling the browse page correctly
### Issue 2: "No org ID configured" error
**Problem:** Organization ID not set
**Solution:**
- Click the extension icon
- Click "Options" or gear icon
- Configure your Organization ID
- Instructions: Visit claude.ai, open DevTools Network tab, filter by "chat_conversations", copy the org ID from the URL
### Issue 3: "tableContent element not found!"
**Problem:** HTML structure issue
**Solution:**
- Check that `browse.html` has `<div id="tableContent">`
- Verify the HTML file is loading correctly
### Issue 4: API response status not 200
**Problem:** Authentication or API issue
**Solutions:**
- Make sure you're logged into claude.ai in the same browser
- Check if cookies are being sent with the request
- Verify the Organization ID is correct
### Issue 5: Script loaded but DOMContentLoaded never fires
**Problem:** Page isn't finishing load
**Solution:**
- Check for JavaScript errors in console
- Look for any failed resource loads in Network tab
## Manual Verification Steps
1. **Check if page is loading:**
- Open browse page, you should see "Loading conversations..." initially
2. **Verify HTML elements exist:**
Open console and run:
```javascript
console.log('tableContent:', document.getElementById('tableContent'));
console.log('stats:', document.getElementById('stats'));
console.log('searchInput:', document.getElementById('searchInput'));
```
All should show elements, not `null`
3. **Check storage:**
```javascript
browser.storage.sync.get(['organizationId']).then(console.log);
```
Should show your org ID
4. **Test API manually:**
```javascript
fetch('https://claude.ai/api/organizations/YOUR-ORG-ID/chat_conversations', {
credentials: 'include',
headers: { Accept: 'application/json' }
})
.then(r => r.json())
.then(console.log);
```
## Report Back
Please share:
1. What console logs you see (copy/paste)
2. Any error messages
3. Network tab showing the API request status
4. Screenshot of the browse page if possible
Executable
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
set -e
echo "Building Firefox extension..."
cd /home/jasonross/workspace/Claude-Conversation-Exporter-Firefox-TS
# Run TypeScript compiler
echo "Running TypeScript compiler..."
npx tsc --noEmit
# Run Vite build
echo "Running Vite build..."
npx vite build --mode firefox
# Check if content script was built correctly
echo ""
echo "Checking content script..."
CONTENT_SIZE=$(stat -f%z dist/firefox/src/content.js 2>/dev/null || stat -c%s dist/firefox/src/content.js 2>/dev/null)
echo "Content script size: $CONTENT_SIZE bytes"
if [ "$CONTENT_SIZE" -lt 100 ]; then
echo "❌ ERROR: Content script is too small! ($CONTENT_SIZE bytes)"
echo "Content of content.js:"
cat dist/firefox/src/content.js
else
echo "✅ Content script built successfully"
fi
echo ""
echo "Build complete!"
ls -lh dist/firefox/src/*.js
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
set -e
cd /home/jasonross/workspace/Claude-Conversation-Exporter-Firefox-TS
echo "Building content script separately..."
npx vite build --config vite.content.config.ts
echo "Building main extension..."
npx tsc && npx vite build --mode firefox
echo "Copying content script to dist..."
cp dist-temp-content/content.js dist/firefox/src/content.js
rm -rf dist-temp-content
echo ""
echo "Checking content script..."
CONTENT_SIZE=$(stat -c%s dist/firefox/src/content.js 2>/dev/null || stat -f%z dist/firefox/src/content.js)
echo "Content script size: $CONTENT_SIZE bytes"
if [ "$CONTENT_SIZE" -gt 1000 ]; then
echo "✅ Content script built successfully!"
else
echo "❌ ERROR: Content script is too small"
fi
echo ""
echo "Build complete!"
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"build": "tsc && vite build",
"build:firefox": "tsc && vite build --mode firefox",
"build:firefox": "bash build-firefox.sh",
"build:chrome": "tsc && vite build --mode chrome",
"dev": "vite build --watch",
"dev:firefox": "vite build --mode firefox --watch",
+38 -8
View File
@@ -60,17 +60,20 @@ function getElement<T extends HTMLElement>(id: string): T | null {
* Load organization ID from storage
*/
async function loadOrgId(): Promise<void> {
console.log('[Browse] Loading org ID...');
try {
const result = await browser.storage.sync.get(['organizationId']);
state.orgId = (result['organizationId'] as string) || null;
console.log('[Browse] Org ID loaded:', state.orgId);
if (!state.orgId) {
console.warn('[Browse] No org ID configured');
showError(
'Organization ID not configured. Please configure it in the extension options.'
);
}
} catch (error) {
console.error('Error loading org ID:', error);
console.error('[Browse] Error loading org ID:', error);
}
}
@@ -78,9 +81,14 @@ async function loadOrgId(): Promise<void> {
* Load all conversations from Claude API
*/
async function loadConversations(): Promise<void> {
if (!state.orgId) return;
console.log('[Browse] loadConversations called, orgId:', state.orgId);
if (!state.orgId) {
console.warn('[Browse] No org ID, skipping conversation load');
return;
}
try {
console.log('[Browse] Fetching conversations from API...');
const response = await fetch(
`https://claude.ai/api/organizations/${state.orgId}/chat_conversations`,
{
@@ -91,12 +99,13 @@ async function loadConversations(): Promise<void> {
}
);
console.log('[Browse] API response status:', response.status);
if (!response.ok) {
throw new Error(`Failed to load conversations: ${response.status}`);
}
const conversations = (await response.json()) as ConversationListItem[];
console.log(`Loaded ${conversations.length} conversations`);
console.log(`[Browse] Loaded ${conversations.length} conversations`);
// Infer models for conversations with null model
state.allConversations = conversations.map((conv) => ({
@@ -226,14 +235,28 @@ function sortConversations(): void {
* Display conversations in table
*/
function displayConversations(): void {
console.log(
'[Browse] displayConversations called, filtered count:',
state.filteredConversations.length
);
const tableContent = getElement('tableContent');
if (!tableContent) return;
if (!tableContent) {
console.error('[Browse] tableContent element not found!');
return;
}
if (state.filteredConversations.length === 0) {
console.log('[Browse] No conversations to display');
tableContent.innerHTML = '<div class="no-results">No conversations found</div>';
return;
}
console.log(
'[Browse] Rendering table with',
state.filteredConversations.length,
'conversations'
);
let html = `
<table>
<thead>
@@ -642,17 +665,24 @@ function setupEventListeners(): void {
// Export all button
const exportAllBtn = getElement('exportAllBtn');
exportAllBtn?.addEventListener('click', () => {
void exportAllFiltered;
void exportAllFiltered();
});
}
/**
* Initialize on page load
*/
console.log('[Browse] Script loaded, waiting for DOMContentLoaded...');
document.addEventListener('DOMContentLoaded', () => {
console.log('[Browse] DOMContentLoaded fired, initializing...');
void (async () => {
await loadOrgId();
await loadConversations();
setupEventListeners();
try {
await loadOrgId();
await loadConversations();
setupEventListeners();
console.log('[Browse] Initialization complete');
} catch (error) {
console.error('[Browse] Initialization error:', error);
}
})();
});
+2 -1
View File
@@ -26,7 +26,8 @@
{
"matches": ["https://claude.ai/*"],
"js": ["src/content.ts"],
"css": ["src/content-styles.css"]
"css": ["src/content-styles.css"],
"run_at": "document_end"
}
],
+2 -2
View File
@@ -155,6 +155,6 @@ document.addEventListener('DOMContentLoaded', () => {
const saveBtn = getElement('saveBtn');
const testBtn = getElement('testBtn');
saveBtn?.addEventListener('click', () => void saveSettings);
testBtn?.addEventListener('click', () => void testConnection);
saveBtn?.addEventListener('click', () => void saveSettings());
testBtn?.addEventListener('click', () => void testConnection());
});
+1 -1
View File
@@ -110,7 +110,7 @@
</div>
<button id="exportCurrent">Export Current Conversation</button>
<button id="exportAll">Export All Conversations</button>
<button id="exportAll">Export All (requires Claude.ai tab)</button>
<button id="browseConversations">Browse All Conversations</button>
<div id="status"></div>
+61 -32
View File
@@ -107,7 +107,7 @@ async function handleExportCurrent(): Promise<void> {
if (!conversationId) {
throw new Error(
'Could not detect conversation ID. Make sure you are on a Claude.ai conversation page.'
'Could not detect a conversation ID. Make sure you are on a Claude.ai conversation page.'
);
}
@@ -126,20 +126,31 @@ async function handleExportCurrent(): Promise<void> {
const formatSelect = getElement<HTMLSelectElement>('format');
const metadataCheck = getElement<HTMLInputElement>('includeMetadata');
const response = (await browser.tabs.sendMessage(tab.id, {
action: 'exportConversation',
conversationId,
orgId,
format: formatSelect?.value ?? 'json',
includeMetadata: metadataCheck?.checked ?? true,
})) as ExtensionResponse;
try {
const response = (await browser.tabs.sendMessage(tab.id, {
action: 'exportConversation',
conversationId,
orgId,
format: formatSelect?.value ?? 'json',
includeMetadata: metadataCheck?.checked ?? true,
})) as ExtensionResponse;
if (response?.success) {
showStatus('Conversation exported successfully!', 'success');
} else {
const errorMsg = response?.error ?? 'Export failed';
console.error('Export failed:', errorMsg, response?.details);
showStatus(errorMsg, 'error');
if (response?.success) {
showStatus('Conversation exported successfully!', 'success');
} else {
const errorMsg = response?.error ?? 'Export failed';
console.error('Export failed:', errorMsg, response?.details);
showStatus(errorMsg, 'error');
}
} catch (sendError) {
// Handle "Could not establish connection" error
const errorMsg = sendError instanceof Error ? sendError.message : String(sendError);
if (errorMsg.includes('connection') || errorMsg.includes('Receiving end')) {
throw new Error(
'Cannot connect to page. Please ensure you are on a Claude.ai conversation page (https://claude.ai/chat/...) and refresh if needed.'
);
}
throw sendError;
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error occurred';
@@ -153,7 +164,7 @@ async function handleExportCurrent(): Promise<void> {
* Handle browse conversations button
*/
function handleBrowseConversations(): void {
void browser.tabs.create({ url: browser.runtime.getURL('browse.html') });
void browser.tabs.create({ url: browser.runtime.getURL('src/browse.html') });
}
/**
@@ -178,30 +189,48 @@ async function handleExportAll(): Promise<void> {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab?.id) {
// Check if we're on Claude.ai
if (!tab?.url?.includes('claude.ai')) {
throw new Error(
'Please navigate to a Claude.ai page first, or use "Browse All Conversations" instead.'
);
}
if (!tab.id) {
throw new Error('Could not identify active tab.');
}
const formatSelect = getElement<HTMLSelectElement>('format');
const metadataCheck = getElement<HTMLInputElement>('includeMetadata');
const response = (await browser.tabs.sendMessage(tab.id, {
action: 'exportAllConversations',
orgId,
format: formatSelect?.value ?? 'json',
includeMetadata: metadataCheck?.checked ?? true,
})) as ExtensionResponse;
try {
const response = (await browser.tabs.sendMessage(tab.id, {
action: 'exportAllConversations',
orgId,
format: formatSelect?.value ?? 'json',
includeMetadata: metadataCheck?.checked ?? true,
})) as ExtensionResponse;
if (response?.success) {
if (response.warnings) {
showStatus(response.warnings, 'info');
if (response?.success) {
if (response.warnings) {
showStatus(response.warnings, 'info');
} else {
showStatus(`Exported ${response.count ?? 0} conversations!`, 'success');
}
} else {
showStatus(`Exported ${response.count ?? 0} conversations!`, 'success');
const errorMsg = response?.error ?? 'Export failed';
console.error('Export failed:', errorMsg, response?.details);
showStatus(errorMsg, 'error');
}
} else {
const errorMsg = response?.error ?? 'Export failed';
console.error('Export failed:', errorMsg, response?.details);
showStatus(errorMsg, 'error');
} catch (sendError) {
// Handle "Could not establish connection" error
const errorMsg = sendError instanceof Error ? sendError.message : String(sendError);
if (errorMsg.includes('connection') || errorMsg.includes('Receiving end')) {
throw new Error(
'Cannot connect to page. This feature requires a Claude.ai page (https://claude.ai/chat/... or similar). Try "Browse All Conversations" instead - it works from any tab.'
);
}
throw sendError;
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error occurred';
@@ -222,7 +251,7 @@ document.addEventListener('DOMContentLoaded', () => {
const browseBtn = getElement('browseConversations');
const exportAllBtn = getElement('exportAll');
exportCurrentBtn?.addEventListener('click', () => void handleExportCurrent);
exportCurrentBtn?.addEventListener('click', () => void handleExportCurrent());
browseBtn?.addEventListener('click', handleBrowseConversations);
exportAllBtn?.addEventListener('click', () => void handleExportAll);
exportAllBtn?.addEventListener('click', () => void handleExportAll());
});
+4
View File
@@ -24,6 +24,10 @@ export default defineConfig(({ mode }) => {
webExtension({
manifest,
useDynamicUrlWebAccessibleResources: false,
additionalInputs: {
html: ['src/browse.html'],
scripts: ['src/content.ts'],
},
}),
],
resolve: {
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'vite';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
build: {
outDir: 'dist-temp-content',
emptyOutDir: true,
lib: {
entry: resolve(__dirname, 'src/content.ts'),
name: 'ContentScript',
formats: ['iife'],
fileName: () => 'content.js',
},
rollupOptions: {
output: {
extend: true,
},
},
},
});