Skip to content

Commit fbce3d3

Browse files
authored
feat: Add Chrome browser support (#7)
* feat(chrome): add Chrome Web Store support - Add separate manifests for Chrome and Firefox - Fix cross-browser API compatibility (browser vs chrome) - Fix Chrome MV3 async messaging (sendResponse pattern) - Fix CustomEvent detail passing via JSON serialization - Add auto-reload on settings change - Add tabs permission for reload functionality - Add .dev marker for debug mode detection - Add comprehensive tests (188 total) * fix(chrome): add retry logic for service worker wake-up Chrome MV3 service workers can be inactive when popup opens, causing sendMessage to return undefined. Added exponential backoff retry logic (50ms, 100ms, 200ms) to handle service worker wake-up delay. - Add MESSAGE_RETRY_DELAYS_MS constant for backoff intervals - Validate response is not undefined before returning - Check chrome.runtime.lastError in Chrome context - Add 5 new tests for retry scenarios * fix(settings): sync settings to localStorage for reliable page-script access Settings were not consistently applied on page reload because page-script runs before content-script can provide config via events. Changes: - Add localStorage mirroring in storage.ts (syncToLocalStorage) - page-inject.ts now syncs settings before injecting page-script - page-script.ts reads localStorage on every getConfig() call - .gitignore: exclude extension/manifest.json (build artifact) This eliminates the race condition where fetch interception used default settings instead of user-configured values. * feat(ci): add Chrome Web Store publishing to release workflow - Build and publish both Firefox and Chrome extensions on release - Create separate browser-specific zip files (-firefox.zip, -chrome.zip) - Add Chrome Web Store publishing step using browser-actions/release-chrome-extension - Update GitHub Release notes with both store links - Document required GitHub secrets (CHROME_EXTENSION_ID, CHROME_CLIENT_ID, CHROME_CLIENT_SECRET, CHROME_REFRESH_TOKEN) - Fix lint error in messages.ts for chrome.runtime.lastError typing * docs: add privacy policy for Chrome Web Store
1 parent 4a0e86c commit fbce3d3

28 files changed

+1827
-254
lines changed

.github/workflows/release.yml

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,45 @@ jobs:
3434
- name: Type check
3535
run: npm run build:types
3636

37-
- name: Build
38-
run: npm run build
39-
4037
- name: Get version from tag
4138
id: version
4239
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
4340

44-
- name: Create extension zip
41+
# =========================================================================
42+
# Firefox Build
43+
# =========================================================================
44+
- name: Build Firefox extension
45+
run: npm run build:prod:firefox
46+
47+
- name: Create Firefox extension zip
4548
run: |
4649
cd extension
47-
zip -r ../light-session-${{ steps.version.outputs.VERSION }}.zip \
50+
zip -r ../light-session-${{ steps.version.outputs.VERSION }}-firefox.zip \
4851
manifest.json \
4952
dist/ \
5053
popup/ \
5154
icons/ \
5255
-x "*.map"
5356
57+
# =========================================================================
58+
# Chrome Build
59+
# =========================================================================
60+
- name: Build Chrome extension
61+
run: npm run build:prod:chrome
62+
63+
- name: Create Chrome extension zip
64+
run: |
65+
cd extension
66+
zip -r ../light-session-${{ steps.version.outputs.VERSION }}-chrome.zip \
67+
manifest.json \
68+
dist/ \
69+
popup/ \
70+
icons/ \
71+
-x "*.map"
72+
73+
# =========================================================================
74+
# Source Archive
75+
# =========================================================================
5476
- name: Create source zip
5577
run: |
5678
zip -r light-session-${{ steps.version.outputs.VERSION }}-source.zip \
@@ -68,15 +90,20 @@ jobs:
6890
docs/ \
6991
extension/src/ \
7092
extension/icons/ \
71-
extension/manifest.json \
93+
extension/manifest.firefox.json \
94+
extension/manifest.chrome.json \
7295
tests/
7396
97+
# =========================================================================
98+
# GitHub Release
99+
# =========================================================================
74100
- name: Create GitHub Release
75101
uses: softprops/action-gh-release@v2
76102
with:
77103
generate_release_notes: true
78104
files: |
79-
light-session-${{ steps.version.outputs.VERSION }}.zip
105+
light-session-${{ steps.version.outputs.VERSION }}-firefox.zip
106+
light-session-${{ steps.version.outputs.VERSION }}-chrome.zip
80107
light-session-${{ steps.version.outputs.VERSION }}-source.zip
81108
body: |
82109
## LightSession v${{ steps.version.outputs.VERSION }}
@@ -86,19 +113,43 @@ jobs:
86113
**Firefox Add-ons (recommended):**
87114
[Install from AMO](https://addons.mozilla.org/en-US/firefox/addon/lightsession-for-chatgpt/)
88115
89-
**Manual install:**
90-
1. Download `light-session-${{ steps.version.outputs.VERSION }}.zip`
116+
**Chrome Web Store:**
117+
[Install from Chrome Web Store](https://chrome.google.com/webstore/detail/lightsession-for-chatgpt/${{ secrets.CHROME_EXTENSION_ID }})
118+
119+
**Manual install (Firefox):**
120+
1. Download `light-session-${{ steps.version.outputs.VERSION }}-firefox.zip`
91121
2. Open `about:debugging#/runtime/this-firefox` in Firefox
92122
3. Click "Load Temporary Add-on"
93123
4. Select the downloaded zip file
94124
125+
**Manual install (Chrome):**
126+
1. Download `light-session-${{ steps.version.outputs.VERSION }}-chrome.zip`
127+
2. Open `chrome://extensions` in Chrome
128+
3. Enable "Developer mode"
129+
4. Click "Load unpacked" and select the extracted folder
130+
95131
---
96132
133+
# =========================================================================
134+
# Firefox Add-ons Publishing
135+
# =========================================================================
97136
- name: Publish to Firefox Add-ons
98137
uses: browser-actions/release-firefox-addon@latest
99138
with:
100139
addon-id: ${{ secrets.FIREFOX_ADDON_ID }}
101-
addon-path: light-session-${{ steps.version.outputs.VERSION }}.zip
140+
addon-path: light-session-${{ steps.version.outputs.VERSION }}-firefox.zip
102141
auth-api-issuer: ${{ secrets.FIREFOX_API_ISSUER }}
103142
auth-api-secret: ${{ secrets.FIREFOX_API_SECRET }}
104143
release-note: "See release notes at https://github.com/11me/light-session/releases/tag/v${{ steps.version.outputs.VERSION }}"
144+
145+
# =========================================================================
146+
# Chrome Web Store Publishing
147+
# =========================================================================
148+
- name: Publish to Chrome Web Store
149+
uses: browser-actions/release-chrome-extension@latest
150+
with:
151+
extension-id: ${{ secrets.CHROME_EXTENSION_ID }}
152+
extension-path: light-session-${{ steps.version.outputs.VERSION }}-chrome.zip
153+
oauth-client-id: ${{ secrets.CHROME_CLIENT_ID }}
154+
oauth-client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
155+
oauth-refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ extension/.dev
4949
*.tmp
5050
*.temp
5151
.serena/
52+
extension/manifest.json

CLAUDE.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
55
## Build & Test Commands
66

77
```bash
8-
npm install # Install dependencies
9-
npm run build # Build extension (esbuild)
10-
npm run dev # Run in Firefox Developer Edition with auto-reload
11-
npm run test # Run unit tests (vitest)
12-
npm run test:watch # Run tests in watch mode
13-
npm run lint # ESLint check
14-
npm run lint:fix # ESLint autofix
15-
npm run build:types # TypeScript type check (no emit)
8+
npm install # Install dependencies
9+
npm run build # Build for Firefox (default)
10+
npm run build:firefox # Build for Firefox
11+
npm run build:chrome # Build for Chrome
12+
npm run dev # Run in Firefox Developer Edition
13+
npm run watch:chrome # Watch mode for Chrome development
14+
npm run test # Run unit tests (vitest)
15+
npm run lint # ESLint check
16+
npm run build:types # TypeScript type check
17+
npm run package # Package for Firefox (web-ext-artifacts/)
18+
npm run package:chrome # Package for Chrome (ZIP)
1619
```
1720

1821
## Architecture
1922

20-
**Firefox extension (Manifest V3)** that uses Fetch Proxy to trim ChatGPT conversations before React renders.
23+
**Cross-browser extension (Manifest V3)** for Firefox and Chrome that uses Fetch Proxy to trim ChatGPT conversations before React renders.
2124

2225
### Core Components
2326

@@ -48,22 +51,26 @@ content.ts → dispatches settings via CustomEvent → receives status updates
4851
### Message-Based Counting
4952

5053
ChatGPT creates multiple nodes per assistant response (especially with Extended Thinking).
51-
LightSession counts **messages** (role changes) instead of nodes:
54+
LightSession Pro counts **messages** (role changes) instead of nodes:
5255
- `[user, assistant, assistant, user, assistant]` = 4 messages
5356
- Consecutive same-role nodes are aggregated as ONE message
5457
- HIDDEN_ROLES: `system`, `tool`, `thinking` excluded from count
5558

5659
## Project Structure
5760

5861
```
59-
extension/src/
60-
├── page/ # Page script (Fetch Proxy, runs in page context)
61-
├── content/ # Content scripts (settings, status bar)
62-
├── background/ # Background service worker
63-
├── popup/ # Popup HTML/CSS/TS
64-
└── shared/ # Types, constants, storage, logger
65-
tests/ # Unit tests (vitest + happy-dom)
66-
build.cjs # esbuild build script (CommonJS)
62+
extension/
63+
├── manifest.json # Symlink → manifest.firefox.json (or chrome copy)
64+
├── manifest.firefox.json # Firefox-specific manifest
65+
├── manifest.chrome.json # Chrome-specific manifest
66+
└── src/
67+
├── page/ # Page script (Fetch Proxy, runs in page context)
68+
├── content/ # Content scripts (settings, status bar)
69+
├── background/ # Background service worker
70+
├── popup/ # Popup HTML/CSS/TS
71+
└── shared/ # Types, constants, storage, logger
72+
tests/ # Unit tests (vitest + happy-dom)
73+
build.cjs # esbuild build script (supports --target=firefox|chrome)
6774
```
6875

6976
## Conventions

PRIVACY.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Privacy Policy
2+
3+
**LightSession for ChatGPT** is a privacy-first browser extension. This policy explains what data the extension accesses and how it is handled.
4+
5+
## Data Collection
6+
7+
**We do not collect any data.**
8+
9+
LightSession operates entirely locally in your browser. It does not:
10+
11+
- Collect personal information
12+
- Track your browsing activity
13+
- Send data to external servers
14+
- Use analytics or telemetry
15+
- Store conversation content
16+
17+
## Permissions Explained
18+
19+
| Permission | Purpose |
20+
|------------|---------|
21+
| `storage` | Saves your preferences (message limit, UI settings) locally in your browser |
22+
| `tabs` | Detects when you navigate to ChatGPT to apply settings |
23+
| Host permissions (`chatgpt.com`, `chat.openai.com`) | Required to inject the performance optimization script on ChatGPT pages |
24+
25+
## How It Works
26+
27+
LightSession intercepts ChatGPT's API responses **locally in your browser** and trims the conversation data before React renders it. This keeps the UI fast without modifying your actual conversation on OpenAI's servers.
28+
29+
All processing happens entirely within your browser. No data ever leaves your device.
30+
31+
## Third Parties
32+
33+
This extension does not share any data with third parties because it does not collect any data.
34+
35+
## Open Source
36+
37+
LightSession is open source. You can review the code at:
38+
https://github.com/11me/light-session
39+
40+
## Contact
41+
42+
For privacy questions, open an issue on GitHub.
43+
44+
---
45+
46+
*Last updated: January 2026*

build.cjs

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#!/usr/bin/env node
22
/**
3-
* Build script for LightSession extension
3+
* Build script for LightSession Pro extension
44
* Bundles TypeScript → single JS files (no imports) for MV3 compatibility
55
*
66
* Usage:
7-
* node build.js - Development build (with sourcemaps)
8-
* node build.js --watch - Watch mode for development
9-
* NODE_ENV=production node build.js - Production build (minified, no sourcemaps)
7+
* node build.cjs - Development build for Firefox (default)
8+
* node build.cjs --target=firefox - Build for Firefox
9+
* node build.cjs --target=chrome - Build for Chrome
10+
* node build.cjs --watch - Watch mode for development
11+
* NODE_ENV=production node build.cjs - Production build (minified, no sourcemaps)
1012
*/
1113

1214
const esbuild = require('esbuild');
@@ -16,6 +18,38 @@ const path = require('path');
1618
const isWatch = process.argv.includes('--watch');
1719
const isProduction = process.env.NODE_ENV === 'production';
1820

21+
// Parse --target=firefox|chrome (default: firefox)
22+
const targetArg = process.argv.find((arg) => arg.startsWith('--target='));
23+
const target = targetArg ? targetArg.split('=')[1] : 'firefox';
24+
const validTargets = ['firefox', 'chrome'];
25+
if (!validTargets.includes(target)) {
26+
console.error(`❌ Invalid target: ${target}. Use: ${validTargets.join(', ')}`);
27+
process.exit(1);
28+
}
29+
30+
/**
31+
* Copy manifest for target browser
32+
*/
33+
function copyManifest() {
34+
const manifestSrc = `extension/manifest.${target}.json`;
35+
const manifestDest = 'extension/manifest.json';
36+
37+
// Always remove existing manifest.json first
38+
if (fs.existsSync(manifestDest)) {
39+
fs.unlinkSync(manifestDest);
40+
}
41+
42+
if (target === 'chrome') {
43+
// For Chrome, copy manifest.chrome.json
44+
fs.copyFileSync(manifestSrc, manifestDest);
45+
console.log(`✓ Copied manifest.${target}.json → manifest.json`);
46+
} else {
47+
// For Firefox, create symlink to manifest.firefox.json
48+
fs.symlinkSync('manifest.firefox.json', manifestDest);
49+
console.log('✓ Created symlink manifest.json → manifest.firefox.json');
50+
}
51+
}
52+
1953
/**
2054
* Copy static files from src to extension folder
2155
*/
@@ -33,6 +67,26 @@ function copyStaticFiles() {
3367
console.log('✓ Copied static files (popup.html, popup.css)');
3468
}
3569

70+
/**
71+
* Create or remove .dev marker file for development mode detection.
72+
* The popup checks for this file to show/hide debug options.
73+
*/
74+
function handleDevMarker() {
75+
const devMarkerPath = 'extension/.dev';
76+
77+
if (isProduction) {
78+
// Remove .dev marker in production
79+
if (fs.existsSync(devMarkerPath)) {
80+
fs.unlinkSync(devMarkerPath);
81+
console.log('✓ Removed .dev marker (production build)');
82+
}
83+
} else {
84+
// Create .dev marker in development
85+
fs.writeFileSync(devMarkerPath, 'Development build marker\n');
86+
console.log('✓ Created .dev marker (development build)');
87+
}
88+
}
89+
3690
const buildOptions = {
3791
bundle: true,
3892
format: 'iife',
@@ -52,7 +106,7 @@ const buildOptions = {
52106

53107
async function build() {
54108
const mode = isProduction ? 'production' : 'development';
55-
console.log(`🔧 Building in ${mode} mode${isProduction ? ' (minified)' : ' (with sourcemaps)'}...\n`);
109+
console.log(`🔧 Building for ${target.toUpperCase()} in ${mode} mode${isProduction ? ' (minified)' : ' (with sourcemaps)'}...\n`);
56110

57111
try {
58112
await esbuild.build({
@@ -91,16 +145,18 @@ async function build() {
91145
console.log('✓ Built popup script');
92146

93147
copyStaticFiles();
148+
copyManifest();
149+
handleDevMarker();
94150

95-
console.log(`\n✅ ${mode.charAt(0).toUpperCase() + mode.slice(1)} build complete! Extension ready for Firefox.`);
151+
console.log(`\n✅ ${mode.charAt(0).toUpperCase() + mode.slice(1)} build complete! Extension ready for ${target.charAt(0).toUpperCase() + target.slice(1)}.`);
96152
} catch (error) {
97153
console.error('❌ Build failed:', error);
98154
process.exit(1);
99155
}
100156
}
101157

102158
async function watch() {
103-
console.log('👀 Watch mode enabled. Watching for changes...\n');
159+
console.log(`👀 Watch mode enabled for ${target.toUpperCase()}. Watching for changes...\n`);
104160

105161
const contexts = await Promise.all([
106162
esbuild.context({
@@ -135,7 +191,9 @@ async function watch() {
135191
await ctx.rebuild();
136192
}
137193
copyStaticFiles();
138-
console.log('✅ Initial build complete.\n');
194+
copyManifest();
195+
handleDevMarker();
196+
console.log(`✅ Initial build complete for ${target.toUpperCase()}.\n`);
139197

140198
// Start watching
141199
for (const ctx of contexts) {

0 commit comments

Comments
 (0)