Skip to content

Commit ec8bd08

Browse files
committed
feat: migrate to Fetch Proxy architecture (v1.5.0)
Replace DOM manipulation with Fetch Proxy - intercept HTTP responses and trim conversation JSON BEFORE React renders. This eliminates flash of untrimmed content and simplifies the codebase significantly. Architecture change: - BEFORE: API → React renders ALL → MutationObserver → [TRIM DOM] - AFTER: API → [FETCH PROXY] → Trim JSON → React renders trimmed data Key changes: - Add page-script.ts: patches window.fetch to intercept /backend-api/ - Add page-inject.ts: injects page script at document_start - Simplify content.ts: now only handles settings and status bar - Count TURNS (role transitions) instead of individual nodes - Add HIDDEN_ROLES: system, tool, thinking excluded from count - Propagate debug flag to page script for diagnostics Deleted (no longer needed): - trimmer.ts, dom-helpers.ts, selectors.ts, observers.ts - compactor.ts, stream-detector.ts, early-hide.ts/css - Related test files Code reduction: ~2850 lines → ~500 lines
1 parent f89656f commit ec8bd08

20 files changed

+823
-4580
lines changed

CLAUDE.md

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,47 @@ npm run build:types # TypeScript type check (no emit)
1717

1818
## Architecture
1919

20-
**Firefox extension (Manifest V3)** that trims old DOM nodes from ChatGPT conversations to fix UI lag.
20+
**Firefox extension (Manifest V3)** that uses Fetch Proxy to trim ChatGPT conversations before React renders.
2121

2222
### Core Components
2323

2424
| Component | Path | Purpose |
2525
|-----------|------|---------|
26-
| Content Script | `extension/src/content/` | Injected into ChatGPT pages, handles trimming |
27-
| Background | `extension/src/background/` | Settings storage, message routing |
26+
| Page Script | `extension/src/page/` | Fetch Proxy, intercepts API responses |
27+
| Content Script | `extension/src/content/` | Settings dispatch, status bar UI |
28+
| Background | `extension/src/background/` | Settings storage |
2829
| Popup | `extension/src/popup/` | Extension toolbar UI |
2930
| Shared | `extension/src/shared/` | Types, constants, storage, logger |
3031

31-
### Content Script Flow
32+
### Fetch Proxy Flow
3233

3334
```
34-
content.ts (entry) → trimmer.ts (state machine) → dom-helpers.ts (find nodes)
35-
→ observers.ts (MutationObserver)
35+
page-inject.ts (document_start) → injects page-script.ts
36+
page-script.ts → patches window.fetch → intercepts /backend-api/ responses
37+
content.ts → dispatches settings via CustomEvent → receives status updates
3638
```
3739

38-
**State machine:** `IDLE ↔ OBSERVING` (two states, `trimScheduled` flag controls trimming)
39-
4040
**Trimming flow:**
41-
1. MutationObserver detects DOM changes (debounced 75ms)
42-
2. `evaluateTrim()` checks preconditions (enabled, not streaming, etc.)
43-
3. `buildActiveThread()` finds message nodes using tiered selectors
44-
4. `executeTrim()` removes excess nodes via `requestIdleCallback`
45-
46-
### Selector Strategy
41+
1. Page script intercepts GET `/backend-api/` JSON responses
42+
2. Parses conversation `mapping` and `current_node`
43+
3. Builds path from current_node to root via parent links
44+
4. Counts TURNS (role transitions), not individual nodes
45+
5. Keeps last N turns, filters to user/assistant only
46+
6. Returns modified Response with trimmed JSON
4747

48-
ChatGPT DOM selectors in `shared/constants.ts``SELECTOR_TIERS`:
49-
- **Tier A:** Data attributes (`[data-message-id]`)
50-
- **Tier B:** Test IDs and specific classes
51-
- **Tier C:** Structural fallbacks with heuristics
48+
### Turn-Based Counting
5249

53-
If ChatGPT UI changes, update selectors in `SELECTOR_TIERS`.
50+
ChatGPT creates multiple nodes per assistant response (especially with Extended Thinking).
51+
LightSession counts **turns** (role changes) instead of nodes:
52+
- `[user, assistant, assistant, user, assistant]` = 4 turns
53+
- HIDDEN_ROLES: `system`, `tool`, `thinking` excluded from count
5454

5555
## Project Structure
5656

5757
```
5858
extension/src/
59-
├── content/ # Content scripts (trimmer, observers, dom-helpers)
59+
├── page/ # Page script (Fetch Proxy, runs in page context)
60+
├── content/ # Content scripts (settings, status bar)
6061
├── background/ # Background service worker
6162
├── popup/ # Popup HTML/CSS/TS
6263
└── shared/ # Types, constants, storage, logger

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Local-only, privacy-first browser extension that fixes UI lag in long conversati
1313

1414
Long ChatGPT threads are brutal for the browser: the UI keeps every message in the DOM and the tab slowly turns into molasses — scroll becomes choppy, typing lags, devtools crawl.
1515

16-
**LightSession** fixes that by trimming old DOM nodes *on the client side* while keeping the actual conversation intact on OpenAI's side.
16+
**LightSession** fixes that by intercepting API responses and trimming conversation data *before* React renders it, keeping the actual conversation intact on OpenAI's side.
1717

1818
- **Fixes UI lag** in long chats
1919
- **Keeps model context intact** (only the DOM is trimmed)
@@ -35,10 +35,10 @@ Built after too many coding sessions where a single ChatGPT tab would start eati
3535

3636
**Performance**
3737

38-
- **Automatic trimming**keeps only the last _N_ messages visible (configurable range: 1–100 messages)
39-
- **DOM batching**node removals are batched within the ~16 ms budget for 60 fps scrolling
40-
- **Smart timing**waits for AI responses to fully finish streaming before trimming
41-
- **Ultra Lean Mode** _(Experimental)_ – aggressive optimizations: kills animations, applies CSS containment, dehighlights old code blocks
38+
- **Fetch Proxy**intercepts API responses and trims JSON before React renders (no flash of untrimmed content)
39+
- **Turn-based counting**counts conversation turns (user→assistant), not individual nodes, for accurate message limits
40+
- **Automatic trimming**keeps only the last _N_ conversation turns visible (configurable range: 1–100)
41+
- **Ultra Lean Mode** _(Experimental)_ – aggressive CSS optimizations: kills animations, applies containment
4242

4343
**User experience**
4444

@@ -148,14 +148,14 @@ LightSession uses a multi-tier selector strategy and conservative fallbacks, but
148148

149149
## 🔧 How it works
150150

151-
LightSession uses a non-destructive trimming pipeline:
151+
LightSession uses a **Fetch Proxy** architecture:
152152

153-
1. **Detection**finds ChatGPT message nodes with a multi-tier selector system
154-
(data attributes → test IDs → structural + heuristic fallback).
155-
2. **Classification**labels nodes as user / assistant / system / tool messages.
156-
3. **Calculation**determines which messages to keep based on your settings.
157-
4. **Batching** – removes excess nodes in small chunks using `requestIdleCallback` to stay within the frame budget.
158-
5. **Markers** – optionally leaves comment markers in the DOM for debugging.
153+
1. **Injection**at `document_start`, injects a script into the page context before ChatGPT loads.
154+
2. **Interception** – patches `window.fetch` to intercept `/backend-api/` JSON responses.
155+
3. **Trimming**parses the conversation mapping, counts turns (role transitions), keeps the last N turns.
156+
4. **Response**returns a modified Response with trimmed JSON; React renders only kept messages.
157+
158+
**Turn counting**: A "turn" is a contiguous sequence of messages from the same role. This matches how ChatGPT renders messages — multiple assistant nodes may render as a single bubble.
159159

160160
Trimming only affects what the browser renders. The conversation itself remains on OpenAI's side and is fully recoverable by reloading the page.
161161

@@ -208,7 +208,8 @@ npm run clean
208208
```
209209
extension/
210210
├── src/
211-
│ ├── content/ # Content scripts (run on ChatGPT pages)
211+
│ ├── content/ # Content scripts (settings dispatch, status bar)
212+
│ ├── page/ # Page script (Fetch Proxy, runs in page context)
212213
│ ├── background/ # Background script (settings management)
213214
│ ├── popup/ # Popup UI (HTML/CSS/JS)
214215
│ └── shared/ # Shared types, constants, utilities
@@ -219,10 +220,10 @@ extension/
219220

220221
### Architecture
221222

222-
- **State machine** for the trimmer: `IDLE ↔ OBSERVING` (simplified two-state design)
223-
- **Debounced MutationObserver** (~75ms) to batch DOM changes
224-
- **Idle callback** (`requestIdleCallback`) for non-blocking node removal
225-
- **Fail-safe thresholds** (e.g. minimum message count) to avoid over-trimming
223+
- **Fetch Proxy** – patches `window.fetch` in page context to intercept API responses
224+
- **Turn-based trimming** – counts role transitions, not individual nodes
225+
- **Content ↔ Page communication** – CustomEvents for settings dispatch and status updates
226+
- **HIDDEN_ROLES** – system, tool, thinking nodes excluded from turn count
226227

227228
---
228229

build.cjs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ function copyStaticFiles() {
2323
const filesToCopy = [
2424
{ src: 'extension/src/popup/popup.html', dest: 'extension/popup/popup.html' },
2525
{ src: 'extension/src/popup/popup.css', dest: 'extension/popup/popup.css' },
26-
{ src: 'extension/src/content/early-hide.css', dest: 'extension/dist/early-hide.css' },
2726
];
2827

2928
for (const { src, dest } of filesToCopy) {
3029
if (fs.existsSync(src)) {
3130
fs.copyFileSync(src, dest);
3231
}
3332
}
34-
console.log('✓ Copied static files (popup.html, popup.css, early-hide.css)');
33+
console.log('✓ Copied static files (popup.html, popup.css)');
3534
}
3635

3736
const buildOptions = {
@@ -65,10 +64,17 @@ async function build() {
6564

6665
await esbuild.build({
6766
...buildOptions,
68-
entryPoints: ['extension/src/content/early-hide.ts'],
69-
outfile: 'extension/dist/early-hide.js',
67+
entryPoints: ['extension/src/page/page-script.ts'],
68+
outfile: 'extension/dist/page-script.js',
7069
});
71-
console.log('✓ Built early-hide script');
70+
console.log('✓ Built page-script (fetch proxy)');
71+
72+
await esbuild.build({
73+
...buildOptions,
74+
entryPoints: ['extension/src/content/page-inject.ts'],
75+
outfile: 'extension/dist/page-inject.js',
76+
});
77+
console.log('✓ Built page-inject script');
7278

7379
await esbuild.build({
7480
...buildOptions,
@@ -104,8 +110,13 @@ async function watch() {
104110
}),
105111
esbuild.context({
106112
...buildOptions,
107-
entryPoints: ['extension/src/content/early-hide.ts'],
108-
outfile: 'extension/dist/early-hide.js',
113+
entryPoints: ['extension/src/page/page-script.ts'],
114+
outfile: 'extension/dist/page-script.js',
115+
}),
116+
esbuild.context({
117+
...buildOptions,
118+
entryPoints: ['extension/src/content/page-inject.ts'],
119+
outfile: 'extension/dist/page-inject.js',
109120
}),
110121
esbuild.context({
111122
...buildOptions,
@@ -135,7 +146,6 @@ async function watch() {
135146
const staticFiles = [
136147
'extension/src/popup/popup.html',
137148
'extension/src/popup/popup.css',
138-
'extension/src/content/early-hide.css',
139149
];
140150
for (const file of staticFiles) {
141151
fs.watchFile(file, { interval: 500 }, () => {

extension/manifest.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "LightSession for ChatGPT",
4-
"version": "1.4.0",
4+
"version": "1.5.0",
55
"description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.",
66
"icons": {
77
"16": "icons/icon-16.png",
@@ -24,8 +24,7 @@
2424
"*://chat.openai.com/*",
2525
"*://chatgpt.com/*"
2626
],
27-
"css": ["dist/early-hide.css"],
28-
"js": ["dist/early-hide.js"],
27+
"js": ["dist/page-inject.js"],
2928
"run_at": "document_start"
3029
},
3130
{
@@ -34,7 +33,7 @@
3433
"*://chatgpt.com/*"
3534
],
3635
"js": ["dist/content.js"],
37-
"run_at": "document_end"
36+
"run_at": "document_idle"
3837
}
3938
],
4039
"background": {
@@ -44,8 +43,8 @@
4443
},
4544
"web_accessible_resources": [
4645
{
47-
"resources": [".dev"],
48-
"matches": ["<all_urls>"]
46+
"resources": ["dist/page-script.js", ".dev"],
47+
"matches": ["*://chat.openai.com/*", "*://chatgpt.com/*"]
4948
}
5049
],
5150
"browser_specific_settings": {

0 commit comments

Comments
 (0)