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: 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.7.3",
"version": "1.7.4",
"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.7.3",
"version": "1.7.4",
"description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.",
"icons": {
"16": "icons/icon-16.png",
Expand Down
16 changes: 16 additions & 0 deletions extension/src/content/chat-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ export function hasConversationTurns(root: ParentNode): boolean {
return false;
}

export function countConversationTurns(root: ParentNode): number {
const main = root.querySelector('main');
if (!main) {
return 0;
}

for (const selector of ['[data-message-author-role]', '[data-message-id]', '[data-testid="conversation-turn"]', 'article']) {
const count = main.querySelectorAll(selector).length;
if (count > 0) {
return count;
}
}

return 0;
}

export function isEmptyChatView(root: ParentNode): boolean {
const main = root.querySelector('main');
if (!main) {
Expand Down
55 changes: 49 additions & 6 deletions extension/src/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import {
updateStatusBar,
resetAccumulatedTrimmed,
refreshStatusBar,
showBootstrapStatus,
setStatusBarVisibility,
} from './status-bar';
import { isEmptyChatView } from './chat-view';
import { countConversationTurns, isEmptyChatView } from './chat-view';
import { installUserCollapse, type UserCollapseController } from './user-collapse';
import { isLightSessionRejection } from './rejection-filter';
import { isProxyReadySatisfied } from '../shared/proxy-ready';
import { extractConversationPageId } from '../shared/url';


// ============================================================================
Expand Down Expand Up @@ -60,6 +63,8 @@ let emptyChatState = false;
let emptyChatCheckTimer: number | null = null;
let emptyChatObserver: MutationObserver | null = null;
let userCollapse: UserCollapseController | null = null;
let hasAuthoritativeStatus = false;
let requestedBootstrapSyncConversationId: string | null = null;

// ============================================================================
// Page Script Communication
Expand Down Expand Up @@ -110,6 +115,8 @@ function handleTrimStatus(event: CustomEvent<unknown>): void {
}

logDebug('Received trim status:', status);
hasAuthoritativeStatus = true;
requestedBootstrapSyncConversationId = extractConversationPageId(location.href);

// Convert page script status format to status bar format
updateStatusBar({
Expand Down Expand Up @@ -139,7 +146,7 @@ function handleProxyReady(): void {
* Shows a warning in the status bar if not.
*/
function checkProxyStatus(): void {
if (!proxyReady) {
if (!isProxyReadySatisfied(proxyReady)) {
logWarn('Fetch proxy did not signal ready within timeout');
// Don't show warning to user - proxy may still work, just didn't send ready message
}
Expand Down Expand Up @@ -250,8 +257,11 @@ function setupNavigationDetection(): void {
lastUrl = location.href;

logDebug(`${source} navigation:`, lastUrl);
hasAuthoritativeStatus = false;
requestedBootstrapSyncConversationId = null;
resetAccumulatedTrimmed();
refreshStatusBar();
scheduleEmptyChatCheck();

// Re-bind DOM observers for per-chat containers (SPA navigation can replace the message list DOM).
// Make the settings intent explicit; userCollapse being non-null is an implementation detail.
Expand Down Expand Up @@ -301,11 +311,44 @@ function setupNavigationDetection(): void {

function checkEmptyChatView(): void {
const isEmpty = isEmptyChatView(document);
if (isEmpty && !emptyChatState) {
resetAccumulatedTrimmed();
refreshStatusBar();
if (isEmpty) {
if (!emptyChatState) {
hasAuthoritativeStatus = false;
resetAccumulatedTrimmed();
refreshStatusBar();
}
emptyChatState = true;
return;
}

emptyChatState = false;

if (!currentSettings?.enabled || hasAuthoritativeStatus) {
return;
}

const conversationId = extractConversationPageId(location.href);
if (conversationId && conversationId !== requestedBootstrapSyncConversationId) {
requestedBootstrapSyncConversationId = conversationId;
window.dispatchEvent(
new CustomEvent('lightsession-bootstrap-sync', {
detail: JSON.stringify({ conversationId }),
})
);
}

const visibleTurns = countConversationTurns(document);
if (visibleTurns > 0 && visibleTurns <= currentSettings.keep) {
updateStatusBar({
totalMessages: visibleTurns,
visibleMessages: visibleTurns,
trimmedMessages: 0,
keepLastN: currentSettings.keep,
});
return;
}
emptyChatState = isEmpty;

showBootstrapStatus();
}

function scheduleEmptyChatCheck(): void {
Expand Down
34 changes: 33 additions & 1 deletion extension/src/content/status-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TIMING } from '../shared/constants';

const STATUS_BAR_ID = 'lightsession-status-bar';
const WAITING_TEXT = 'LightSession · waiting for messages…';
const PENDING_TEXT = 'LightSession · sync pending…';

export interface StatusBarStats {
totalMessages: number;
Expand All @@ -15,7 +16,7 @@ export interface StatusBarStats {
keepLastN: number;
}

type StatusBarState = 'active' | 'waiting' | 'all-visible' | 'unrecognized';
type StatusBarState = 'active' | 'waiting' | 'pending' | 'all-visible' | 'unrecognized';

let currentStats: StatusBarStats | null = null;
let isVisible = true;
Expand Down Expand Up @@ -124,6 +125,11 @@ function applyStateStyles(bar: HTMLElement, state: StatusBarState): void {
case 'waiting':
bar.style.color = '#9ca3af';
break;
case 'pending':
bar.style.color = '#bfdbfe';
bar.style.backgroundColor = 'rgba(30, 41, 59, 0.92)';
bar.style.borderColor = 'rgba(96, 165, 250, 0.45)';
break;
case 'all-visible':
// Keep neutral styling
break;
Expand Down Expand Up @@ -169,6 +175,12 @@ function renderWaitingStatusBar(bar: HTMLElement): void {
lastUpdateTime = performance.now();
}

function renderPendingStatusBar(bar: HTMLElement): void {
bar.textContent = PENDING_TEXT;
applyStateStyles(bar, 'pending');
lastUpdateTime = performance.now();
}

/**
* Update the status bar with new stats (throttled, with change detection)
*/
Expand Down Expand Up @@ -311,6 +323,26 @@ export function resetAccumulatedTrimmed(): void {
renderWaitingStatusBar(bar);
}

export function showBootstrapStatus(): void {
currentStats = null;
pendingStats = null;
if (pendingUpdateTimer !== null) {
clearTimeout(pendingUpdateTimer);
pendingUpdateTimer = null;
}

if (!isVisible) {
return;
}

const bar = getOrCreateStatusBar();
if (!bar) {
return;
}

renderPendingStatusBar(bar);
}

/**
* Refresh status bar after SPA navigation or DOM resets
*/
Expand Down
96 changes: 95 additions & 1 deletion extension/src/page/page-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
trimMapping,
type ConversationData,
} from '../shared/trimmer';
import { TIMING } from '../shared/constants';
import { markProxyReady } from '../shared/proxy-ready';
import type { TrimStatus } from '../shared/types';

// ============================================================================
Expand All @@ -39,6 +41,7 @@ declare global {
__LS_CONFIG__?: LsConfig;
__LS_PROXY_PATCHED__?: boolean;
__LS_DEBUG__?: boolean;
__LS_BOOTSTRAP_SYNC_LISTENER__?: boolean;
}
}

Expand Down Expand Up @@ -93,6 +96,8 @@ async function ensureConfigReady(timeoutMs = 50): Promise<void> {
let configReceived = false;
const CONFIG_FALLBACK_TIMEOUT_MS = 2000;
const configStartTime = Date.now();
const completedBootstrapSyncIds = new Set<string>();
const inFlightBootstrapSyncIds = new Set<string>();

/**
* localStorage key - must match storage.ts LOCAL_STORAGE_KEY
Expand Down Expand Up @@ -146,6 +151,63 @@ function dispatchStatus(status: TrimStatus): void {
);
}

function extractConversationRequestId(url: URL): string | null {
const match = url.pathname.match(
/^\/backend-api\/(?:conversation|shared_conversation)\/([^/]+)\/?$/
);
return match?.[1] ?? null;
}

function looksLikeConversationData(
json: ConversationData | null
): json is ConversationData & {
mapping: NonNullable<ConversationData['mapping']>;
current_node: string;
} {
return !!json && typeof json === 'object' && !!json.mapping && typeof json.current_node === 'string';
}

async function attemptAuthoritativeConversationSync(conversationId: string): Promise<void> {
if (
!conversationId ||
completedBootstrapSyncIds.has(conversationId) ||
inFlightBootstrapSyncIds.has(conversationId)
) {
return;
}

inFlightBootstrapSyncIds.add(conversationId);

try {
for (const delayMs of TIMING.NEW_CHAT_SYNC_RETRY_DELAYS_MS) {
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));

if (completedBootstrapSyncIds.has(conversationId)) {
return;
}

try {
const response = await window.fetch(
`/backend-api/conversation/${encodeURIComponent(conversationId)}`
);
if (!response.ok || !isJsonResponse(response)) {
continue;
}

const json = (await response.clone().json().catch(() => null)) as ConversationData | null;
if (looksLikeConversationData(json)) {
completedBootstrapSyncIds.add(conversationId);
return;
}
} catch (error) {
log('Bootstrap authoritative sync attempt failed:', error);
}
}
} finally {
inFlightBootstrapSyncIds.delete(conversationId);
}
}

// ============================================================================
// Fetch Proxy
// ============================================================================
Expand Down Expand Up @@ -312,10 +374,15 @@ async function interceptedFetch(
}

// Check if this looks like conversation data
if (!json.mapping || !json.current_node) {
if (!looksLikeConversationData(json)) {
return res;
}

const requestConversationId = extractConversationRequestId(url);
if (requestConversationId) {
completedBootstrapSyncIds.add(requestConversationId);
}

// Trim the mapping
const trimmed = trimMapping(json, cfg.limit);

Expand Down Expand Up @@ -396,6 +463,7 @@ function patchFetch(): void {

window.__LS_PROXY_PATCHED__ = true;
log('Fetch proxy installed');
markProxyReady();

// Notify content script that proxy is ready (use origin for security)
window.postMessage({ type: 'lightsession-proxy-ready' }, location.origin);
Expand Down Expand Up @@ -446,6 +514,31 @@ function setupConfigListener(): void {
}) as EventListener);
}

function setupBootstrapSyncListener(): void {
if (window.__LS_BOOTSTRAP_SYNC_LISTENER__) {
return;
}
window.__LS_BOOTSTRAP_SYNC_LISTENER__ = true;

window.addEventListener('lightsession-bootstrap-sync', ((event: CustomEvent<string>) => {
if (typeof event.detail !== 'string') {
return;
}

try {
const parsed = JSON.parse(event.detail) as { conversationId?: string };
const conversationId = parsed.conversationId?.trim();
if (!conversationId) {
return;
}

void attemptAuthoritativeConversationSync(conversationId);
} catch {
// Ignore malformed bootstrap sync events
}
}) as EventListener);
}

// ============================================================================
// Entry Point
// ============================================================================
Expand All @@ -465,6 +558,7 @@ function setupConfigListener(): void {
}

setupConfigListener();
setupBootstrapSyncListener();
patchFetch();

log('Fetch Proxy loaded');
Expand Down
5 changes: 5 additions & 0 deletions extension/src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export const TIMING = {
* - Status bar is informational, doesn't need real-time updates
*/
STATUS_BAR_THROTTLE_MS: 500,

/**
* Delays before bounded authoritative-sync retries for a new chat (ms).
*/
NEW_CHAT_SYNC_RETRY_DELAYS_MS: [800, 1600, 3200] as const,
} as const;

// ============================================================================
Expand Down
Loading
Loading