Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{"id":"light-session-16d","title":"Implement code review recommendations","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-08T15:51:21.70403199+03:00","updated_at":"2026-01-08T15:54:39.953267761+03:00","closed_at":"2026-01-08T15:54:39.953267761+03:00","close_reason":"Closed"}
{"id":"light-session-2hd","title":"Test cache + navigation trigger for load more feature","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-09T00:46:45.660702419+03:00","updated_at":"2026-01-09T10:45:29.927755814+03:00","closed_at":"2026-01-09T10:45:29.927759601+03:00"}
{"id":"light-session-4ec","title":"Fix off-by-one error in turn counting - trimMapping","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T21:21:25.453075056+03:00","updated_at":"2026-01-07T21:23:59.887784624+03:00","closed_at":"2026-01-07T21:23:59.887784624+03:00","close_reason":"Closed"}
{"id":"light-session-67h","title":"Bug: trimming not working on page reload with extension enabled","description":"Settings show keep=5 but more messages visible. Race condition or localStorage sync issue.","status":"in_progress","priority":1,"issue_type":"task","created_at":"2026-01-09T22:39:11.800084105+03:00","created_by":"mayor","updated_at":"2026-01-09T22:40:24.864320247+03:00"}
{"id":"light-session-6sc","title":"Implement code review improvements","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T22:59:50.319980052+03:00","updated_at":"2026-01-07T23:06:39.948042561+03:00","closed_at":"2026-01-07T23:06:39.948042561+03:00","close_reason":"Closed"}
{"id":"light-session-7sq","title":"Rename 'turns' to 'messages' in UI","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-07T21:50:55.111515332+03:00","updated_at":"2026-01-07T21:53:42.151552035+03:00","closed_at":"2026-01-07T21:53:42.151552035+03:00","close_reason":"Closed"}
{"id":"light-session-8bq","title":"Fix 'Body has already been consumed' error in fetch interceptor","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T22:06:28.74686929+03:00","updated_at":"2026-01-07T22:07:29.911516018+03:00","closed_at":"2026-01-07T22:07:29.911516018+03:00","close_reason":"Fixed by extracting URL/method before nativeFetch"}
Expand All @@ -9,3 +10,4 @@
{"id":"light-session-hqc","title":"Fix Firefox Xray vision bug - cloneInto for CustomEvent detail","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T21:09:56.751185775+03:00","updated_at":"2026-01-07T21:12:56.968676465+03:00","closed_at":"2026-01-07T21:12:56.968676465+03:00","close_reason":"Closed"}
{"id":"light-session-kf1","title":"Fix ESLint error for cloneInto Firefox API","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T19:23:38.808971016+03:00","updated_at":"2026-01-08T19:24:30.315635209+03:00","closed_at":"2026-01-08T19:24:30.315635209+03:00","close_reason":"Closed"}
{"id":"light-session-oho","title":"Fix: empty user nodes counted but not rendered","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T22:21:42.141200219+03:00","updated_at":"2026-01-07T22:44:23.70359928+03:00","closed_at":"2026-01-07T22:44:23.70359928+03:00","close_reason":"Fixed: preserve original root node as tree anchor for ChatGPT"}
{"id":"light-session-q3p","title":"Fix race condition: sync settings to localStorage for page-script","status":"in_progress","priority":1,"issue_type":"task","created_at":"2026-01-09T22:28:10.93947054+03:00","created_by":"mayor","updated_at":"2026-01-09T22:28:17.158771046+03:00"}
71 changes: 61 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,45 @@ jobs:
- name: Type check
run: npm run build:types

- name: Build
run: npm run build

- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

- name: Create extension zip
# =========================================================================
# Firefox Build
# =========================================================================
- name: Build Firefox extension
run: npm run build:prod:firefox

- name: Create Firefox extension zip
run: |
cd extension
zip -r ../light-session-${{ steps.version.outputs.VERSION }}.zip \
zip -r ../light-session-${{ steps.version.outputs.VERSION }}-firefox.zip \
manifest.json \
dist/ \
popup/ \
icons/ \
-x "*.map"

# =========================================================================
# Chrome Build
# =========================================================================
- name: Build Chrome extension
run: npm run build:prod:chrome

- name: Create Chrome extension zip
run: |
cd extension
zip -r ../light-session-${{ steps.version.outputs.VERSION }}-chrome.zip \
manifest.json \
dist/ \
popup/ \
icons/ \
-x "*.map"

# =========================================================================
# Source Archive
# =========================================================================
- name: Create source zip
run: |
zip -r light-session-${{ steps.version.outputs.VERSION }}-source.zip \
Expand All @@ -68,15 +90,20 @@ jobs:
docs/ \
extension/src/ \
extension/icons/ \
extension/manifest.json \
extension/manifest.firefox.json \
extension/manifest.chrome.json \
tests/

# =========================================================================
# GitHub Release
# =========================================================================
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
light-session-${{ steps.version.outputs.VERSION }}.zip
light-session-${{ steps.version.outputs.VERSION }}-firefox.zip
light-session-${{ steps.version.outputs.VERSION }}-chrome.zip
light-session-${{ steps.version.outputs.VERSION }}-source.zip
body: |
## LightSession v${{ steps.version.outputs.VERSION }}
Expand All @@ -86,19 +113,43 @@ jobs:
**Firefox Add-ons (recommended):**
[Install from AMO](https://addons.mozilla.org/en-US/firefox/addon/lightsession-for-chatgpt/)

**Manual install:**
1. Download `light-session-${{ steps.version.outputs.VERSION }}.zip`
**Chrome Web Store:**
[Install from Chrome Web Store](https://chrome.google.com/webstore/detail/lightsession-for-chatgpt/${{ secrets.CHROME_EXTENSION_ID }})

**Manual install (Firefox):**
1. Download `light-session-${{ steps.version.outputs.VERSION }}-firefox.zip`
2. Open `about:debugging#/runtime/this-firefox` in Firefox
3. Click "Load Temporary Add-on"
4. Select the downloaded zip file

**Manual install (Chrome):**
1. Download `light-session-${{ steps.version.outputs.VERSION }}-chrome.zip`
2. Open `chrome://extensions` in Chrome
3. Enable "Developer mode"
4. Click "Load unpacked" and select the extracted folder

---

# =========================================================================
# Firefox Add-ons Publishing
# =========================================================================
- name: Publish to Firefox Add-ons
uses: browser-actions/release-firefox-addon@latest
with:
addon-id: ${{ secrets.FIREFOX_ADDON_ID }}
addon-path: light-session-${{ steps.version.outputs.VERSION }}.zip
addon-path: light-session-${{ steps.version.outputs.VERSION }}-firefox.zip
auth-api-issuer: ${{ secrets.FIREFOX_API_ISSUER }}
auth-api-secret: ${{ secrets.FIREFOX_API_SECRET }}
release-note: "See release notes at https://github.com/11me/light-session/releases/tag/v${{ steps.version.outputs.VERSION }}"

# =========================================================================
# Chrome Web Store Publishing
# =========================================================================
- name: Publish to Chrome Web Store
uses: browser-actions/release-chrome-extension@latest
with:
extension-id: ${{ secrets.CHROME_EXTENSION_ID }}
extension-path: light-session-${{ steps.version.outputs.VERSION }}-chrome.zip
oauth-client-id: ${{ secrets.CHROME_CLIENT_ID }}
oauth-client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
oauth-refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ extension/.dev
*.tmp
*.temp
.serena/
extension/manifest.json
43 changes: 25 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build & Test Commands

```bash
npm install # Install dependencies
npm run build # Build extension (esbuild)
npm run dev # Run in Firefox Developer Edition with auto-reload
npm run test # Run unit tests (vitest)
npm run test:watch # Run tests in watch mode
npm run lint # ESLint check
npm run lint:fix # ESLint autofix
npm run build:types # TypeScript type check (no emit)
npm install # Install dependencies
npm run build # Build for Firefox (default)
npm run build:firefox # Build for Firefox
npm run build:chrome # Build for Chrome
npm run dev # Run in Firefox Developer Edition
npm run watch:chrome # Watch mode for Chrome development
npm run test # Run unit tests (vitest)
npm run lint # ESLint check
npm run build:types # TypeScript type check
npm run package # Package for Firefox (web-ext-artifacts/)
npm run package:chrome # Package for Chrome (ZIP)
```

## Architecture

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

### Core Components

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

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

## Project Structure

```
extension/src/
├── page/ # Page script (Fetch Proxy, runs in page context)
├── content/ # Content scripts (settings, status bar)
├── background/ # Background service worker
├── popup/ # Popup HTML/CSS/TS
└── shared/ # Types, constants, storage, logger
tests/ # Unit tests (vitest + happy-dom)
build.cjs # esbuild build script (CommonJS)
extension/
├── manifest.json # Symlink → manifest.firefox.json (or chrome copy)
├── manifest.firefox.json # Firefox-specific manifest
├── manifest.chrome.json # Chrome-specific manifest
└── src/
├── page/ # Page script (Fetch Proxy, runs in page context)
├── content/ # Content scripts (settings, status bar)
├── background/ # Background service worker
├── popup/ # Popup HTML/CSS/TS
└── shared/ # Types, constants, storage, logger
tests/ # Unit tests (vitest + happy-dom)
build.cjs # esbuild build script (supports --target=firefox|chrome)
```

## Conventions
Expand Down
46 changes: 46 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Privacy Policy

**LightSession for ChatGPT** is a privacy-first browser extension. This policy explains what data the extension accesses and how it is handled.

## Data Collection

**We do not collect any data.**

LightSession operates entirely locally in your browser. It does not:

- Collect personal information
- Track your browsing activity
- Send data to external servers
- Use analytics or telemetry
- Store conversation content

## Permissions Explained

| Permission | Purpose |
|------------|---------|
| `storage` | Saves your preferences (message limit, UI settings) locally in your browser |
| `tabs` | Detects when you navigate to ChatGPT to apply settings |
| Host permissions (`chatgpt.com`, `chat.openai.com`) | Required to inject the performance optimization script on ChatGPT pages |

## How It Works

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.

All processing happens entirely within your browser. No data ever leaves your device.

## Third Parties

This extension does not share any data with third parties because it does not collect any data.

## Open Source

LightSession is open source. You can review the code at:
https://github.com/11me/light-session

## Contact

For privacy questions, open an issue on GitHub.

---

*Last updated: January 2026*
74 changes: 66 additions & 8 deletions build.cjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#!/usr/bin/env node
/**
* Build script for LightSession extension
* Build script for LightSession Pro extension
* Bundles TypeScript → single JS files (no imports) for MV3 compatibility
*
* Usage:
* node build.js - Development build (with sourcemaps)
* node build.js --watch - Watch mode for development
* NODE_ENV=production node build.js - Production build (minified, no sourcemaps)
* node build.cjs - Development build for Firefox (default)
* node build.cjs --target=firefox - Build for Firefox
* node build.cjs --target=chrome - Build for Chrome
* node build.cjs --watch - Watch mode for development
* NODE_ENV=production node build.cjs - Production build (minified, no sourcemaps)
*/

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

// Parse --target=firefox|chrome (default: firefox)
const targetArg = process.argv.find((arg) => arg.startsWith('--target='));
const target = targetArg ? targetArg.split('=')[1] : 'firefox';
const validTargets = ['firefox', 'chrome'];
if (!validTargets.includes(target)) {
console.error(`❌ Invalid target: ${target}. Use: ${validTargets.join(', ')}`);
process.exit(1);
}

/**
* Copy manifest for target browser
*/
function copyManifest() {
const manifestSrc = `extension/manifest.${target}.json`;
const manifestDest = 'extension/manifest.json';

// Always remove existing manifest.json first
if (fs.existsSync(manifestDest)) {
fs.unlinkSync(manifestDest);
}

if (target === 'chrome') {
// For Chrome, copy manifest.chrome.json
fs.copyFileSync(manifestSrc, manifestDest);
console.log(`✓ Copied manifest.${target}.json → manifest.json`);
} else {
// For Firefox, create symlink to manifest.firefox.json
fs.symlinkSync('manifest.firefox.json', manifestDest);
console.log('✓ Created symlink manifest.json → manifest.firefox.json');
}
}

/**
* Copy static files from src to extension folder
*/
Expand All @@ -33,6 +67,26 @@ function copyStaticFiles() {
console.log('✓ Copied static files (popup.html, popup.css)');
}

/**
* Create or remove .dev marker file for development mode detection.
* The popup checks for this file to show/hide debug options.
*/
function handleDevMarker() {
const devMarkerPath = 'extension/.dev';

if (isProduction) {
// Remove .dev marker in production
if (fs.existsSync(devMarkerPath)) {
fs.unlinkSync(devMarkerPath);
console.log('✓ Removed .dev marker (production build)');
}
} else {
// Create .dev marker in development
fs.writeFileSync(devMarkerPath, 'Development build marker\n');
console.log('✓ Created .dev marker (development build)');
}
}

const buildOptions = {
bundle: true,
format: 'iife',
Expand All @@ -52,7 +106,7 @@ const buildOptions = {

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

try {
await esbuild.build({
Expand Down Expand Up @@ -91,16 +145,18 @@ async function build() {
console.log('✓ Built popup script');

copyStaticFiles();
copyManifest();
handleDevMarker();

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

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

const contexts = await Promise.all([
esbuild.context({
Expand Down Expand Up @@ -135,7 +191,9 @@ async function watch() {
await ctx.rebuild();
}
copyStaticFiles();
console.log('✅ Initial build complete.\n');
copyManifest();
handleDevMarker();
console.log(`✅ Initial build complete for ${target.toUpperCase()}.\n`);

// Start watching
for (const ctx of contexts) {
Expand Down
Loading