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
6 changes: 3 additions & 3 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{"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-3qq","title":"Set up CI for Chrome Web Store publishing","description":"Configure GitHub Actions workflow to build and publish extension to Chrome Web Store on release","status":"in_progress","priority":2,"issue_type":"task","created_at":"2026-01-09T22:58:36.99477884+03:00","created_by":"mayor","updated_at":"2026-01-09T23:01:15.306260186+03:00"}
{"id":"light-session-3qq","title":"Set up CI for Chrome Web Store publishing","description":"Configure GitHub Actions workflow to build and publish extension to Chrome Web Store on release","status":"in_progress","priority":2,"issue_type":"task","created_at":"2026-01-09T22:58:36.99477884+03:00","updated_at":"2026-01-09T23:01:15.306260186+03:00","created_by":"mayor"}
{"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-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","updated_at":"2026-01-09T22:40:24.864320247+03:00","created_by":"mayor"}
{"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 @@ -11,4 +11,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"}
{"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","updated_at":"2026-01-09T22:28:17.158771046+03:00","created_by":"mayor"}
2 changes: 1 addition & 1 deletion extension/manifest.chrome.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "LightSession Pro for ChatGPT",
"version": "1.6.1",
"version": "1.6.2",
"description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.",
"icons": {
"16": "icons/icon-16.png",
Expand Down
2 changes: 1 addition & 1 deletion extension/manifest.firefox.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "LightSession Pro for ChatGPT",
"version": "1.6.1",
"version": "1.6.2",
"description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.",
"icons": {
"16": "icons/icon-16.png",
Expand Down
9 changes: 8 additions & 1 deletion extension/src/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import browser from '../shared/browser-polyfill';
import type { LsSettings, TrimStatus } from '../shared/types';
import { loadSettings, validateSettings } from '../shared/storage';
import { loadSettings, validateSettings, syncToLocalStorage } from '../shared/storage';
import { TIMING } from '../shared/constants';
import { setDebugMode, logDebug, logInfo, logWarn, logError } from '../shared/logger';
import {
Expand Down Expand Up @@ -199,6 +199,10 @@ function handleStorageChange(
// Validate settings to ensure proper types and ranges
const newSettings = validateSettings(changes.ls_settings.newValue as Partial<LsSettings>);
logInfo('Settings changed via storage:', newSettings);

// Sync to localStorage for page-script access (Chrome MV3 workaround)
syncToLocalStorage(newSettings);

applySettings(newSettings);
}

Expand Down Expand Up @@ -296,6 +300,9 @@ async function initialize(): Promise<void> {
const settings = await loadSettings();
logInfo('Loaded settings:', settings);

// Sync to localStorage for page-script access (Chrome MV3 workaround)
syncToLocalStorage(settings);

// Apply settings
applySettings(settings);

Expand Down
21 changes: 13 additions & 8 deletions extension/src/content/page-inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,28 @@ const STORAGE_KEY = 'ls_settings';
const LOCAL_STORAGE_KEY = 'ls_config';

/**
* Sync settings from browser.storage to localStorage.
* This runs BEFORE page-script injection to ensure localStorage has correct data.
* Sync settings from browser.storage to localStorage AND dispatch CustomEvent.
* Runs in parallel with page-script injection. The CustomEvent signals config ready
* to page-script, allowing it to gate first fetch until config is available.
*/
async function syncSettingsToLocalStorage(): Promise<void> {
try {
const result = await browser.storage.local.get(STORAGE_KEY);
const stored = result[STORAGE_KEY] as { enabled?: boolean; keep?: number; debug?: boolean } | undefined;

if (stored) {
const config = {
enabled: stored.enabled ?? true,
limit: stored.keep ?? 10,
debug: stored.debug ?? false,
};
// Write to localStorage for page-script access
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(config));
// Dispatch event immediately - faster than waiting for content.ts (document_idle)
window.dispatchEvent(new CustomEvent('lightsession-config', { detail: JSON.stringify(config) }));
}
} catch {
// Storage access failed - page-script will use defaults or existing localStorage
// Storage access failed - page-script will use defaults after timeout
}
}

Expand All @@ -57,8 +61,9 @@ function injectPageScript(): void {
}

// Main execution:
// 1. Start syncing settings (async, but fast)
// 2. Inject page script immediately (can't wait - need to patch fetch early)
// The sync will complete and update localStorage, which page-script checks on each fetch.
void syncSettingsToLocalStorage();
// 1. Inject page script IMMEDIATELY to patch fetch before ChatGPT's code runs
// 2. Sync localStorage in parallel (best effort for first fetch)
// 3. content.ts will dispatch config via CustomEvent as fallback
// Priority is early patching - page-script uses defaults if localStorage not ready.
injectPageScript();
void syncSettingsToLocalStorage();
72 changes: 64 additions & 8 deletions extension/src/page/page-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,48 @@ const DEFAULT_CONFIG: LsConfig = {
debug: false,
};

// ============================================================================
// Config Ready Gating
// ============================================================================

/**
* Promise that resolves when config is ready (from localStorage or CustomEvent).
* First fetch waits on this to ensure correct config is used.
*/
let resolveConfigReady: (() => void) | null = null;
const configReady = new Promise<void>((resolve) => {
resolveConfigReady = resolve;
});

/**
* Resolve the configReady promise (idempotent - only resolves once).
*/
function tryResolveConfigReady(): void {
if (resolveConfigReady) {
resolveConfigReady();
resolveConfigReady = null;
}
}

/**
* Wait for config to be ready with timeout.
* Returns immediately if config already loaded.
* After timeout, marks config as ready to avoid repeated delays on subsequent fetches.
* @param timeoutMs Max time to wait (default 50ms)
*/
async function ensureConfigReady(timeoutMs = 50): Promise<void> {
if (!resolveConfigReady) {
// Already resolved
return;
}
await Promise.race([
configReady,
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
]);
// After timeout (or config arrived), mark as ready so subsequent fetches don't wait
tryResolveConfigReady();
}

/**
* localStorage key - must match storage.ts LOCAL_STORAGE_KEY
*/
Expand Down Expand Up @@ -197,13 +239,6 @@ async function interceptedFetch(
nativeFetch: typeof fetch,
...args: Parameters<typeof fetch>
): Promise<Response> {
const cfg = getConfig();

// Skip if disabled
if (!cfg.enabled) {
return nativeFetch(...args);
}

// Extract URL/method BEFORE fetching (handles string, URL, Request)
// This avoids "Body has already been consumed" error when args[0] is a Request
const [input, init] = args;
Expand All @@ -223,11 +258,21 @@ async function interceptedFetch(

const url = new URL(urlString, location.href);

// Early return for non-matching requests (before fetching)
// Early return for non-matching requests - no config wait needed
if (!isConversationRequest(method, url)) {
return nativeFetch(...args);
}

// Wait for config only for ChatGPT API requests (max 50ms on first request)
await ensureConfigReady();

const cfg = getConfig();

// Skip if disabled
if (!cfg.enabled) {
return nativeFetch(...args);
}

// Fetch and process matching requests
const res = await nativeFetch(...args);

Expand Down Expand Up @@ -355,6 +400,9 @@ function setupConfigListener(): void {
debug: config.debug ?? DEFAULT_CONFIG.debug,
};
log('Config updated:', window.__LS_CONFIG__);

// Signal that config is ready (unblocks first fetch)
tryResolveConfigReady();
}
}) as EventListener);
}
Expand All @@ -369,6 +417,14 @@ function setupConfigListener(): void {
window.__LS_DEBUG__ = false;
}

// Check localStorage first - if already synced by page-inject, resolve immediately
const stored = loadFromLocalStorage();
if (stored) {
window.__LS_CONFIG__ = stored;
window.__LS_DEBUG__ = stored.debug;
tryResolveConfigReady();
}

setupConfigListener();
patchFetch();

Expand Down
30 changes: 19 additions & 11 deletions extension/src/shared/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import browser from './browser-polyfill';
import type { RuntimeMessage, RuntimeResponse } from './types';
import type { RuntimeMessage, RuntimeResponse, ErrorResponse } from './types';
import { TIMING } from './constants';
import { logError } from './logger';

Expand All @@ -18,7 +18,11 @@ export async function sendMessageWithTimeout<T extends RuntimeResponse>(
message: RuntimeMessage,
timeoutMs: number = TIMING.MESSAGE_TIMEOUT_MS
): Promise<T> {
const isChrome = typeof chrome !== 'undefined' && typeof browser === 'undefined';
// Detect Chrome vs Firefox using feature detection
// Firefox has browser.runtime.getBrowserInfo() which Chrome doesn't have
// This is more reliable than checking global objects which can be polyfilled
const isFirefox = typeof browser.runtime.getBrowserInfo === 'function';
const isChrome = !isFirefox && typeof chrome !== 'undefined' && !!chrome.runtime;
const retryDelays = TIMING.MESSAGE_RETRY_DELAYS_MS;
let lastError: Error | undefined;

Expand All @@ -33,11 +37,8 @@ export async function sendMessageWithTimeout<T extends RuntimeResponse>(
]);

// Check Chrome lastError (set when no listener exists)
if (isChrome) {
const chromeLastError = (chrome as { runtime: { lastError?: { message?: string } } }).runtime.lastError;
if (chromeLastError) {
throw new Error(chromeLastError.message ?? 'Chrome runtime error');
}
if (isChrome && chrome.runtime.lastError) {
throw new Error(chrome.runtime.lastError.message ?? 'Chrome runtime error');
}

// Validate response is not undefined (Chrome returns undefined if service worker inactive)
Expand All @@ -50,12 +51,17 @@ export async function sendMessageWithTimeout<T extends RuntimeResponse>(
throw new Error('Service worker not responding - received undefined after retries');
}

// Check for error response from handler (don't retry real errors)
if ('error' in response && typeof response.error === 'string') {
throw new Error(`Handler error: ${response.error}`);
}

return response;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));

// Don't retry on timeout - it's already waited long enough
if (lastError.message === 'Message timeout') {
// Don't retry on timeout or handler errors - these are real failures
if (lastError.message === 'Message timeout' || lastError.message.startsWith('Handler error:')) {
throw lastError;
}

Expand Down Expand Up @@ -115,8 +121,10 @@ export function createMessageHandler(
sendResponse(response);
} catch (error) {
logError('Message handler error:', error);
// Send error response so caller doesn't hang
sendResponse({ error: String(error) } as unknown as RuntimeResponse);
// Send error response so caller doesn't hang waiting for response
// Caller must check for error field in response
const errorResponse: ErrorResponse = { error: String(error) };
sendResponse(errorResponse);
}
})();

Expand Down
20 changes: 9 additions & 11 deletions extension/src/shared/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export function validateSettings(input: Partial<LsSettings>): LsSettings {
* This eliminates race conditions on page load.
*/
export function syncToLocalStorage(settings: LsSettings): void {
// Guard: localStorage unavailable in service worker (Chrome MV3)
if (typeof localStorage === 'undefined') {
logDebug('localStorage not available (service worker context)');
return;
}

try {
const config = {
enabled: settings.enabled,
Expand All @@ -67,21 +73,15 @@ export async function loadSettings(): Promise<LsSettings> {

if (stored) {
logDebug('Loaded settings from storage:', stored);
const validated = validateSettings(stored);
syncToLocalStorage(validated);
return validated;
return validateSettings(stored);
}

// No stored settings, return defaults
logDebug('No stored settings found, using defaults');
const defaults = validateSettings({});
syncToLocalStorage(defaults);
return defaults;
return validateSettings({});
} catch (error) {
logError('Failed to load settings:', error);
const defaults = validateSettings({});
syncToLocalStorage(defaults);
return defaults;
return validateSettings({});
}
}

Expand All @@ -99,7 +99,6 @@ export async function updateSettings(updates: Partial<Omit<LsSettings, 'version'

// Save to storage
await browser.storage.local.set({ [STORAGE_KEY]: merged });
syncToLocalStorage(merged);

logDebug('Updated settings:', merged);
} catch (error) {
Expand All @@ -118,7 +117,6 @@ export async function initializeSettings(): Promise<void> {

if (!result[STORAGE_KEY]) {
await browser.storage.local.set({ [STORAGE_KEY]: DEFAULT_SETTINGS });
syncToLocalStorage(DEFAULT_SETTINGS);
logDebug('Initialized default settings');
}
} catch (error) {
Expand Down
9 changes: 8 additions & 1 deletion extension/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ export interface PongMessage {
timestamp: number;
}

/**
* Error response from message handler
*/
export interface ErrorResponse {
error: string;
}

/**
* Union of all runtime messages
*/
Expand All @@ -87,4 +94,4 @@ export type RuntimeMessage = GetSettingsMessage | SetSettingsMessage | PingMessa
/**
* Union of all runtime responses
*/
export type RuntimeResponse = GetSettingsResponse | SetSettingsResponse | PongMessage;
export type RuntimeResponse = GetSettingsResponse | SetSettingsResponse | PongMessage | ErrorResponse;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "light-session",
"version": "1.6.1",
"version": "1.6.2",
"type": "module",
"description": "LightSession Pro - Browser extension to optimize ChatGPT performance",
"engines": {
Expand Down