Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref: bump to v 3.1.3 #51

Merged
merged 3 commits into from
Nov 28, 2024
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 extensions/chrome/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion extensions/chrome/public/manifest.json
Original file line number Diff line number Diff line change
@@ -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": [
{
Expand Down
14 changes: 3 additions & 11 deletions extensions/chrome/src/content/index.ts
Original file line number Diff line number Diff line change
@@ -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()
28 changes: 28 additions & 0 deletions extensions/chrome/src/content/v3/features/base-feature.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
Expand Down
65 changes: 65 additions & 0 deletions extensions/chrome/src/content/v3/managers/extension-manager.ts
Original file line number Diff line number Diff line change
@@ -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()
}
}
56 changes: 56 additions & 0 deletions extensions/chrome/src/content/v3/managers/feature-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Feature } from "@/types"

export class FeatureManager {
private features = new Map<string, Feature>()
private cleanupFunctions = new Map<string, () => 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)
}
}
46 changes: 40 additions & 6 deletions extensions/chrome/src/services/mutation-observer.ts
Original file line number Diff line number Diff line change
@@ -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<ObserverCallback> = new Set()
private timeouts: Map<ObserverCallback, NodeJS.Timeout> = 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
Expand All @@ -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)
Expand All @@ -48,5 +84,3 @@ class MutationObserverService {
}
}
}

export const mutationObserver = new MutationObserverService()
7 changes: 7 additions & 0 deletions extensions/chrome/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Base interface for all features
*/
export interface Feature {
id: string
initialize(): void | (() => void) // Return cleanup function if needed
}