From 5acdcdf7e6b0ee369d0fa76731dc8b2412808fd9 Mon Sep 17 00:00:00 2001 From: Felix Lu Date: Thu, 28 Nov 2024 23:52:41 +0800 Subject: [PATCH 1/2] refactor: Implement feature management architecture - Add ExtensionManager for high-level orchestration - Create FeatureManager for feature lifecycle - Convert TCThinkingBlock to new architecture - Add MutationObserverService with configurable options - Remove singleton usage - Improve code organization and modularity --- extensions/chrome/src/content/index.ts | 14 +--- .../src/content/v3/features/base-feature.ts | 28 ++++++++ .../v3/features/thinking-block/index.ts | 34 +++++++++- ...ck-toggle.ts => process-thinking-block.ts} | 8 +-- .../content/v3/managers/extension-manager.ts | 65 +++++++++++++++++++ .../content/v3/managers/feature-manager.ts | 56 ++++++++++++++++ .../chrome/src/services/mutation-observer.ts | 46 +++++++++++-- extensions/chrome/src/types/index.ts | 7 ++ 8 files changed, 233 insertions(+), 25 deletions(-) create mode 100644 extensions/chrome/src/content/v3/features/base-feature.ts rename extensions/chrome/src/content/v3/features/thinking-block/{thinking-block-toggle.ts => process-thinking-block.ts} (77%) create mode 100644 extensions/chrome/src/content/v3/managers/extension-manager.ts create mode 100644 extensions/chrome/src/content/v3/managers/feature-manager.ts create mode 100644 extensions/chrome/src/types/index.ts diff --git a/extensions/chrome/src/content/index.ts b/extensions/chrome/src/content/index.ts index 167eb16..bd2e857 100644 --- a/extensions/chrome/src/content/index.ts +++ b/extensions/chrome/src/content/index.ts @@ -1,14 +1,6 @@ import "@/styles/globals.css" -import { shouldInitialize } from "@/utils/url-utils" +import { ExtensionManager } from "./v3/managers/extension-manager" -import { addThinkingBlockToggle } from "./v3/features/thinking-block" - -// Only initialize on appropriate pages -if (shouldInitialize(window.location.href)) { - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", addThinkingBlockToggle) - } else { - addThinkingBlockToggle() - } -} +const extensionManager = new ExtensionManager() +extensionManager.initialize() diff --git a/extensions/chrome/src/content/v3/features/base-feature.ts b/extensions/chrome/src/content/v3/features/base-feature.ts new file mode 100644 index 0000000..9480f31 --- /dev/null +++ b/extensions/chrome/src/content/v3/features/base-feature.ts @@ -0,0 +1,28 @@ +import { Feature } from "@/types" + +/** + * Base abstract class for features + * Provides common functionality and enforces feature contract + */ +export abstract class BaseFeature implements Feature { + constructor(readonly id: string) {} + + /** + * Initialize the feature + * @returns cleanup function if needed + */ + abstract initialize(): void | (() => void) + + /** + * Helper method to safely add event listeners with automatic cleanup + */ + protected addEventListenerWithCleanup( + element: Element, + event: string, + handler: EventListener, + options?: boolean | AddEventListenerOptions + ): () => void { + element.addEventListener(event, handler, options) + return () => element.removeEventListener(event, handler, options) + } +} diff --git a/extensions/chrome/src/content/v3/features/thinking-block/index.ts b/extensions/chrome/src/content/v3/features/thinking-block/index.ts index cfadc24..368b1b8 100644 --- a/extensions/chrome/src/content/v3/features/thinking-block/index.ts +++ b/extensions/chrome/src/content/v3/features/thinking-block/index.ts @@ -1 +1,33 @@ -export { addThinkingBlockToggle } from "./thinking-block-toggle" +import type { MutationObserverService } from "@/services/mutation-observer" + +import { BaseFeature } from "../base-feature" +import { processThinkingBlocks } from "./process-thinking-block" + +/** + * Feature that adds toggle functionality to thinking blocks in the UI + * Manages the collapse/expand and copy functionality for code blocks + */ +export class TCThinkingBlock extends BaseFeature { + /** + * @param mutationObserver - Service to observe DOM changes for thinking blocks + */ + constructor(private mutationObserver: MutationObserverService) { + super("tc-thinking-block") + } + + /** + * Initialize the thinking block feature + * Sets up mutation observer to watch for new thinking blocks + * @returns Cleanup function to unsubscribe from mutation observer + */ + initialize(): void | (() => void) { + this.mutationObserver.initialize() + + const unsubscribe = this.mutationObserver.subscribe(processThinkingBlocks) + + return () => { + // Unsubscribe from mutation observer + unsubscribe() + } + } +} diff --git a/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts b/extensions/chrome/src/content/v3/features/thinking-block/process-thinking-block.ts similarity index 77% rename from extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts rename to extensions/chrome/src/content/v3/features/thinking-block/process-thinking-block.ts index 1b748b6..775b743 100644 --- a/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts +++ b/extensions/chrome/src/content/v3/features/thinking-block/process-thinking-block.ts @@ -1,14 +1,8 @@ import { THINKING_BLOCK_CONTROLS_SELECTORS } from "@/selectors" -import { mutationObserver } from "@/services/mutation-observer" import { setupControls } from "./setup-controls" -export function addThinkingBlockToggle() { - mutationObserver.initialize() - return mutationObserver.subscribe(processThinkingBlocks) -} - -function processThinkingBlocks() { +export function processThinkingBlocks() { const thinkingBlockControls = document.querySelectorAll( THINKING_BLOCK_CONTROLS_SELECTORS ) diff --git a/extensions/chrome/src/content/v3/managers/extension-manager.ts b/extensions/chrome/src/content/v3/managers/extension-manager.ts new file mode 100644 index 0000000..7508204 --- /dev/null +++ b/extensions/chrome/src/content/v3/managers/extension-manager.ts @@ -0,0 +1,65 @@ +import { MutationObserverService } from "@/services/mutation-observer" +import { shouldInitialize } from "@/utils/url-utils" + +import { TCThinkingBlock } from "../features/thinking-block" +import { FeatureManager } from "./feature-manager" + +/** + * Manages the lifecycle and coordination of all extension features and services + */ +export class ExtensionManager { + private featureManager: FeatureManager + private mutationObserver: MutationObserverService + + constructor() { + this.mutationObserver = new MutationObserverService() + this.featureManager = new FeatureManager() + + this.registerFeatures() + this.setupNavigationListener() + } + + /** + * Register all extension features + */ + private registerFeatures(): void { + // Register features with their required services + this.featureManager.register(new TCThinkingBlock(this.mutationObserver)) + // Add more features here + } + + /** + * Initialize the extension if conditions are met + */ + initialize(): void { + if (!shouldInitialize(window.location.href)) { + return + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + this.featureManager.initialize() + }) + } else { + this.featureManager.initialize() + } + } + + /** + * Set up listener for navigation events + */ + private setupNavigationListener(): void { + chrome.runtime.onMessage.addListener((message) => { + if (message.type === "NAVIGATION") { + this.featureManager.cleanup() + } + }) + } + + /** + * Clean up all features and services + */ + cleanup(): void { + this.featureManager.cleanup() + } +} diff --git a/extensions/chrome/src/content/v3/managers/feature-manager.ts b/extensions/chrome/src/content/v3/managers/feature-manager.ts new file mode 100644 index 0000000..3df0ac5 --- /dev/null +++ b/extensions/chrome/src/content/v3/managers/feature-manager.ts @@ -0,0 +1,56 @@ +import { Feature } from "@/types" + +export class FeatureManager { + private features = new Map() + private cleanupFunctions = new Map void>() + + /** + * Register a new feature + * @param feature Feature instance to register + * @throws Error if feature with same id already exists + */ + register(feature: Feature): void { + if (this.features.has(feature.id)) { + throw new Error(`Feature with id ${feature.id} already exists`) + } + this.features.set(feature.id, feature) + } + + /** + * Initialize all registered features + */ + initialize(): void { + this.features.forEach((feature, id) => { + try { + const cleanup = feature.initialize() + if (cleanup) { + this.cleanupFunctions.set(id, cleanup) + } + } catch (error) { + console.error(`Failed to initialize feature ${id}:`, error) + } + }) + } + + /** + * Clean up all features + */ + cleanup(): void { + this.cleanupFunctions.forEach((cleanup, id) => { + try { + cleanup() + } catch (error) { + console.error(`Failed to cleanup feature ${id}:`, error) + } + }) + this.cleanupFunctions.clear() + this.features.clear() + } + + /** + * Get a registered feature by id + */ + getFeature(id: string): Feature | undefined { + return this.features.get(id) + } +} diff --git a/extensions/chrome/src/services/mutation-observer.ts b/extensions/chrome/src/services/mutation-observer.ts index efc8c29..0380e71 100644 --- a/extensions/chrome/src/services/mutation-observer.ts +++ b/extensions/chrome/src/services/mutation-observer.ts @@ -1,10 +1,29 @@ type ObserverCallback = () => void -class MutationObserverService { +export interface MutationObserverOptions { + childList?: boolean + subtree?: boolean + attributes?: boolean + characterData?: boolean + debounceTime?: number +} + +export class MutationObserverService { private observer: MutationObserver | null = null private callbacks: Set = new Set() private timeouts: Map = new Map() private isProcessing = false + private options: MutationObserverOptions + + constructor( + options: MutationObserverOptions = { + childList: true, + subtree: true, + debounceTime: 200, + } + ) { + this.options = options + } initialize() { if (this.observer) return @@ -22,18 +41,35 @@ class MutationObserverService { const timeout = setTimeout(() => { callback() this.isProcessing = false - }, 200) // Slightly increased debounce time + }, this.options.debounceTime) this.timeouts.set(callback, timeout) }) }) this.observer.observe(document.body, { - childList: true, - subtree: true, + childList: this.options.childList, + subtree: this.options.subtree, + attributes: this.options.attributes, + characterData: this.options.characterData, }) } + /* service-level cleanup but we don't usually need this */ + cleanup() { + // 1. Disconnect the MutationObserver + this.observer?.disconnect() + // 2. Clear the observer reference + this.observer = null + // 3. Clear all pending timeouts + this.timeouts.forEach((timeout) => clearTimeout(timeout)) + this.timeouts.clear() + // 4. Clear all callbacks + this.callbacks.clear() + // 5. Reset processing flag + this.isProcessing = false + } + subscribe(callback: ObserverCallback) { this.callbacks.add(callback) return () => this.unsubscribe(callback) @@ -48,5 +84,3 @@ class MutationObserverService { } } } - -export const mutationObserver = new MutationObserverService() diff --git a/extensions/chrome/src/types/index.ts b/extensions/chrome/src/types/index.ts new file mode 100644 index 0000000..7b4da1c --- /dev/null +++ b/extensions/chrome/src/types/index.ts @@ -0,0 +1,7 @@ +/** + * Base interface for all features + */ +export interface Feature { + id: string + initialize(): void | (() => void) // Return cleanup function if needed +} From 24ab4285f4d454e2a50e2658e03b25a2e49df6ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Nov 2024 15:57:34 +0000 Subject: [PATCH 2/2] chore: bump version to 3.1.3 [skip ci] --- extensions/chrome/package.json | 2 +- extensions/chrome/public/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json index f76e4d8..8548e12 100644 --- a/extensions/chrome/package.json +++ b/extensions/chrome/package.json @@ -1,6 +1,6 @@ { "name": "thinking-claude", - "version": "3.1.2", + "version": "3.1.3", "description": "Chrome extension for letting Claude think like a real human", "type": "module", "scripts": { diff --git a/extensions/chrome/public/manifest.json b/extensions/chrome/public/manifest.json index 232d2ce..498d1e9 100644 --- a/extensions/chrome/public/manifest.json +++ b/extensions/chrome/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Thinking Claude", - "version": "3.1.2", + "version": "3.1.3", "description": "Chrome extension for letting Claude think like a real human", "content_scripts": [ {