From d787c52724b15a3c44f155cb8d49b34bcb2d72dc Mon Sep 17 00:00:00 2001 From: Felix Lu Date: Mon, 25 Nov 2024 22:21:45 +0800 Subject: [PATCH 01/15] feat: experimenting with adding new implementation logic by using hybird approach with building new react component while keeping previous implementation separated --- extensions/chrome/components.json | 2 +- extensions/chrome/eslint.config.cjs | 5 + extensions/chrome/package.json | 7 + extensions/chrome/postcss.config.mjs | 2 +- .../chrome/src/components/ui/accordion.tsx | 56 +++ .../chrome/src/components/ui/button.tsx | 58 +++ .../chrome/src/components/ui/collapsible.tsx | 9 + .../chrome/src/components/ui/scroll-area.tsx | 47 +++ .../chrome/src/components/ui/tooltip.tsx | 31 ++ .../content/code-block-collapser/constants.ts | 58 +++ .../code-block-collapser/index_collapser.tsx | 340 ++++++++++++++++++ extensions/chrome/src/content/index.ts | 73 ++++ .../index_managers.tsx} | 2 +- .../src/content/react/project_instruction.md | 144 ++++++++ .../chrome/src/content/react/selectors.ts | 70 ++++ .../react/thinking-process-manager.tsx | 216 +++++++++++ .../src/content/react/thinking-process.tsx | 194 ++++++++++ extensions/chrome/src/styles/globals.css | 143 ++++---- extensions/chrome/src/utils/url-utils.ts | 7 + extensions/chrome/tailwind.config.cjs | 92 +++-- extensions/chrome/webpack/webpack.common.js | 2 +- 21 files changed, 1463 insertions(+), 95 deletions(-) create mode 100644 extensions/chrome/src/components/ui/accordion.tsx create mode 100644 extensions/chrome/src/components/ui/button.tsx create mode 100644 extensions/chrome/src/components/ui/collapsible.tsx create mode 100644 extensions/chrome/src/components/ui/scroll-area.tsx create mode 100644 extensions/chrome/src/components/ui/tooltip.tsx create mode 100644 extensions/chrome/src/content/code-block-collapser/constants.ts create mode 100644 extensions/chrome/src/content/code-block-collapser/index_collapser.tsx create mode 100644 extensions/chrome/src/content/index.ts rename extensions/chrome/src/content/{index.tsx => managers/index_managers.tsx} (87%) create mode 100644 extensions/chrome/src/content/react/project_instruction.md create mode 100644 extensions/chrome/src/content/react/selectors.ts create mode 100644 extensions/chrome/src/content/react/thinking-process-manager.tsx create mode 100644 extensions/chrome/src/content/react/thinking-process.tsx create mode 100644 extensions/chrome/src/utils/url-utils.ts diff --git a/extensions/chrome/components.json b/extensions/chrome/components.json index 23aac84..907cbba 100644 --- a/extensions/chrome/components.json +++ b/extensions/chrome/components.json @@ -4,7 +4,7 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.cjs", "css": "src/styles/globals.css", "baseColor": "zinc", "cssVariables": true, diff --git a/extensions/chrome/eslint.config.cjs b/extensions/chrome/eslint.config.cjs index 640b2bc..69cf971 100644 --- a/extensions/chrome/eslint.config.cjs +++ b/extensions/chrome/eslint.config.cjs @@ -33,6 +33,9 @@ module.exports = [ setInterval: "readonly", Node: "readonly", HTMLButtonElement: "readonly", + MutationRecord: "readonly", + MouseEvent: "readonly", + SVGSVGElement: "readonly", }, }, plugins: { @@ -58,6 +61,8 @@ module.exports = [ "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "no-inline-styles": "off", + "no-undef": "off", + "react/prop-types": "off", }, settings: { react: { diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json index 7c1de56..499155a 100644 --- a/extensions/chrome/package.json +++ b/extensions/chrome/package.json @@ -19,6 +19,12 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-collapsible": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.460.0", "react": "^18.2.0", @@ -46,6 +52,7 @@ "identity-obj-proxy": "^3.0.0", "lint-staged": "^14.0.1", "markdownlint-cli2": "^0.15.0", + "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.4.31", "postcss-loader": "^7.3.3", "postcss-nesting": "^12.0.1", diff --git a/extensions/chrome/postcss.config.mjs b/extensions/chrome/postcss.config.mjs index 2aa7205..2e7af2b 100644 --- a/extensions/chrome/postcss.config.mjs +++ b/extensions/chrome/postcss.config.mjs @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -}; +} diff --git a/extensions/chrome/src/components/ui/accordion.tsx b/extensions/chrome/src/components/ui/accordion.tsx new file mode 100644 index 0000000..58be9bd --- /dev/null +++ b/extensions/chrome/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" + +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/extensions/chrome/src/components/ui/button.tsx b/extensions/chrome/src/components/ui/button.tsx new file mode 100644 index 0000000..e70c05c --- /dev/null +++ b/extensions/chrome/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react" + +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-0 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/extensions/chrome/src/components/ui/collapsible.tsx b/extensions/chrome/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..a23e7a2 --- /dev/null +++ b/extensions/chrome/src/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/extensions/chrome/src/components/ui/scroll-area.tsx b/extensions/chrome/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..8bb00b6 --- /dev/null +++ b/extensions/chrome/src/components/ui/scroll-area.tsx @@ -0,0 +1,47 @@ +import * as React from "react" + +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/extensions/chrome/src/components/ui/tooltip.tsx b/extensions/chrome/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..79ce777 --- /dev/null +++ b/extensions/chrome/src/components/ui/tooltip.tsx @@ -0,0 +1,31 @@ +import * as React from "react" + +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/extensions/chrome/src/content/code-block-collapser/constants.ts b/extensions/chrome/src/content/code-block-collapser/constants.ts new file mode 100644 index 0000000..b56b6a0 --- /dev/null +++ b/extensions/chrome/src/content/code-block-collapser/constants.ts @@ -0,0 +1,58 @@ +export const SELECTORS = { + PRE: "pre", + CODE_CONTAINER: ".code-block__code", + MAIN_CONTAINER: ".relative.flex.flex-col", + THINKING_LABEL: ".text-text-300", + ORIGINAL_COPY_BTN: ".pointer-events-none", + CODE: "code", +} as const + +export const CLASSES = { + THINKING_HEADER: "thinking-header", + COPY_CONTAINER: + "from-bg-300/90 to-bg-300/70 pointer-events-auto rounded-md bg-gradient-to-b p-0.5 backdrop-blur-md", + COPY_BUTTON: + "flex flex-row items-center gap-1 rounded-md p-1 py-0.5 text-xs transition-opacity delay-100 hover:bg-bg-200 opacity-60 hover:opacity-100", + COPY_TEXT: "text-text-200 pr-0.5", + TOGGLE_BUTTON: "flex items-center text-text-500 hover:text-text-300", + TOGGLE_LABEL: "font-medium text-sm", + THINKING_ANIMATION: "thinking-animation", +} as const + +export const ANIMATION_STYLES = ` + @keyframes gradientWave { + 0% { background-position: 200% 50%; } + 100% { background-position: -200% 50%; } + } + + .thinking-animation { + background: linear-gradient( + 90deg, + rgba(156, 163, 175, 0.7) 0%, + rgba(209, 213, 219, 1) 25%, + rgba(156, 163, 175, 0.7) 50%, + rgba(209, 213, 219, 1) 75%, + rgba(156, 163, 175, 0.7) 100% + ); + background-size: 200% 100%; + animation: gradientWave 3s linear infinite; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; + } +` as const + +export const ICONS = { + COPY: ``, + TICK: ``, + ARROW: ``, +} as const + +export const TIMINGS = { + RETRY_DELAY: 1000, + MUTATION_DELAY: 100, + CHECK_INTERVAL: 2000, + COPY_FEEDBACK: 2000, + MAX_RETRIES: 10, +} as const diff --git a/extensions/chrome/src/content/code-block-collapser/index_collapser.tsx b/extensions/chrome/src/content/code-block-collapser/index_collapser.tsx new file mode 100644 index 0000000..f05fb2b --- /dev/null +++ b/extensions/chrome/src/content/code-block-collapser/index_collapser.tsx @@ -0,0 +1,340 @@ +import { + ANIMATION_STYLES, + CLASSES, + ICONS, + SELECTORS, + TIMINGS, +} from "./constants" + +// Initial style injection - runs immediately +const initialStyles = document.createElement("style") +initialStyles.id = "thinking-initial-styles" +initialStyles.textContent = ` + ${SELECTORS.PRE} { + opacity: 0; + transition: opacity 0.3s ease-in-out; + } +` +document.documentElement.appendChild(initialStyles) + +class CodeBlockCollapser { + private observers: Set + static instance: CodeBlockCollapser | null = null + + static getInstance(): CodeBlockCollapser { + if (!CodeBlockCollapser.instance) { + CodeBlockCollapser.instance = new CodeBlockCollapser() + } + return CodeBlockCollapser.instance + } + + private constructor() { + this.observers = new Set() + this.injectStyles() + this.initWithRetry() + + window.addEventListener("unload", () => this.cleanup()) + } + + private injectStyles() { + // Remove initial hiding style + const initialStyle = document.getElementById("thinking-initial-styles") + if (initialStyle) { + initialStyle.remove() + } + + // Inject permanent styles + if (!document.getElementById("thinking-styles")) { + const styleSheet = document.createElement("style") + styleSheet.id = "thinking-styles" + styleSheet.textContent = ` + ${ANIMATION_STYLES} + ${SELECTORS.PRE} { + opacity: 1; + transition: opacity 0.3s ease-in-out; + } + ` + document.head.appendChild(styleSheet) + } + } + + createElement( + tag: string, + className: string = "", + innerHTML: string = "" + ): HTMLElement { + const element = document.createElement(tag) + if (className) element.className = className + if (innerHTML) element.innerHTML = innerHTML + return element + } + + createCopyButton() { + const container = this.createElement("div", CLASSES.COPY_CONTAINER) + const button = this.createElement("button", CLASSES.COPY_BUTTON) + const iconSpan = this.createElement("span", "", ICONS.COPY) + const textSpan = this.createElement("span", CLASSES.COPY_TEXT, "Copy") + + button.append(iconSpan, textSpan) + container.appendChild(button) + + button.addEventListener("click", () => { + const preElement = button.closest(SELECTORS.PRE) + const codeElement = preElement?.querySelector(SELECTORS.CODE) + const codeText = codeElement?.textContent + + if (!codeText) return + + navigator.clipboard + .writeText(codeText) + .then(() => { + iconSpan.innerHTML = ICONS.TICK + textSpan.textContent = "Copied!" + + setTimeout(() => { + iconSpan.innerHTML = ICONS.COPY + textSpan.textContent = "Copy" + }, TIMINGS.COPY_FEEDBACK) + }) + .catch((error) => { + console.error("Failed to copy:", error) + }) + }) + + return container + } + + createToggleButton(isStreaming: boolean = false) { + const button = this.createElement("button", CLASSES.TOGGLE_BUTTON) + const labelText = isStreaming + ? "Claude is thinking..." + : "View thinking process" + button.innerHTML = ` + ${ICONS.ARROW} + ${labelText} + ` + return button + } + + updateHeaderState(headerContainer: HTMLElement, isStreaming: boolean) { + const toggleBtn = headerContainer.querySelector( + `.${CLASSES.TOGGLE_BUTTON}` + ) + if (!toggleBtn) return + + const label = toggleBtn.querySelector("span") + if (!label) return + + label.textContent = isStreaming + ? "Claude is thinking..." + : "View thinking process" + + if (isStreaming) { + label.classList.add(CLASSES.THINKING_ANIMATION) + } else { + label.classList.remove(CLASSES.THINKING_ANIMATION) + } + } + + setupCodeContainer(container: HTMLElement, toggleBtn: HTMLElement) { + if (!container) return + + container.style.cssText = ` + background: var(--bg-300); + transition: all 0.3s ease-in-out; + overflow-x: hidden; + overflow-y: auto; + padding: 1em; + max-width: 100%; + display: block; + max-height: 50vh; + opacity: 1; + ` + + const codeElement = container.querySelector(SELECTORS.CODE) + if (!codeElement) return + + codeElement.style.cssText = ` + white-space: pre-wrap !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + display: block !important; + max-width: 100% !important; + ` + + const arrow = toggleBtn.querySelector("svg") + const label = toggleBtn.querySelector("span") + if (!arrow || !label) return + + // Set initial state to open + arrow.style.transform = "rotate(180deg)" + if (!label.classList.contains(CLASSES.THINKING_ANIMATION)) { + label.textContent = "Hide thinking process" + } + + toggleBtn.addEventListener("click", () => { + const shouldToggleOpen = container.style.maxHeight === "0px" + container.style.maxHeight = shouldToggleOpen ? "50vh" : "0" + container.style.opacity = shouldToggleOpen ? "1" : "0" + container.style.padding = shouldToggleOpen ? "1em" : "0" + + arrow.style.transform = `rotate(${shouldToggleOpen ? 180 : 0}deg)` + if (!label.classList.contains(CLASSES.THINKING_ANIMATION)) { + label.textContent = shouldToggleOpen + ? "Hide thinking process" + : "View thinking process" + } + }) + } + + processBlock(pre: HTMLElement) { + const headerContainer = this.createElement("div", CLASSES.THINKING_HEADER) + headerContainer.style.cssText = + "display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: var(--bg-300);" + + const isStreaming = pre.closest('[data-is-streaming="true"]') !== null + const toggleBtn = this.createToggleButton(isStreaming) + const copyBtn = this.createCopyButton() + + headerContainer.append(toggleBtn, copyBtn) + + const codeContainer = pre.querySelector( + SELECTORS.CODE_CONTAINER + ) + if (!codeContainer) return + this.setupCodeContainer(codeContainer, toggleBtn) + + const mainContainer = pre.querySelector( + SELECTORS.MAIN_CONTAINER + ) + if (!mainContainer) return + + const codeParentElement = pre.querySelector( + SELECTORS.CODE_CONTAINER + )?.parentElement + if (!codeParentElement) return + + mainContainer.insertBefore(headerContainer, codeParentElement) + + const streamingContainer = pre.closest("[data-is-streaming]") + if (streamingContainer) { + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if ( + mutation.type === "attributes" && + mutation.attributeName === "data-is-streaming" + ) { + const isStreamingNow = + streamingContainer.getAttribute("data-is-streaming") === "true" + this.updateHeaderState(headerContainer, isStreamingNow) + } + } + }) + + observer.observe(streamingContainer, { + attributes: true, + attributeFilter: ["data-is-streaming"], + }) + + this.observers.add(observer) + + new MutationObserver(() => { + if (!document.contains(streamingContainer)) { + observer.disconnect() + this.observers.delete(observer) + } + }).observe(document.body, { childList: true, subtree: true }) + } + + for (const selector of [ + SELECTORS.THINKING_LABEL, + SELECTORS.ORIGINAL_COPY_BTN, + ]) { + const element = pre.querySelector(selector) as HTMLElement | null + if (element) element.style.display = "none" + } + } + + processExistingBlocks(): void { + for (const pre of document.querySelectorAll(SELECTORS.PRE)) { + const header = pre.querySelector(SELECTORS.THINKING_LABEL) + if (!header?.textContent) continue + + if ( + header.textContent.trim() === "thinking" && + !pre.querySelector(`.${CLASSES.THINKING_HEADER}`) + ) { + this.processBlock(pre) + } + } + } + + initWithRetry(retryCount: number = 0) { + if (retryCount >= TIMINGS.MAX_RETRIES) return + + const blocks = document.querySelectorAll(SELECTORS.PRE) + if (blocks.length === 0) { + setTimeout(() => this.initWithRetry(retryCount + 1), TIMINGS.RETRY_DELAY) + return + } + + this.processExistingBlocks() + this.setupObserver() + this.setupPeriodicCheck() + } + + setupObserver() { + const observer = new MutationObserver((mutations) => { + let shouldProcess = false + for (const mutation of mutations) { + if ( + mutation.addedNodes.length > 0 || + (mutation.type === "attributes" && + mutation.attributeName === "data-is-streaming") + ) { + shouldProcess = true + } + } + + if (shouldProcess) { + this.processExistingBlocks() + } + }) + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["data-is-streaming"], + }) + + this.observers.add(observer) + } + + setupPeriodicCheck(): void { + setInterval(() => { + this.processExistingBlocks() + }, TIMINGS.CHECK_INTERVAL) + } + + cleanup(): void { + this.observers.forEach((observer) => observer.disconnect()) + this.observers.clear() + } +} + +declare global { + interface Window { + codeBlockCollapser?: CodeBlockCollapser + } +} + +CodeBlockCollapser.getInstance() + +document.addEventListener("DOMContentLoaded", () => { + if (!window.codeBlockCollapser) { + window.codeBlockCollapser = CodeBlockCollapser.getInstance() + } +}) diff --git a/extensions/chrome/src/content/index.ts b/extensions/chrome/src/content/index.ts new file mode 100644 index 0000000..01350cb --- /dev/null +++ b/extensions/chrome/src/content/index.ts @@ -0,0 +1,73 @@ +import "@/styles/globals.css" + +import { shouldInitialize } from "@/utils/url-utils" + +import { thinkingProcessManager } from "./react/thinking-process-manager" + +const initializeExtension = async () => { + console.log("[Thinking Claude] Initializing extension...") + + // Skip initialization for unsupported pages + if (!shouldInitialize(window.location.href)) { + console.log( + "[Thinking Claude] Skipping initialization for unsupported page" + ) + return + } + + // Inject minimal CSS for FOUC prevention and original element hiding + const style = document.createElement("style") + style.textContent = ` + /* Hide visual elements of original thinking block when our component is loaded */ + .grid.grid-cols-1 pre > div:first-child:has(+ div[data-thinking-process-root]) { + position: absolute !important; + opacity: 0 !important; + pointer-events: none !important; + z-index: -1 !important; + /* Keep the element in DOM but visually hidden */ + clip: rect(0 0 0 0) !important; + clip-path: inset(50%) !important; + height: 1px !important; + width: 1px !important; + margin: -1px !important; + overflow: hidden !important; + } + + /* Hide unenhanced elements to prevent FOUC */ + .grid.grid-cols-1 pre .absolute:not([data-extension-loaded="true"]), + .code-block__code:not([data-extension-loaded="true"]) { + visibility: hidden !important; + height: 0 !important; + overflow: hidden !important; + } + + /* Shimmer animation for streaming state */ + @keyframes gradientWave { + 0% { background-position: 200% 50%; } + 100% { background-position: -200% 50%; } + } + + /* Ensure code block has proper styling */ + .grid.grid-cols-1 pre { + margin: 0 !important; + padding: 0 !important; + background: none !important; + } + ` + document.head.appendChild(style) + + // Start observing thinking process blocks + thinkingProcessManager.startObserving() +} + +// Initialize when DOM is ready +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initializeExtension) +} else { + initializeExtension() +} + +// Cleanup when extension is disabled or removed +window.addEventListener("unload", () => { + thinkingProcessManager.stopObserving() +}) diff --git a/extensions/chrome/src/content/index.tsx b/extensions/chrome/src/content/managers/index_managers.tsx similarity index 87% rename from extensions/chrome/src/content/index.tsx rename to extensions/chrome/src/content/managers/index_managers.tsx index 124e7d3..ad666a5 100644 --- a/extensions/chrome/src/content/index.tsx +++ b/extensions/chrome/src/content/managers/index_managers.tsx @@ -1,4 +1,4 @@ -import { ThinkingBlockManager } from "./managers/thinking-block-manager" +import { ThinkingBlockManager } from "./thinking-block-manager" /** * Check if current URL matches Claude chat pattern diff --git a/extensions/chrome/src/content/react/project_instruction.md b/extensions/chrome/src/content/react/project_instruction.md new file mode 100644 index 0000000..8e61dbf --- /dev/null +++ b/extensions/chrome/src/content/react/project_instruction.md @@ -0,0 +1,144 @@ +## Project Context + +- This is a chrome production-ready extension built with typescript and react for Claude.ai. + +- It is a chrome extension designed to be used together with `Thinking Claude`. Thinking Claude is a comprehensive set of instructions that guides Claude to think like a real human and responding with two main parts: a thinking process + a final answer. + +## Current status and challenges + +- Currently Claude already provides a code block with 'thinking' header for the thinking process and a final answer presented outside of the code block beneath the thinking process. + +- The original UI is still limited for long text content reading, as it is not collapsible and content is not auto-wrapped which causes it horizontally overflow and resulting requiring user to horizontally scroll for more content, and not vertically scrollable. + +- Regarding the copy button functionality, it is well implemented with the svg icons and the copy functionality, and it doesn't seem need any further improvement for now. We can just keep it as it is or adapt it into the new UI. + +## Project Objectives + +Our main object for now is to build the extension that mainly focuses on enhancing the code block with 'thinking' header for the thinking process by making the original code block more readable and collapsible while persisting the exsiting copy button functionality and strictly maintaining the original UI styles. + +### Features + +- Make the exsting code block with 'thinking' header for thinking process collapsible, which means it can be folded and unfolded when user clicks on the header text of the code block. +- Enhance the code block with 'thinking' header with context-specific status messages, such as "Claude is thinking..." when the thinking process is streaming, "Hide thinking process" when the thinking process is opened, and "View thinking process" when the thinking process is closed. +- Make the thinking process content auto-wrap and only vertically scrollable if the content is long +- Preserve the original copy button functionality +- Maintain the primitive and original UI styles + +### Technical Implementation Details + +### Styling Approach + +- Use Tailwind CSS directly to match Claude's UI +- Copy exact class names from Claude's DOM where possible for visual consistency +- No need for custom design system currently as Claude's UI is stable +- Utilize shadcn/ui components for complex UI elements: + - Use Collapsible component for thinking process block + - Use Button component for copy functionality + - Leverage built-in animations and accessibility features + - No need to handle complex animations manually + +### Component Libraries + +- shadcn/ui: A collection of accessible and customizable React components + - Pre-configured in the project + - Uses Radix UI primitives under the hood + - Provides consistent animations and interactions + - Components we'll use: + - Collapsible: For expandable thinking process + - Button: For copy functionality + - Card: For content container (if needed) + +### DOM Structure + +- Response container uses `data-is-streaming` attribute for streaming state +- Thinking process block structure: + + ```html +
+    
+ +
thinking
+ +
+
+ +
+
+ +
+ + + +
+
+
+ ``` + +- Content uses specific font families: "Fira Code", "Fira Mono", Menlo, Consolas + +### Key Components + +1. ThinkingProcess + + - Manages collapsible state + - Handles streaming state + - Preserves copy functionality + - Maintains original UI appearance + +2. Content Script (index.ts) + - Observes DOM for Claude responses + - Injects React components + - Handles initialization and cleanup + +### Performance Considerations + +- Use efficient DOM observation strategies +- Minimize re-renders during streaming +- Maintain smooth collapse/expand animations + +### Testing Requirements + +- Test with various thinking process lengths +- Verify streaming state handling +- Ensure copy functionality works +- Check collapse/expand behavior + +### Coding guidelines and structure + +- The extension is built using React and Typescript, which is bundled with Webpack. +- Use kebab-case naming convention for file and component names +- Alaways use full name for the variable names for the maximum readability +- This project is OSS that needs to be well documented with comments for others to understand the code, including those who are beginners of development. +- The project structure should be based on the src directory structure recommended by Chrome extension development and react, following the best practices for prioritizing performance and maintainbility. +- Starting with a clean, well-organized and maintainable index.ts or index.tsx as the main entry point for the extension, following separated concerns and modules for different parts of the extension +- Always remember to keep the code following the best practices of single responsibility and modularity, DRY (Don't Repeat Yourself) and KISS (Keep It Simple, Stupid) rules +- Always keep in mind the performance and maintainability issues when making changes to the existing code structure, and try to keep the code as simple and clean as possible. + +### Pitfalls and Technology approaches + +- There are fews ways to implement our enhenced code block with 'thinking' header, such as directly modifying the dom structure and content, creating and implementinga brand new custom React component with full control while maintaining the original UI, or using a combination of both. +- Carefully consider the app flows and the effecient dom observations (e.g., MutationObserver) logic to make sure the implementation is maintainable and scaleable +- Regarding the dom observations, we need to watch for the streaming thinking process content as the Claude AI is thinking and generating the response in order to update the UI accordingly +- Always remember to prvent Flash of Unstyled Content (FOUC) from original UIs when page loads or refreshes,when we are updating the UI. +- Regarding the state management, we need to ensure the states are well managed and updated accordingly, such as the thinking process visibility and the thinking process content. +- The only place we need to enable this extension is the Claude chat pages, in which we need to check the url pattern if starts with `https://claude.ai/chat/(chatid)`, and make sure the extension is only taking effect when there is thinking process code block in the responses. +- There can be one response or more responses in one Claude chat page, and each response may contains one thinking process code block, so we need to consider the multiple responses situation and states when updating the UI. +- Always remember to ask for original HTML and UI styles for double check and corrections + +### Key CSS Variables from Claude's theme + +- Colors: +- Background colors: --bg-000 through --bg-500 (dark theme colors) +- Text colors: --text-000 through --text-500 +- Accent colors: +- Main: --accent-main-000, --accent-main-100, --accent-main-200 +- Secondary: --accent-secondary-000, --accent-secondary-100, --accent-secondary-200 +- Border colors: --border-100 through --border-400 +- Typography: +- Font families: +- Claude's messages: --font-claude-message (uses --font-serif) +- User messages: --font-user-message (uses --font-sans-serif) +- Base fonts defined in --font-serif and --font-sans-serif diff --git a/extensions/chrome/src/content/react/selectors.ts b/extensions/chrome/src/content/react/selectors.ts new file mode 100644 index 0000000..b909f99 --- /dev/null +++ b/extensions/chrome/src/content/react/selectors.ts @@ -0,0 +1,70 @@ +/** Sequence of selectors from outside to inside + * 1. responseContainer: '[data-is-streaming]' + * The outermost container div with data-is-streaming attribute, containing the entire Claude response including thinking process, message, and footer. Has gradient background and rounded corners. + * + * 2. claudeMessageContainer: '.font-claude-message' + * The message container div with specific font styling, containing both thinking process and Claude's response. + * Has specific padding and leading styles. + * + * 3. thinkingProcessBlock: '.grid.grid-cols-1 pre' + * The
 element within a grid container, holds the entire thinking process section including header and content.
+ *    Has rounded corners and specific styling.
+ *
+ * 4. thinkingProcessBlockHeaderTitle: '.grid.grid-cols-1 pre .absolute'
+ *    The absolute-positioned div containing the text "thinking" in the header.
+ *    Located at the top of the thinking process block.
+ *
+ * 5. thinkingProcessBlockHeaderCopyButton: '.grid.grid-cols-1 pre .sticky.pointer-events-none'
+ *    The sticky-positioned div containing the copy button with pointer-events-none.
+ *    Located in the header area, includes an SVG icon and "Copy" text.
+ *
+ * 6. thinkingProcessContentContainer: '.code-block__code'
+ *    The div with class "code-block__code" that wraps the thinking process content.
+ *    Has specific background color, padding, and code styling properties.
+ *
+ * 7. thinkingProcessContent: '.code-block__code .language-thinking code'
+ *    The innermost  element within language-thinking class, containing the actual thinking process text.
+ *    Content is wrapped in  elements.
+ *
+ * 8. Additional content structure:
+ *    - thinkingContentSpans: Targets the outer spans that contain blocks of text
+ *    - thinkingContentTextSpans: Targets the inner spans with the actual thinking process text
+ */
+
+export const THINKING_SELECTORS = {
+  // Container elements
+  responseContainer: "[data-is-streaming]",
+  claudeMessageContainer: ".font-claude-message",
+  thinkingProcessBlock: ".grid.grid-cols-1 pre",
+
+  // Header elements
+  thinkingProcessBlockHeaderTitle: ".grid.grid-cols-1 pre .absolute",
+  thinkingProcessBlockHeaderCopyButton:
+    ".grid.grid-cols-1 pre .sticky.pointer-events-none",
+
+  // Content elements
+  thinkingProcessContentContainer: ".code-block__code",
+  thinkingProcessContent: ".code-block__code .language-thinking code",
+
+  // Specific content structure
+  thinkingContentSpans: ".language-thinking > span", // Outer spans containing text blocks
+  thinkingContentTextSpans: ".language-thinking > span > span", // Inner spans with actual text content
+} as const
+
+/** Style properties for the thinking process content */
+export const THINKING_CONTENT_STYLES = {
+  codeStyle: {
+    color: "rgb(171, 178, 191)",
+    textShadow: "rgba(0, 0, 0, 0.3) 0px 1px",
+    fontFamily:
+      '"Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace',
+    direction: "ltr",
+    textAlign: "left",
+    whiteSpace: "pre",
+    wordSpacing: "normal",
+    wordBreak: "normal",
+    lineHeight: 1.5,
+    tabSize: 2,
+    hyphens: "none",
+  },
+} as const
diff --git a/extensions/chrome/src/content/react/thinking-process-manager.tsx b/extensions/chrome/src/content/react/thinking-process-manager.tsx
new file mode 100644
index 0000000..c5210fa
--- /dev/null
+++ b/extensions/chrome/src/content/react/thinking-process-manager.tsx
@@ -0,0 +1,216 @@
+import React from "react"
+
+import { createRoot, type Root } from "react-dom/client"
+
+import { ThinkingProcess } from "./thinking-process"
+
+// Selectors for finding elements in Claude's DOM
+const THINKING_SELECTORS = {
+  responseContainer: "[data-is-streaming]",
+  thinkingProcessBlock: ".grid.grid-cols-1 pre",
+  thinkingProcessContent: ".code-block__code",
+} as const
+
+interface MountedComponent {
+  root: Root
+  streamingObserver?: MutationObserver
+  container: HTMLElement
+  rootDiv: HTMLElement
+}
+
+class ThinkingProcessManager {
+  private mountedComponents: Map = new Map()
+  private observer: MutationObserver
+
+  constructor() {
+    this.observer = new MutationObserver(this.handleDOMChanges.bind(this))
+  }
+
+  public startObserving(): void {
+    console.log("[Thinking Claude] Starting observation...")
+    this.processExistingBlocks()
+
+    this.observer.observe(document.body, {
+      childList: true,
+      subtree: true,
+    })
+  }
+
+  public stopObserving(): void {
+    console.log("[Thinking Claude] Stopping observation...")
+    this.observer.disconnect()
+
+    // Cleanup all mounted components
+    this.mountedComponents.forEach(({ root, streamingObserver, rootDiv }) => {
+      streamingObserver?.disconnect()
+      root.unmount()
+      rootDiv.remove() // Remove the root div from DOM
+    })
+    this.mountedComponents.clear()
+  }
+
+  private processExistingBlocks(): void {
+    try {
+      const containers = document.querySelectorAll(
+        THINKING_SELECTORS.thinkingProcessBlock
+      )
+      containers.forEach(this.mountComponent.bind(this))
+    } catch (error) {
+      console.error(
+        "[Thinking Claude] Error processing existing blocks:",
+        error
+      )
+    }
+  }
+
+  private handleDOMChanges(mutations: MutationRecord[]): void {
+    for (const mutation of mutations) {
+      mutation.addedNodes.forEach((node) => {
+        if (!(node instanceof HTMLElement)) return
+
+        if (
+          node.matches(THINKING_SELECTORS.thinkingProcessBlock) &&
+          !this.isOwnRootDiv(node) &&
+          !node.querySelector("[data-thinking-process-root]")
+        ) {
+          this.mountComponent(node)
+        }
+
+        // Process child containers
+        node
+          .querySelectorAll(
+            THINKING_SELECTORS.thinkingProcessBlock
+          )
+          .forEach((container) => {
+            if (
+              !this.isOwnRootDiv(container) &&
+              !container.querySelector("[data-thinking-process-root]")
+            ) {
+              this.mountComponent(container)
+            }
+          })
+      })
+    }
+  }
+
+  private isOwnRootDiv(element: HTMLElement): boolean {
+    // Check if this element is one of our root divs
+    return Array.from(this.mountedComponents.values()).some(
+      ({ rootDiv }) => rootDiv === element
+    )
+  }
+
+  private mountComponent(container: HTMLElement): void {
+    try {
+      // Additional check at the start of mount
+      if (
+        this.mountedComponents.has(container) ||
+        container.querySelector("[data-thinking-process-root]")
+      ) {
+        return
+      }
+
+      // Get streaming state from the closest response container
+      const responseContainer = container.closest(
+        THINKING_SELECTORS.responseContainer
+      )
+      if (!responseContainer) {
+        console.log(
+          "[Thinking Claude] No response container found, skipping mount"
+        )
+        return
+      }
+
+      const isStreaming =
+        responseContainer.getAttribute("data-is-streaming") === "true"
+
+      // Create root div and append it to container
+      const rootDiv = document.createElement("div")
+      rootDiv.id = "thinking-claude-root"
+      rootDiv.setAttribute("data-thinking-process-root", "true")
+      rootDiv.className = "rounded-xl w-full"
+      // TODO: REMOVE THE MARGIN RIGHT WHEN WE HAVE A BETTER SOLUTION
+      container.appendChild(rootDiv)
+
+      // Create React root
+      const root = createRoot(rootDiv)
+
+      // Create streaming observer
+      const streamingObserver = new MutationObserver((mutations) => {
+        mutations.forEach((mutation) => {
+          if (
+            mutation.type === "attributes" &&
+            mutation.attributeName === "data-is-streaming" &&
+            mutation.target instanceof HTMLElement
+          ) {
+            const newIsStreaming =
+              mutation.target.getAttribute("data-is-streaming") === "true"
+            root.render(
+              
+            )
+          }
+        })
+      })
+
+      // Store component info before observing
+      this.mountedComponents.set(container, {
+        root,
+        streamingObserver,
+        container,
+        rootDiv,
+      })
+
+      // Start observing streaming state changes
+      streamingObserver.observe(responseContainer, {
+        attributes: true,
+        attributeFilter: ["data-is-streaming"],
+      })
+
+      // Initial render
+      root.render(
+        
+      )
+
+      console.log("[Thinking Claude] Mounted component to:", container)
+    } catch (error) {
+      console.error("[Thinking Claude] Error mounting component:", error)
+    }
+  }
+
+  private unmountComponent(element: HTMLElement): void {
+    try {
+      // Check if this element is a mounted container
+      const mounted = this.mountedComponents.get(element)
+      if (mounted) {
+        const { root, streamingObserver, rootDiv } = mounted
+        streamingObserver?.disconnect()
+        root.unmount()
+        rootDiv.remove() // Remove the root div from DOM
+        this.mountedComponents.delete(element)
+        console.log("[Thinking Claude] Unmounted component from:", element)
+        return
+      }
+
+      // Check if any mounted components are children of this element
+      for (const [container, component] of this.mountedComponents.entries()) {
+        if (element.contains(container)) {
+          const { root, streamingObserver, rootDiv } = component
+          streamingObserver?.disconnect()
+          root.unmount()
+          rootDiv.remove() // Remove the root div from DOM
+          this.mountedComponents.delete(container)
+          console.log(
+            "[Thinking Claude] Unmounted child component from:",
+            container
+          )
+        }
+      }
+    } catch (error) {
+      console.error("[Thinking Claude] Error unmounting component:", error)
+    }
+  }
+}
+export const thinkingProcessManager = new ThinkingProcessManager()
diff --git a/extensions/chrome/src/content/react/thinking-process.tsx b/extensions/chrome/src/content/react/thinking-process.tsx
new file mode 100644
index 0000000..fd71abb
--- /dev/null
+++ b/extensions/chrome/src/content/react/thinking-process.tsx
@@ -0,0 +1,194 @@
+import React, { useCallback, useEffect, useState } from "react"
+
+import { cn } from "@/lib/utils"
+
+import { Button } from "@/components/ui/button"
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from "@/components/ui/collapsible"
+
+interface ThinkingProcessProps {
+  containerRef: HTMLElement
+  isStreaming: boolean
+}
+
+interface CodeViewerProps {
+  className?: string
+  originalElement: HTMLElement
+}
+
+const CodeViewer = ({ className, originalElement }: CodeViewerProps) => {
+  return (
+    
+  )
+}
+
+export function ThinkingProcess({
+  containerRef,
+  isStreaming,
+}: ThinkingProcessProps) {
+  const [isOpen, setIsOpen] = useState(true)
+  const [codeElement, setCodeElement] = useState(null)
+  const [isCopied, setIsCopied] = useState(false)
+
+  useEffect(() => {
+    const element = containerRef.querySelector(
+      ".code-block__code > code"
+    ) as HTMLElement
+    if (!element) return
+    setCodeElement(element)
+  }, [containerRef, isStreaming])
+
+  const handleCopy = useCallback(async () => {
+    try {
+      if (!codeElement) return
+      await navigator.clipboard.writeText(codeElement.textContent || "")
+      setIsCopied(true)
+      setTimeout(() => setIsCopied(false), 2000)
+    } catch (error) {
+      console.error("[Thinking Claude] Failed to copy:", error)
+    }
+  }, [codeElement])
+
+  if (!codeElement) return null
+
+  return (
+    
+      
+ + + + + +
+ + +
+ {codeElement && } +
+
+
+ ) +} + +// Icons +function ChevronUpIcon(props: React.SVGProps) { + return ( + + + + ) +} + +function ChevronDownIcon(props: React.SVGProps) { + return ( + + + + ) +} + +function CopyIcon(props: React.SVGProps) { + return ( + + + + ) +} + +function CheckIcon(props: React.SVGProps) { + return ( + + + + ) +} diff --git a/extensions/chrome/src/styles/globals.css b/extensions/chrome/src/styles/globals.css index d8fade3..4c5ab8e 100644 --- a/extensions/chrome/src/styles/globals.css +++ b/extensions/chrome/src/styles/globals.css @@ -4,87 +4,102 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 100% 50%; - --destructive-foreground: 210 40% 98%; - --ring: 215 20.2% 65.1%; - --radius: 0.5rem; + --tc-background: 0 0% 100%; + --tc-foreground: 222.2 47.4% 11.2%; + --tc-muted: 210 40% 96.1%; + --tc-muted-foreground: 215.4 16.3% 46.9%; + --tc-popover: 0 0% 100%; + --tc-popover-foreground: 222.2 47.4% 11.2%; + --tc-border: 214.3 31.8% 91.4%; + --tc-input: 214.3 31.8% 91.4%; + --tc-card: 0 0% 100%; + --tc-card-foreground: 222.2 47.4% 11.2%; + --tc-primary: 222.2 47.4% 11.2%; + --tc-primary-foreground: 210 40% 98%; + --tc-secondary: 210 40% 96.1%; + --tc-secondary-foreground: 222.2 47.4% 11.2%; + --tc-accent: 210 40% 96.1%; + --tc-accent-foreground: 222.2 47.4% 11.2%; + --tc-destructive: 0 100% 50%; + --tc-destructive-foreground: 210 40% 98%; + --tc-ring: 215 20.2% 65.1%; + --tc-radius: 0.5rem; + --tc-thinking-gradient: linear-gradient( + 90deg, + hsl(var(--tc-muted)) 0%, + hsl(var(--tc-muted-foreground)) 25%, + hsl(var(--tc-muted)) 50%, + hsl(var(--tc-muted-foreground)) 75%, + hsl(var(--tc-muted)) 100% + ); } .dark { - --background: 224 71% 4%; - --foreground: 213 31% 91%; - --muted: 223 47% 11%; - --muted-foreground: 215.4 16.3% 56.9%; - --accent: 216 34% 17%; - --accent-foreground: 210 40% 98%; - --popover: 224 71% 4%; - --popover-foreground: 215 20.2% 65.1%; - --border: 216 34% 17%; - --input: 216 34% 17%; - --card: 224 71% 4%; - --card-foreground: 213 31% 91%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 1.2%; - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 210 40% 98%; - --destructive: 0 63% 31%; - --destructive-foreground: 210 40% 98%; - --ring: 216 34% 17%; + --tc-background: 224 71% 4%; + --tc-foreground: 213 31% 91%; + --tc-muted: 223 47% 11%; + --tc-muted-foreground: 215.4 16.3% 56.9%; + --tc-accent: 216 34% 17%; + --tc-accent-foreground: 210 40% 98%; + --tc-popover: 224 71% 4%; + --tc-popover-foreground: 215 20.2% 65.1%; + --tc-border: 216 34% 17%; + --tc-input: 216 34% 17%; + --tc-card: 224 71% 4%; + --tc-card-foreground: 213 31% 91%; + --tc-primary: 210 40% 98%; + --tc-primary-foreground: 222.2 47.4% 1.2%; + --tc-secondary: 222.2 47.4% 11.2%; + --tc-secondary-foreground: 210 40% 98%; + --tc-destructive: 0 63% 31%; + --tc-destructive-foreground: 210 40% 98%; + --tc-ring: 216 34% 17%; } } -/* @layer base { - * { - @apply border-border; - } - body { - @apply font-sans antialiased bg-background text-foreground; +@layer components { + .tc-animate-shimmer { + background: var(--tc-thinking-gradient); + background-size: 200% 100%; + animation: tc-shimmer 3s linear infinite; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; } -} - -@layer base { - :root { - font-family: Inter, system-ui, sans-serif; + .tc-collapsible-content[data-state="open"] { + animation: collapsible-slide-down 0.25s ease-out; } -} */ -@layer components { - .btn { - @apply inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50; + .tc-collapsible-content[data-state="closed"] { + animation: collapsible-slide-up 0.2s ease-out; } +} - .btn-primary { - @apply bg-primary-600 text-white hover:bg-primary-700; +@keyframes tc-shimmer { + from { + background-position: 0% center; } - - .btn-secondary { - @apply bg-bg-200 text-text-700 hover:bg-bg-300; + to { + background-position: -200% center; } +} - .btn-outline { - @apply border border-bg-300 bg-transparent text-text-700 hover:bg-bg-100; +@keyframes collapsible-slide-down { + from { + height: 0; + } + to { + height: var(--radix-collapsible-content-height); } } -@layer utilities { - .text-gradient { - @apply bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent; +@keyframes collapsible-slide-up { + from { + height: var(--radix-collapsible-content-height); + } + to { + height: 0; } } diff --git a/extensions/chrome/src/utils/url-utils.ts b/extensions/chrome/src/utils/url-utils.ts new file mode 100644 index 0000000..4f06dbc --- /dev/null +++ b/extensions/chrome/src/utils/url-utils.ts @@ -0,0 +1,7 @@ +export const isChatPage = (url: string): boolean => { + return url.startsWith("https://claude.ai/chat/") +} + +export const shouldInitialize = (url: string): boolean => { + return isChatPage(url) +} diff --git a/extensions/chrome/tailwind.config.cjs b/extensions/chrome/tailwind.config.cjs index 53bc121..a841cac 100644 --- a/extensions/chrome/tailwind.config.cjs +++ b/extensions/chrome/tailwind.config.cjs @@ -1,50 +1,88 @@ +/**Know Issue for now: + * Our own Tailwind CSS is not being bundled into the build. + * TODO: Figure out why and fix this later. + */ + /** @type {import('tailwindcss').Config} */ module.exports = { - darkMode: ['class'], - content: ['./src/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}', './src/content/**/*.{ts,tsx}'], + prefix: "tc-", + important: true, + content: [ + "./src/**/*.{ts,tsx}", + "./src/components/**/*.{ts,tsx}", + "./src/content/**/*.{ts,tsx}", + ], theme: { extend: { colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + border: "hsl(var(--tc-border))", + input: "hsl(var(--tc-input))", + ring: "hsl(var(--tc-ring))", + background: "hsl(var(--tc-background))", + foreground: "hsl(var(--tc-foreground))", primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', + DEFAULT: "hsl(var(--tc-primary))", + foreground: "hsl(var(--tc-primary-foreground))", }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', + DEFAULT: "hsl(var(--tc-secondary))", + foreground: "hsl(var(--tc-secondary-foreground))", }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', + DEFAULT: "hsl(var(--tc-destructive))", + foreground: "hsl(var(--tc-destructive-foreground))", }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', + DEFAULT: "hsl(var(--tc-muted))", + foreground: "hsl(var(--tc-muted-foreground))", }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', + DEFAULT: "hsl(var(--tc-accent))", + foreground: "hsl(var(--tc-accent-foreground))", }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', + DEFAULT: "hsl(var(--tc-popover))", + foreground: "hsl(var(--tc-popover-foreground))", }, card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', + DEFAULT: "hsl(var(--tc-card))", + foreground: "hsl(var(--tc-card-foreground))", }, }, - borderRadius: { - lg: `var(--radius)`, - md: `calc(var(--radius) - 2px)`, - sm: 'calc(var(--radius) - 4px)', + // we can inherit the original border radius form claude here + // borderRadius: { + // lg: "`var(--radius)`", + // md: "`calc(var(--radius) - 2px)`", + // sm: "calc(var(--radius) - 4px)", + // }, + keyframes: { + "accordion-down": { + from: { + height: "0", + }, + to: { + height: "var(--radix-accordion-content-height)", + }, + }, + "accordion-up": { + from: { + height: "var(--radix-accordion-content-height)", + }, + to: { + height: "0", + }, + }, + // shimmer: { + // "0%": { backgroundPosition: "200% 50%" }, + // "100%": { backgroundPosition: "-200% 50%" }, + // }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + // shimmer: "shimmer 3s linear infinite", }, }, }, - plugins: [require('tailwindcss-animate')], -}; + plugins: [require("tailwindcss-animate")], +} diff --git a/extensions/chrome/webpack/webpack.common.js b/extensions/chrome/webpack/webpack.common.js index a285e3b..f8b5d62 100644 --- a/extensions/chrome/webpack/webpack.common.js +++ b/extensions/chrome/webpack/webpack.common.js @@ -10,7 +10,7 @@ export default { entry: { // popup: path.resolve(__dirname, '..', 'src', 'popup', 'index.tsx'), //popup is not being developed yet // background: path.resolve(__dirname, '..', 'src', 'background', 'index.ts'), //background is not being developed yet - content: path.resolve(__dirname, "..", "src", "content", "index.tsx"), + content: path.resolve(__dirname, "..", "src", "content", "index.ts"), }, module: { rules: [ From 4299c0ab2ae5dbe6d261b3f37a5717886a4a3ca9 Mon Sep 17 00:00:00 2001 From: Felix Lu Date: Tue, 26 Nov 2024 01:16:56 +0800 Subject: [PATCH 02/15] feat: clean up the previous react experienment and conclue the new one with updated project_instruction document --- extensions/chrome/src/content/index.ts | 76 +++-- .../src/content/react/project_instruction.md | 262 +++++++++--------- .../chrome/src/content/react/selectors.ts | 107 +++---- .../content/react/thinking-block-manager.tsx | 235 ++++++++++++++++ ...hinking-process.tsx => thinking-block.tsx} | 4 +- .../react/thinking-process-manager.tsx | 216 --------------- 6 files changed, 486 insertions(+), 414 deletions(-) create mode 100644 extensions/chrome/src/content/react/thinking-block-manager.tsx rename extensions/chrome/src/content/react/{thinking-process.tsx => thinking-block.tsx} (98%) delete mode 100644 extensions/chrome/src/content/react/thinking-process-manager.tsx diff --git a/extensions/chrome/src/content/index.ts b/extensions/chrome/src/content/index.ts index 01350cb..8676ab1 100644 --- a/extensions/chrome/src/content/index.ts +++ b/extensions/chrome/src/content/index.ts @@ -2,10 +2,10 @@ import "@/styles/globals.css" import { shouldInitialize } from "@/utils/url-utils" -import { thinkingProcessManager } from "./react/thinking-process-manager" +import { thinkingBlockManager } from "./react/thinking-block-manager" const initializeExtension = async () => { - console.log("[Thinking Claude] Initializing extension...") + console.log("[Thinking Claude] Starting extension initialization...") // Skip initialization for unsupported pages if (!shouldInitialize(window.location.href)) { @@ -15,16 +15,32 @@ const initializeExtension = async () => { return } - // Inject minimal CSS for FOUC prevention and original element hiding + console.log("[Thinking Claude] Page supported, continuing initialization") + + // Immediately inject initial CSS to prevent FOUC + const initialStyle = document.createElement("style") + initialStyle.id = "tc-initial-styles" + initialStyle.textContent = ` + /* Initially hide all thinking blocks with transition */ + pre > div:first-child { + opacity: 0; + transition: opacity 0.2s ease-in; + } + ` + document.head.appendChild(initialStyle) + + // Add a small delay to ensure DOM is fully loaded + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Inject main CSS for enhanced UI const style = document.createElement("style") style.textContent = ` - /* Hide visual elements of original thinking block when our component is loaded */ - .grid.grid-cols-1 pre > div:first-child:has(+ div[data-thinking-process-root]) { + /* Only hide elements that have our enhanced version */ + pre > div:first-child:has(+ div.thinking-block-container) { position: absolute !important; opacity: 0 !important; pointer-events: none !important; z-index: -1 !important; - /* Keep the element in DOM but visually hidden */ clip: rect(0 0 0 0) !important; clip-path: inset(50%) !important; height: 1px !important; @@ -33,9 +49,9 @@ const initializeExtension = async () => { overflow: hidden !important; } - /* Hide unenhanced elements to prevent FOUC */ - .grid.grid-cols-1 pre .absolute:not([data-extension-loaded="true"]), - .code-block__code:not([data-extension-loaded="true"]) { + /* Only hide elements after we've processed them */ + pre .text-text-300[data-tc-processed="true"], + .code-block__code[data-tc-processed="true"] { visibility: hidden !important; height: 0 !important; overflow: hidden !important; @@ -48,26 +64,54 @@ const initializeExtension = async () => { } /* Ensure code block has proper styling */ - .grid.grid-cols-1 pre { - margin: 0 !important; - padding: 0 !important; + pre { background: none !important; } ` document.head.appendChild(style) + console.log("[Thinking Claude] Injected CSS styles") + + // Initialize block manager + console.log("[Thinking Claude] Starting block manager initialization...") + thinkingBlockManager.initialize() + + // Remove initial styles after successful initialization + setTimeout(() => { + const initialStyles = document.getElementById("tc-initial-styles") + if (initialStyles) { + // Fade blocks back in if our enhanced UI failed to mount + initialStyles.textContent = ` + pre > div:first-child:not(:has(+ div.thinking-block-container)) { + opacity: 1; + transition: opacity 0.2s ease-out; + } + ` + // Remove initial styles after transition + setTimeout(() => initialStyles.remove(), 250) + } + }, 500) - // Start observing thinking process blocks - thinkingProcessManager.startObserving() + // Add cleanup on unload + window.addEventListener("unload", () => { + thinkingBlockManager.cleanup() + document.getElementById("tc-initial-styles")?.remove() + }) } // Initialize when DOM is ready if (document.readyState === "loading") { + console.log( + "[Thinking Claude] Document loading, waiting for DOMContentLoaded" + ) document.addEventListener("DOMContentLoaded", initializeExtension) } else { + console.log( + "[Thinking Claude] Document already loaded, initializing immediately" + ) initializeExtension() } -// Cleanup when extension is disabled or removed +// Cleanup on unload window.addEventListener("unload", () => { - thinkingProcessManager.stopObserving() + console.log("[Thinking Claude] Extension unloading, starting cleanup...") }) diff --git a/extensions/chrome/src/content/react/project_instruction.md b/extensions/chrome/src/content/react/project_instruction.md index 8e61dbf..9f13bdd 100644 --- a/extensions/chrome/src/content/react/project_instruction.md +++ b/extensions/chrome/src/content/react/project_instruction.md @@ -1,144 +1,142 @@ -## Project Context +# Thinking Claude Extension -- This is a chrome production-ready extension built with typescript and react for Claude.ai. +A Chrome extension that enhances Claude AI's thinking process visualization with dynamic React components. -- It is a chrome extension designed to be used together with `Thinking Claude`. Thinking Claude is a comprehensive set of instructions that guides Claude to think like a real human and responding with two main parts: a thinking process + a final answer. +## Project Overview -## Current status and challenges +This extension enhances Claude.ai's thinking process blocks by making them more readable and interactive while maintaining the original UI aesthetics. -- Currently Claude already provides a code block with 'thinking' header for the thinking process and a final answer presented outside of the code block beneath the thinking process. +### Core Features -- The original UI is still limited for long text content reading, as it is not collapsible and content is not auto-wrapped which causes it horizontally overflow and resulting requiring user to horizontally scroll for more content, and not vertically scrollable. +- Collapsible thinking process blocks +- Dynamic streaming state handling +- Auto-wrapping content with vertical scrolling +- Preserved copy functionality +- Original UI style maintenance -- Regarding the copy button functionality, it is well implemented with the svg icons and the copy functionality, and it doesn't seem need any further improvement for now. We can just keep it as it is or adapt it into the new UI. +## Technical Implementation -## Project Objectives +### Core Architecture -Our main object for now is to build the extension that mainly focuses on enhancing the code block with 'thinking' header for the thinking process by making the original code block more readable and collapsible while persisting the exsiting copy button functionality and strictly maintaining the original UI styles. +#### ThinkingBlockManager (Singleton) -### Features +- Central manager for block detection and enhancement +- Handles initialization, processing, and cleanup +- Maintains state of processed blocks and React roots +- Provides graceful fallback to original UI -- Make the exsting code block with 'thinking' header for thinking process collapsible, which means it can be folded and unfolded when user clicks on the header text of the code block. -- Enhance the code block with 'thinking' header with context-specific status messages, such as "Claude is thinking..." when the thinking process is streaming, "Hide thinking process" when the thinking process is opened, and "View thinking process" when the thinking process is closed. -- Make the thinking process content auto-wrap and only vertically scrollable if the content is long -- Preserve the original copy button functionality -- Maintain the primitive and original UI styles +#### Block Processing -### Technical Implementation Details - -### Styling Approach +- Unique block ID generation and tracking +- React component mounting in DOM +- Streaming state detection +- Automatic cleanup of unmounted components -- Use Tailwind CSS directly to match Claude's UI -- Copy exact class names from Claude's DOM where possible for visual consistency -- No need for custom design system currently as Claude's UI is stable -- Utilize shadcn/ui components for complex UI elements: - - Use Collapsible component for thinking process block - - Use Button component for copy functionality - - Leverage built-in animations and accessibility features - - No need to handle complex animations manually - -### Component Libraries - -- shadcn/ui: A collection of accessible and customizable React components - - Pre-configured in the project - - Uses Radix UI primitives under the hood - - Provides consistent animations and interactions - - Components we'll use: - - Collapsible: For expandable thinking process - - Button: For copy functionality - - Card: For content container (if needed) - -### DOM Structure - -- Response container uses `data-is-streaming` attribute for streaming state -- Thinking process block structure: - - ```html -
-    
- -
thinking
- -
-
- -
-
- -
- - - -
-
-
- ``` - -- Content uses specific font families: "Fira Code", "Fira Mono", Menlo, Consolas - -### Key Components - -1. ThinkingProcess - - - Manages collapsible state - - Handles streaming state - - Preserves copy functionality - - Maintains original UI appearance - -2. Content Script (index.ts) - - Observes DOM for Claude responses - - Injects React components - - Handles initialization and cleanup - -### Performance Considerations - -- Use efficient DOM observation strategies -- Minimize re-renders during streaming -- Maintain smooth collapse/expand animations - -### Testing Requirements - -- Test with various thinking process lengths -- Verify streaming state handling -- Ensure copy functionality works -- Check collapse/expand behavior - -### Coding guidelines and structure - -- The extension is built using React and Typescript, which is bundled with Webpack. -- Use kebab-case naming convention for file and component names -- Alaways use full name for the variable names for the maximum readability -- This project is OSS that needs to be well documented with comments for others to understand the code, including those who are beginners of development. -- The project structure should be based on the src directory structure recommended by Chrome extension development and react, following the best practices for prioritizing performance and maintainbility. -- Starting with a clean, well-organized and maintainable index.ts or index.tsx as the main entry point for the extension, following separated concerns and modules for different parts of the extension -- Always remember to keep the code following the best practices of single responsibility and modularity, DRY (Don't Repeat Yourself) and KISS (Keep It Simple, Stupid) rules -- Always keep in mind the performance and maintainability issues when making changes to the existing code structure, and try to keep the code as simple and clean as possible. - -### Pitfalls and Technology approaches - -- There are fews ways to implement our enhenced code block with 'thinking' header, such as directly modifying the dom structure and content, creating and implementinga brand new custom React component with full control while maintaining the original UI, or using a combination of both. -- Carefully consider the app flows and the effecient dom observations (e.g., MutationObserver) logic to make sure the implementation is maintainable and scaleable -- Regarding the dom observations, we need to watch for the streaming thinking process content as the Claude AI is thinking and generating the response in order to update the UI accordingly -- Always remember to prvent Flash of Unstyled Content (FOUC) from original UIs when page loads or refreshes,when we are updating the UI. -- Regarding the state management, we need to ensure the states are well managed and updated accordingly, such as the thinking process visibility and the thinking process content. -- The only place we need to enable this extension is the Claude chat pages, in which we need to check the url pattern if starts with `https://claude.ai/chat/(chatid)`, and make sure the extension is only taking effect when there is thinking process code block in the responses. -- There can be one response or more responses in one Claude chat page, and each response may contains one thinking process code block, so we need to consider the multiple responses situation and states when updating the UI. -- Always remember to ask for original HTML and UI styles for double check and corrections - -### Key CSS Variables from Claude's theme - -- Colors: -- Background colors: --bg-000 through --bg-500 (dark theme colors) -- Text colors: --text-000 through --text-500 -- Accent colors: -- Main: --accent-main-000, --accent-main-100, --accent-main-200 -- Secondary: --accent-secondary-000, --accent-secondary-100, --accent-secondary-200 -- Border colors: --border-100 through --border-400 -- Typography: -- Font families: -- Claude's messages: --font-claude-message (uses --font-serif) -- User messages: --font-user-message (uses --font-sans-serif) -- Base fonts defined in --font-serif and --font-sans-serif +#### CSS Management + +- Multi-stage CSS injection for FOUC prevention +- Graceful fallback with smooth transitions +- Original UI preservation for error cases +- Scoped styles for enhanced components + +### Implementation Details + +#### Block Detection + +- Use data attributes for tracking (data-tc-block-id, data-tc-container-id) +- Process blocks only once, with reprocessing capability +- Handle streaming state changes via mutation observer +- Clean up React roots and processed state on unmount + +#### CSS Strategy + +1. Initial styles (prevent FOUC) + - Hide blocks with transition + - Immediate injection on page load +2. Enhanced UI styles + - Scoped to processed blocks + - Preserve original UI as fallback +3. Transition handling + - Smooth transitions between states + - Graceful fallback if enhancement fails + +#### Resource Management + +- Disconnect mutation observer +- Clear interval checks +- Unmount React roots +- Remove processed block tracking +- Clean up injected styles + +### Best Practices + +#### Error Handling + +- Graceful fallback to original UI +- Clear error logging +- Resource cleanup on failure + +#### Performance + +- Minimal DOM operations +- Efficient block tracking +- Optimized mutation observer + +#### User Experience + +- Smooth transitions +- No visual disruption +- Consistent behavior + +### Known Limitations + +1. Initial page load may show brief flash of original UI +2. Streaming state detection depends on Claude's DOM structure +3. React component remounting during streaming responses + +### Future Improvements + +#### Performance + +- Optimize block detection +- Reduce style recalculations +- Improve streaming handling + +#### Features + +- User configuration options +- Additional visual enhancements +- More interactive elements + +#### Testing + +- Unit tests for core functionality +- Integration tests for UI components +- Performance benchmarking + +### Development Guidelines + +#### Code Organization + +- Keep ThinkingBlockManager as single source of truth +- Use TypeScript for type safety +- Follow React best practices +- Use kebab-case for file names +- Use descriptive variable names +- Document code thoroughly for OSS contributors + +#### Style Management + +- Use Tailwind CSS to match Claude's UI +- Copy exact class names where possible +- Maintain clear CSS hierarchy +- Use scoped styles when possible +- Handle transitions smoothly + +#### Testing + +- Test error scenarios +- Verify cleanup functionality +- Check streaming state handling +- Test with various content lengths +- Verify component lifecycle diff --git a/extensions/chrome/src/content/react/selectors.ts b/extensions/chrome/src/content/react/selectors.ts index b909f99..c898a56 100644 --- a/extensions/chrome/src/content/react/selectors.ts +++ b/extensions/chrome/src/content/react/selectors.ts @@ -2,69 +2,80 @@ * 1. responseContainer: '[data-is-streaming]' * The outermost container div with data-is-streaming attribute, containing the entire Claude response including thinking process, message, and footer. Has gradient background and rounded corners. * - * 2. claudeMessageContainer: '.font-claude-message' - * The message container div with specific font styling, containing both thinking process and Claude's response. - * Has specific padding and leading styles. - * - * 3. thinkingProcessBlock: '.grid.grid-cols-1 pre' - * The
 element within a grid container, holds the entire thinking process section including header and content.
+ * 2. thinkingProcessBlock: 'pre, .text-text-300, .code-block__code'
+ *    The 
 element, holds the entire thinking process section including header and content.
  *    Has rounded corners and specific styling.
  *
- * 4. thinkingProcessBlockHeaderTitle: '.grid.grid-cols-1 pre .absolute'
- *    The absolute-positioned div containing the text "thinking" in the header.
+ * 3. thinkingLabel: '.text-text-300'
+ *    The div containing the text label for the thinking process.
  *    Located at the top of the thinking process block.
  *
- * 5. thinkingProcessBlockHeaderCopyButton: '.grid.grid-cols-1 pre .sticky.pointer-events-none'
- *    The sticky-positioned div containing the copy button with pointer-events-none.
- *    Located in the header area, includes an SVG icon and "Copy" text.
- *
- * 6. thinkingProcessContentContainer: '.code-block__code'
+ * 4. thinkingProcessContentContainer: '.code-block__code'
  *    The div with class "code-block__code" that wraps the thinking process content.
  *    Has specific background color, padding, and code styling properties.
  *
- * 7. thinkingProcessContent: '.code-block__code .language-thinking code'
- *    The innermost  element within language-thinking class, containing the actual thinking process text.
+ * 5. thinkingProcessContent: 'code'
+ *    The innermost  element, containing the actual thinking process text.
  *    Content is wrapped in  elements.
  *
- * 8. Additional content structure:
- *    - thinkingContentSpans: Targets the outer spans that contain blocks of text
- *    - thinkingContentTextSpans: Targets the inner spans with the actual thinking process text
+ * 6. mainContainer: '.relative.flex.flex-col'
+ *    The main container div with specific styling and layout.
+ *
+ * 7. originalCopyBtn: '.pointer-events-none'
+ *    The copy button with pointer-events-none.
+ *    Located in the header area, includes an SVG icon and "Copy" text.
+ *
+ * 8. enhancedContainer: '[data-tc-container-id]'
+ *    The container element with data-tc-container-id attribute.
+ *
+ * 9. processedElement: '[data-tc-processed]'
+ *    The element with data-tc-processed attribute.
  */
 
+/** Selectors for finding and interacting with Claude's thinking blocks */
 export const THINKING_SELECTORS = {
-  // Container elements
+  // Container selectors
   responseContainer: "[data-is-streaming]",
-  claudeMessageContainer: ".font-claude-message",
-  thinkingProcessBlock: ".grid.grid-cols-1 pre",
+  mainContainer: ".relative.flex.flex-col",
 
-  // Header elements
-  thinkingProcessBlockHeaderTitle: ".grid.grid-cols-1 pre .absolute",
-  thinkingProcessBlockHeaderCopyButton:
-    ".grid.grid-cols-1 pre .sticky.pointer-events-none",
+  // Core thinking block selectors
+  thinkingProcessBlock:
+    "pre, .text-text-300, .code-block__code, [class*='text-text-'], [class*='code-block']",
+  thinkingLabel: ".text-text-300, [class*='text-text-']",
+  thinkingProcessContentContainer: ".code-block__code, [class*='code-block']",
+  thinkingProcessContent: "code",
 
-  // Content elements
-  thinkingProcessContentContainer: ".code-block__code",
-  thinkingProcessContent: ".code-block__code .language-thinking code",
+  // UI elements
+  originalCopyBtn: ".pointer-events-none",
 
-  // Specific content structure
-  thinkingContentSpans: ".language-thinking > span", // Outer spans containing text blocks
-  thinkingContentTextSpans: ".language-thinking > span > span", // Inner spans with actual text content
+  // Enhanced elements
+  enhancedContainer: "[data-tc-container-id]",
+  processedElement: "[data-tc-processed]",
 } as const
 
-/** Style properties for the thinking process content */
-export const THINKING_CONTENT_STYLES = {
-  codeStyle: {
-    color: "rgb(171, 178, 191)",
-    textShadow: "rgba(0, 0, 0, 0.3) 0px 1px",
-    fontFamily:
-      '"Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace',
-    direction: "ltr",
-    textAlign: "left",
-    whiteSpace: "pre",
-    wordSpacing: "normal",
-    wordBreak: "normal",
-    lineHeight: 1.5,
-    tabSize: 2,
-    hyphens: "none",
-  },
-} as const
+// Helper function to check if element is a thinking block
+export const isThinkingBlock = (
+  element: Element | null
+): element is HTMLDivElement => {
+  if (!element || !(element instanceof HTMLDivElement)) {
+    return false
+  }
+
+  // First check if the element itself is a thinking label or code container
+  if (
+    element.matches(THINKING_SELECTORS.thinkingLabel) ||
+    element.matches(THINKING_SELECTORS.thinkingProcessContentContainer)
+  ) {
+    return true
+  }
+
+  // Then check for child elements
+  const hasThinkingLabel = !!element.querySelector(
+    THINKING_SELECTORS.thinkingLabel
+  )
+  const hasCodeContainer = !!element.querySelector(
+    THINKING_SELECTORS.thinkingProcessContentContainer
+  )
+
+  return hasThinkingLabel || hasCodeContainer
+}
diff --git a/extensions/chrome/src/content/react/thinking-block-manager.tsx b/extensions/chrome/src/content/react/thinking-block-manager.tsx
new file mode 100644
index 0000000..ca1f090
--- /dev/null
+++ b/extensions/chrome/src/content/react/thinking-block-manager.tsx
@@ -0,0 +1,235 @@
+import { createRoot } from "react-dom/client"
+
+import { THINKING_SELECTORS } from "./selectors"
+import { ThinkingProcess } from "./thinking-block"
+
+/**
+ * Manages the detection and enhancement of thinking process blocks in Claude's responses
+ */
+class ThinkingBlockManager {
+  private observer: MutationObserver | null = null
+  private checkInterval: number | null = null
+  private processedBlocks = new Set()
+  private roots = new Map>()
+
+  /**
+   * Process a thinking block element by mounting a React component
+   */
+  private processBlock(block: Element) {
+    // Check if block needs reprocessing
+    const blockId = block.getAttribute("data-tc-block-id")
+    const existingContainer = blockId
+      ? document.querySelector(`[data-tc-container-id="${blockId}"]`)
+      : null
+
+    // If block was previously processed but container is missing, clean up
+    if (blockId && !existingContainer) {
+      this.roots.get(blockId)?.unmount()
+      this.roots.delete(blockId)
+      this.processedBlocks.delete(block)
+      // Remove processed attributes to allow reprocessing
+      block.querySelectorAll("[data-tc-processed]").forEach((el) => {
+        el.removeAttribute("data-tc-processed")
+        ;(el as HTMLElement).style.display = ""
+      })
+    }
+
+    // Skip if already processed and container exists
+    if (this.processedBlocks.has(block) && existingContainer) return
+
+    // Check for thinking label
+    const thinkingLabel = block.querySelector(THINKING_SELECTORS.thinkingLabel)
+    if (!thinkingLabel || thinkingLabel.textContent?.trim() !== "thinking")
+      return
+
+    // Create container for React component
+    const container = document.createElement("div")
+    container.className = "thinking-block-container"
+
+    // Get code container and content
+    const codeContainer = block.querySelector(
+      THINKING_SELECTORS.thinkingProcessContentContainer
+    )
+    const codeContent = block.querySelector(
+      THINKING_SELECTORS.thinkingProcessContent
+    )
+    if (!codeContainer || !codeContent) return
+
+    // Check if block is streaming
+    const isStreaming =
+      block
+        .closest(THINKING_SELECTORS.responseContainer)
+        ?.getAttribute("data-is-streaming") === "true"
+
+    // Mount React component
+    try {
+      // Generate unique ID for the block if it doesn't exist
+      const newBlockId =
+        blockId ||
+        `tc-block-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
+      if (!blockId) {
+        block.setAttribute("data-tc-block-id", newBlockId)
+      }
+      container.setAttribute("data-tc-container-id", newBlockId)
+
+      const root = createRoot(container)
+      this.roots.set(newBlockId, root)
+      root.render(
+        
+      )
+
+      // Insert React component
+      const mainContainer = block.querySelector(
+        THINKING_SELECTORS.mainContainer
+      )
+      if (mainContainer) {
+        mainContainer.insertBefore(container, codeContainer.parentElement)
+        this.processedBlocks.add(block)
+
+        // Mark elements as processed
+        const elementsToHide = [
+          block.querySelector(THINKING_SELECTORS.thinkingLabel),
+          block.querySelector(THINKING_SELECTORS.originalCopyBtn),
+          codeContainer,
+        ]
+        elementsToHide.forEach((el) => {
+          if (el) {
+            ;(el as HTMLElement).style.display = "none"
+            el.setAttribute("data-tc-processed", "true")
+          }
+        })
+
+        console.log("[TC Block] Successfully processed block:", {
+          blockId: newBlockId,
+          isStreaming,
+          hasThinkingLabel: !!block.querySelector(
+            THINKING_SELECTORS.thinkingLabel
+          ),
+          hasCodeContainer: !!codeContainer,
+          hasCodeContent: !!codeContent,
+          mountPoint: mainContainer.tagName,
+          containerClass: container.className,
+          timing: {
+            processedAt: new Date().toISOString(),
+            blockCount: this.processedBlocks.size,
+          },
+        })
+      }
+    } catch (error) {
+      console.error("Error mounting thinking block:", error)
+    }
+  }
+
+  /**
+   * Process all existing thinking blocks
+   */
+  private processExistingBlocks = () => {
+    const blocks = document.querySelectorAll(
+      THINKING_SELECTORS.thinkingProcessBlock
+    )
+    blocks.forEach((block) => this.processBlock(block))
+  }
+
+  /**
+   * Set up mutation observer to detect new thinking blocks
+   */
+  private setupObserver() {
+    if (this.observer) return
+
+    this.observer = new MutationObserver((mutations) => {
+      let shouldProcess = false
+      for (const mutation of mutations) {
+        // Check for added nodes
+        if (mutation.addedNodes.length > 0) {
+          shouldProcess = true
+          break
+        }
+        // Check for removed nodes that might be our containers
+        if (mutation.removedNodes.length > 0) {
+          for (const node of mutation.removedNodes) {
+            if (
+              node instanceof Element &&
+              node.matches(".thinking-block-container")
+            ) {
+              shouldProcess = true
+              break
+            }
+          }
+        }
+        // Check for streaming attribute changes
+        if (
+          mutation.type === "attributes" &&
+          mutation.attributeName === "data-is-streaming"
+        ) {
+          shouldProcess = true
+          break
+        }
+      }
+
+      if (shouldProcess) {
+        setTimeout(this.processExistingBlocks, 100) // Small delay to ensure DOM is ready
+      }
+    })
+
+    this.observer.observe(document.body, {
+      childList: true,
+      subtree: true,
+      attributes: true,
+      attributeFilter: ["data-is-streaming"],
+    })
+  }
+
+  /**
+   * Initialize with retry logic
+   */
+  private initWithRetry(retryCount = 0) {
+    const maxRetries = 10
+    const retryDelay = 1000
+
+    if (retryCount >= maxRetries) return
+
+    const blocks = document.querySelectorAll(
+      THINKING_SELECTORS.thinkingProcessBlock
+    )
+    if (blocks.length === 0) {
+      setTimeout(() => this.initWithRetry(retryCount + 1), retryDelay)
+      return
+    }
+
+    this.processExistingBlocks()
+    this.setupObserver()
+
+    // Set up periodic check
+    if (!this.checkInterval) {
+      this.checkInterval = window.setInterval(this.processExistingBlocks, 2000)
+    }
+  }
+
+  /**
+   * Initialize the block manager
+   */
+  initialize() {
+    console.log("Initializing thinking block manager...")
+    this.initWithRetry()
+  }
+
+  /**
+   * Clean up resources
+   */
+  cleanup() {
+    this.observer?.disconnect()
+    if (this.checkInterval) {
+      window.clearInterval(this.checkInterval)
+    }
+    this.roots.forEach((root) => root.unmount())
+    this.roots.clear()
+    this.processedBlocks.clear()
+  }
+}
+
+// Export a singleton instance
+const thinkingBlockManager = new ThinkingBlockManager()
+export { thinkingBlockManager }
diff --git a/extensions/chrome/src/content/react/thinking-process.tsx b/extensions/chrome/src/content/react/thinking-block.tsx
similarity index 98%
rename from extensions/chrome/src/content/react/thinking-process.tsx
rename to extensions/chrome/src/content/react/thinking-block.tsx
index fd71abb..313d896 100644
--- a/extensions/chrome/src/content/react/thinking-process.tsx
+++ b/extensions/chrome/src/content/react/thinking-block.tsx
@@ -9,7 +9,7 @@ import {
   CollapsibleTrigger,
 } from "@/components/ui/collapsible"
 
-interface ThinkingProcessProps {
+interface ThinkingBlockProps {
   containerRef: HTMLElement
   isStreaming: boolean
 }
@@ -31,7 +31,7 @@ const CodeViewer = ({ className, originalElement }: CodeViewerProps) => {
 export function ThinkingProcess({
   containerRef,
   isStreaming,
-}: ThinkingProcessProps) {
+}: ThinkingBlockProps) {
   const [isOpen, setIsOpen] = useState(true)
   const [codeElement, setCodeElement] = useState(null)
   const [isCopied, setIsCopied] = useState(false)
diff --git a/extensions/chrome/src/content/react/thinking-process-manager.tsx b/extensions/chrome/src/content/react/thinking-process-manager.tsx
deleted file mode 100644
index c5210fa..0000000
--- a/extensions/chrome/src/content/react/thinking-process-manager.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-import React from "react"
-
-import { createRoot, type Root } from "react-dom/client"
-
-import { ThinkingProcess } from "./thinking-process"
-
-// Selectors for finding elements in Claude's DOM
-const THINKING_SELECTORS = {
-  responseContainer: "[data-is-streaming]",
-  thinkingProcessBlock: ".grid.grid-cols-1 pre",
-  thinkingProcessContent: ".code-block__code",
-} as const
-
-interface MountedComponent {
-  root: Root
-  streamingObserver?: MutationObserver
-  container: HTMLElement
-  rootDiv: HTMLElement
-}
-
-class ThinkingProcessManager {
-  private mountedComponents: Map = new Map()
-  private observer: MutationObserver
-
-  constructor() {
-    this.observer = new MutationObserver(this.handleDOMChanges.bind(this))
-  }
-
-  public startObserving(): void {
-    console.log("[Thinking Claude] Starting observation...")
-    this.processExistingBlocks()
-
-    this.observer.observe(document.body, {
-      childList: true,
-      subtree: true,
-    })
-  }
-
-  public stopObserving(): void {
-    console.log("[Thinking Claude] Stopping observation...")
-    this.observer.disconnect()
-
-    // Cleanup all mounted components
-    this.mountedComponents.forEach(({ root, streamingObserver, rootDiv }) => {
-      streamingObserver?.disconnect()
-      root.unmount()
-      rootDiv.remove() // Remove the root div from DOM
-    })
-    this.mountedComponents.clear()
-  }
-
-  private processExistingBlocks(): void {
-    try {
-      const containers = document.querySelectorAll(
-        THINKING_SELECTORS.thinkingProcessBlock
-      )
-      containers.forEach(this.mountComponent.bind(this))
-    } catch (error) {
-      console.error(
-        "[Thinking Claude] Error processing existing blocks:",
-        error
-      )
-    }
-  }
-
-  private handleDOMChanges(mutations: MutationRecord[]): void {
-    for (const mutation of mutations) {
-      mutation.addedNodes.forEach((node) => {
-        if (!(node instanceof HTMLElement)) return
-
-        if (
-          node.matches(THINKING_SELECTORS.thinkingProcessBlock) &&
-          !this.isOwnRootDiv(node) &&
-          !node.querySelector("[data-thinking-process-root]")
-        ) {
-          this.mountComponent(node)
-        }
-
-        // Process child containers
-        node
-          .querySelectorAll(
-            THINKING_SELECTORS.thinkingProcessBlock
-          )
-          .forEach((container) => {
-            if (
-              !this.isOwnRootDiv(container) &&
-              !container.querySelector("[data-thinking-process-root]")
-            ) {
-              this.mountComponent(container)
-            }
-          })
-      })
-    }
-  }
-
-  private isOwnRootDiv(element: HTMLElement): boolean {
-    // Check if this element is one of our root divs
-    return Array.from(this.mountedComponents.values()).some(
-      ({ rootDiv }) => rootDiv === element
-    )
-  }
-
-  private mountComponent(container: HTMLElement): void {
-    try {
-      // Additional check at the start of mount
-      if (
-        this.mountedComponents.has(container) ||
-        container.querySelector("[data-thinking-process-root]")
-      ) {
-        return
-      }
-
-      // Get streaming state from the closest response container
-      const responseContainer = container.closest(
-        THINKING_SELECTORS.responseContainer
-      )
-      if (!responseContainer) {
-        console.log(
-          "[Thinking Claude] No response container found, skipping mount"
-        )
-        return
-      }
-
-      const isStreaming =
-        responseContainer.getAttribute("data-is-streaming") === "true"
-
-      // Create root div and append it to container
-      const rootDiv = document.createElement("div")
-      rootDiv.id = "thinking-claude-root"
-      rootDiv.setAttribute("data-thinking-process-root", "true")
-      rootDiv.className = "rounded-xl w-full"
-      // TODO: REMOVE THE MARGIN RIGHT WHEN WE HAVE A BETTER SOLUTION
-      container.appendChild(rootDiv)
-
-      // Create React root
-      const root = createRoot(rootDiv)
-
-      // Create streaming observer
-      const streamingObserver = new MutationObserver((mutations) => {
-        mutations.forEach((mutation) => {
-          if (
-            mutation.type === "attributes" &&
-            mutation.attributeName === "data-is-streaming" &&
-            mutation.target instanceof HTMLElement
-          ) {
-            const newIsStreaming =
-              mutation.target.getAttribute("data-is-streaming") === "true"
-            root.render(
-              
-            )
-          }
-        })
-      })
-
-      // Store component info before observing
-      this.mountedComponents.set(container, {
-        root,
-        streamingObserver,
-        container,
-        rootDiv,
-      })
-
-      // Start observing streaming state changes
-      streamingObserver.observe(responseContainer, {
-        attributes: true,
-        attributeFilter: ["data-is-streaming"],
-      })
-
-      // Initial render
-      root.render(
-        
-      )
-
-      console.log("[Thinking Claude] Mounted component to:", container)
-    } catch (error) {
-      console.error("[Thinking Claude] Error mounting component:", error)
-    }
-  }
-
-  private unmountComponent(element: HTMLElement): void {
-    try {
-      // Check if this element is a mounted container
-      const mounted = this.mountedComponents.get(element)
-      if (mounted) {
-        const { root, streamingObserver, rootDiv } = mounted
-        streamingObserver?.disconnect()
-        root.unmount()
-        rootDiv.remove() // Remove the root div from DOM
-        this.mountedComponents.delete(element)
-        console.log("[Thinking Claude] Unmounted component from:", element)
-        return
-      }
-
-      // Check if any mounted components are children of this element
-      for (const [container, component] of this.mountedComponents.entries()) {
-        if (element.contains(container)) {
-          const { root, streamingObserver, rootDiv } = component
-          streamingObserver?.disconnect()
-          root.unmount()
-          rootDiv.remove() // Remove the root div from DOM
-          this.mountedComponents.delete(container)
-          console.log(
-            "[Thinking Claude] Unmounted child component from:",
-            container
-          )
-        }
-      }
-    } catch (error) {
-      console.error("[Thinking Claude] Error unmounting component:", error)
-    }
-  }
-}
-export const thinkingProcessManager = new ThinkingProcessManager()

From 189437d90c2ef9fb5de20c395dcceb10aedb8af7 Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Tue, 26 Nov 2024 15:23:44 +0800
Subject: [PATCH 03/15] ref: streamline and organized the code for the new
 implementation thinking-block

build: remove old implmentation and temporary files
---
 .../react => components}/thinking-block.tsx   |  29 +-
 .../content/code-block-collapser/constants.ts |  58 ---
 .../code-block-collapser/index_collapser.tsx  | 340 ------------------
 extensions/chrome/src/content/index.ts        |   6 +-
 .../src/content/react/project_instruction.md  | 142 --------
 .../chrome/src/content/react/selectors.ts     |  81 -----
 .../content/react/thinking-block-manager.tsx  | 235 ------------
 .../src/content/thinking-block/README.md      |  16 +
 .../content/thinking-block/core/manager.tsx   | 144 ++++++++
 .../src/content/thinking-block/index.ts       |   2 +
 .../src/content/thinking-block/types/index.ts |  22 ++
 .../src/content/thinking-block/utils/dom.ts   |  46 +++
 .../content/thinking-block/utils/observer.ts  |  51 +++
 extensions/chrome/src/selectors/helper.ts     |  28 ++
 extensions/chrome/src/selectors/index.ts      |  51 +++
 extensions/chrome/src/utils/dom-utils.ts      |  44 ---
 16 files changed, 376 insertions(+), 919 deletions(-)
 rename extensions/chrome/src/{content/react => components}/thinking-block.tsx (85%)
 delete mode 100644 extensions/chrome/src/content/code-block-collapser/constants.ts
 delete mode 100644 extensions/chrome/src/content/code-block-collapser/index_collapser.tsx
 delete mode 100644 extensions/chrome/src/content/react/project_instruction.md
 delete mode 100644 extensions/chrome/src/content/react/selectors.ts
 delete mode 100644 extensions/chrome/src/content/react/thinking-block-manager.tsx
 create mode 100644 extensions/chrome/src/content/thinking-block/README.md
 create mode 100644 extensions/chrome/src/content/thinking-block/core/manager.tsx
 create mode 100644 extensions/chrome/src/content/thinking-block/index.ts
 create mode 100644 extensions/chrome/src/content/thinking-block/types/index.ts
 create mode 100644 extensions/chrome/src/content/thinking-block/utils/dom.ts
 create mode 100644 extensions/chrome/src/content/thinking-block/utils/observer.ts
 create mode 100644 extensions/chrome/src/selectors/helper.ts
 create mode 100644 extensions/chrome/src/selectors/index.ts
 delete mode 100644 extensions/chrome/src/utils/dom-utils.ts

diff --git a/extensions/chrome/src/content/react/thinking-block.tsx b/extensions/chrome/src/components/thinking-block.tsx
similarity index 85%
rename from extensions/chrome/src/content/react/thinking-block.tsx
rename to extensions/chrome/src/components/thinking-block.tsx
index 313d896..d2104f0 100644
--- a/extensions/chrome/src/content/react/thinking-block.tsx
+++ b/extensions/chrome/src/components/thinking-block.tsx
@@ -28,10 +28,10 @@ const CodeViewer = ({ className, originalElement }: CodeViewerProps) => {
   )
 }
 
-export function ThinkingProcess({
+export const ThinkingBlock: React.FC = ({
   containerRef,
   isStreaming,
-}: ThinkingBlockProps) {
+}) => {
   const [isOpen, setIsOpen] = useState(true)
   const [codeElement, setCodeElement] = useState(null)
   const [isCopied, setIsCopied] = useState(false)
@@ -88,16 +88,14 @@ export function ThinkingProcess({
           variant="ghost"
           onClick={handleCopy}
           disabled={!codeElement}
-          className="text-text-500 text-xs"
+          className="transition-all duration-200 ease-out text-text-500 text-xs hover:bg-bg-200 hover:text-text-300 p-1 py-0.5 h-6"
         >
           {isCopied ? (
-            
+            
           ) : (
-            
+            
           )}
-          
-            {isCopied ? "Copied" : "Copy"}
-          
+          Copy
           
             {isCopied ? "Copied" : "Copy thinking process"}
           
@@ -176,19 +174,18 @@ function CopyIcon(props: React.SVGProps) {
   )
 }
 
-function CheckIcon(props: React.SVGProps) {
+function TickIcon(props: React.SVGProps) {
   return (
     
-      
+      
     
   )
 }
diff --git a/extensions/chrome/src/content/code-block-collapser/constants.ts b/extensions/chrome/src/content/code-block-collapser/constants.ts
deleted file mode 100644
index b56b6a0..0000000
--- a/extensions/chrome/src/content/code-block-collapser/constants.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-export const SELECTORS = {
-  PRE: "pre",
-  CODE_CONTAINER: ".code-block__code",
-  MAIN_CONTAINER: ".relative.flex.flex-col",
-  THINKING_LABEL: ".text-text-300",
-  ORIGINAL_COPY_BTN: ".pointer-events-none",
-  CODE: "code",
-} as const
-
-export const CLASSES = {
-  THINKING_HEADER: "thinking-header",
-  COPY_CONTAINER:
-    "from-bg-300/90 to-bg-300/70 pointer-events-auto rounded-md bg-gradient-to-b p-0.5 backdrop-blur-md",
-  COPY_BUTTON:
-    "flex flex-row items-center gap-1 rounded-md p-1 py-0.5 text-xs transition-opacity delay-100 hover:bg-bg-200 opacity-60 hover:opacity-100",
-  COPY_TEXT: "text-text-200 pr-0.5",
-  TOGGLE_BUTTON: "flex items-center text-text-500 hover:text-text-300",
-  TOGGLE_LABEL: "font-medium text-sm",
-  THINKING_ANIMATION: "thinking-animation",
-} as const
-
-export const ANIMATION_STYLES = `
-    @keyframes gradientWave {
-        0% { background-position: 200% 50%; }
-        100% { background-position: -200% 50%; }
-    }
-    
-    .thinking-animation {
-        background: linear-gradient(
-            90deg,
-            rgba(156, 163, 175, 0.7) 0%,
-            rgba(209, 213, 219, 1) 25%,
-            rgba(156, 163, 175, 0.7) 50%,
-            rgba(209, 213, 219, 1) 75%,
-            rgba(156, 163, 175, 0.7) 100%
-        );
-        background-size: 200% 100%;
-        animation: gradientWave 3s linear infinite;
-        -webkit-background-clip: text;
-        -webkit-text-fill-color: transparent;
-        background-clip: text;
-        color: transparent;
-    }
-` as const
-
-export const ICONS = {
-  COPY: ``,
-  TICK: ``,
-  ARROW: ``,
-} as const
-
-export const TIMINGS = {
-  RETRY_DELAY: 1000,
-  MUTATION_DELAY: 100,
-  CHECK_INTERVAL: 2000,
-  COPY_FEEDBACK: 2000,
-  MAX_RETRIES: 10,
-} as const
diff --git a/extensions/chrome/src/content/code-block-collapser/index_collapser.tsx b/extensions/chrome/src/content/code-block-collapser/index_collapser.tsx
deleted file mode 100644
index f05fb2b..0000000
--- a/extensions/chrome/src/content/code-block-collapser/index_collapser.tsx
+++ /dev/null
@@ -1,340 +0,0 @@
-import {
-  ANIMATION_STYLES,
-  CLASSES,
-  ICONS,
-  SELECTORS,
-  TIMINGS,
-} from "./constants"
-
-// Initial style injection - runs immediately
-const initialStyles = document.createElement("style")
-initialStyles.id = "thinking-initial-styles"
-initialStyles.textContent = `
-  ${SELECTORS.PRE} {
-    opacity: 0;
-    transition: opacity 0.3s ease-in-out;
-  }
-`
-document.documentElement.appendChild(initialStyles)
-
-class CodeBlockCollapser {
-  private observers: Set
-  static instance: CodeBlockCollapser | null = null
-
-  static getInstance(): CodeBlockCollapser {
-    if (!CodeBlockCollapser.instance) {
-      CodeBlockCollapser.instance = new CodeBlockCollapser()
-    }
-    return CodeBlockCollapser.instance
-  }
-
-  private constructor() {
-    this.observers = new Set()
-    this.injectStyles()
-    this.initWithRetry()
-
-    window.addEventListener("unload", () => this.cleanup())
-  }
-
-  private injectStyles() {
-    // Remove initial hiding style
-    const initialStyle = document.getElementById("thinking-initial-styles")
-    if (initialStyle) {
-      initialStyle.remove()
-    }
-
-    // Inject permanent styles
-    if (!document.getElementById("thinking-styles")) {
-      const styleSheet = document.createElement("style")
-      styleSheet.id = "thinking-styles"
-      styleSheet.textContent = `
-        ${ANIMATION_STYLES}
-        ${SELECTORS.PRE} {
-          opacity: 1;
-          transition: opacity 0.3s ease-in-out;
-        }
-      `
-      document.head.appendChild(styleSheet)
-    }
-  }
-
-  createElement(
-    tag: string,
-    className: string = "",
-    innerHTML: string = ""
-  ): HTMLElement {
-    const element = document.createElement(tag)
-    if (className) element.className = className
-    if (innerHTML) element.innerHTML = innerHTML
-    return element
-  }
-
-  createCopyButton() {
-    const container = this.createElement("div", CLASSES.COPY_CONTAINER)
-    const button = this.createElement("button", CLASSES.COPY_BUTTON)
-    const iconSpan = this.createElement("span", "", ICONS.COPY)
-    const textSpan = this.createElement("span", CLASSES.COPY_TEXT, "Copy")
-
-    button.append(iconSpan, textSpan)
-    container.appendChild(button)
-
-    button.addEventListener("click", () => {
-      const preElement = button.closest(SELECTORS.PRE)
-      const codeElement = preElement?.querySelector(SELECTORS.CODE)
-      const codeText = codeElement?.textContent
-
-      if (!codeText) return
-
-      navigator.clipboard
-        .writeText(codeText)
-        .then(() => {
-          iconSpan.innerHTML = ICONS.TICK
-          textSpan.textContent = "Copied!"
-
-          setTimeout(() => {
-            iconSpan.innerHTML = ICONS.COPY
-            textSpan.textContent = "Copy"
-          }, TIMINGS.COPY_FEEDBACK)
-        })
-        .catch((error) => {
-          console.error("Failed to copy:", error)
-        })
-    })
-
-    return container
-  }
-
-  createToggleButton(isStreaming: boolean = false) {
-    const button = this.createElement("button", CLASSES.TOGGLE_BUTTON)
-    const labelText = isStreaming
-      ? "Claude is thinking..."
-      : "View thinking process"
-    button.innerHTML = `
-        ${ICONS.ARROW}
-        ${labelText}
-      `
-    return button
-  }
-
-  updateHeaderState(headerContainer: HTMLElement, isStreaming: boolean) {
-    const toggleBtn = headerContainer.querySelector(
-      `.${CLASSES.TOGGLE_BUTTON}`
-    )
-    if (!toggleBtn) return
-
-    const label = toggleBtn.querySelector("span")
-    if (!label) return
-
-    label.textContent = isStreaming
-      ? "Claude is thinking..."
-      : "View thinking process"
-
-    if (isStreaming) {
-      label.classList.add(CLASSES.THINKING_ANIMATION)
-    } else {
-      label.classList.remove(CLASSES.THINKING_ANIMATION)
-    }
-  }
-
-  setupCodeContainer(container: HTMLElement, toggleBtn: HTMLElement) {
-    if (!container) return
-
-    container.style.cssText = `
-        background: var(--bg-300);
-        transition: all 0.3s ease-in-out;
-        overflow-x: hidden;
-        overflow-y: auto;
-        padding: 1em;
-        max-width: 100%;
-        display: block;
-        max-height: 50vh;
-        opacity: 1;
-      `
-
-    const codeElement = container.querySelector(SELECTORS.CODE)
-    if (!codeElement) return
-
-    codeElement.style.cssText = `
-          white-space: pre-wrap !important;
-          word-break: break-word !important;
-          overflow-wrap: break-word !important;
-          display: block !important;
-          max-width: 100% !important;
-        `
-
-    const arrow = toggleBtn.querySelector("svg")
-    const label = toggleBtn.querySelector("span")
-    if (!arrow || !label) return
-
-    // Set initial state to open
-    arrow.style.transform = "rotate(180deg)"
-    if (!label.classList.contains(CLASSES.THINKING_ANIMATION)) {
-      label.textContent = "Hide thinking process"
-    }
-
-    toggleBtn.addEventListener("click", () => {
-      const shouldToggleOpen = container.style.maxHeight === "0px"
-      container.style.maxHeight = shouldToggleOpen ? "50vh" : "0"
-      container.style.opacity = shouldToggleOpen ? "1" : "0"
-      container.style.padding = shouldToggleOpen ? "1em" : "0"
-
-      arrow.style.transform = `rotate(${shouldToggleOpen ? 180 : 0}deg)`
-      if (!label.classList.contains(CLASSES.THINKING_ANIMATION)) {
-        label.textContent = shouldToggleOpen
-          ? "Hide thinking process"
-          : "View thinking process"
-      }
-    })
-  }
-
-  processBlock(pre: HTMLElement) {
-    const headerContainer = this.createElement("div", CLASSES.THINKING_HEADER)
-    headerContainer.style.cssText =
-      "display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: var(--bg-300);"
-
-    const isStreaming = pre.closest('[data-is-streaming="true"]') !== null
-    const toggleBtn = this.createToggleButton(isStreaming)
-    const copyBtn = this.createCopyButton()
-
-    headerContainer.append(toggleBtn, copyBtn)
-
-    const codeContainer = pre.querySelector(
-      SELECTORS.CODE_CONTAINER
-    )
-    if (!codeContainer) return
-    this.setupCodeContainer(codeContainer, toggleBtn)
-
-    const mainContainer = pre.querySelector(
-      SELECTORS.MAIN_CONTAINER
-    )
-    if (!mainContainer) return
-
-    const codeParentElement = pre.querySelector(
-      SELECTORS.CODE_CONTAINER
-    )?.parentElement
-    if (!codeParentElement) return
-
-    mainContainer.insertBefore(headerContainer, codeParentElement)
-
-    const streamingContainer = pre.closest("[data-is-streaming]")
-    if (streamingContainer) {
-      const observer = new MutationObserver((mutations) => {
-        for (const mutation of mutations) {
-          if (
-            mutation.type === "attributes" &&
-            mutation.attributeName === "data-is-streaming"
-          ) {
-            const isStreamingNow =
-              streamingContainer.getAttribute("data-is-streaming") === "true"
-            this.updateHeaderState(headerContainer, isStreamingNow)
-          }
-        }
-      })
-
-      observer.observe(streamingContainer, {
-        attributes: true,
-        attributeFilter: ["data-is-streaming"],
-      })
-
-      this.observers.add(observer)
-
-      new MutationObserver(() => {
-        if (!document.contains(streamingContainer)) {
-          observer.disconnect()
-          this.observers.delete(observer)
-        }
-      }).observe(document.body, { childList: true, subtree: true })
-    }
-
-    for (const selector of [
-      SELECTORS.THINKING_LABEL,
-      SELECTORS.ORIGINAL_COPY_BTN,
-    ]) {
-      const element = pre.querySelector(selector) as HTMLElement | null
-      if (element) element.style.display = "none"
-    }
-  }
-
-  processExistingBlocks(): void {
-    for (const pre of document.querySelectorAll(SELECTORS.PRE)) {
-      const header = pre.querySelector(SELECTORS.THINKING_LABEL)
-      if (!header?.textContent) continue
-
-      if (
-        header.textContent.trim() === "thinking" &&
-        !pre.querySelector(`.${CLASSES.THINKING_HEADER}`)
-      ) {
-        this.processBlock(pre)
-      }
-    }
-  }
-
-  initWithRetry(retryCount: number = 0) {
-    if (retryCount >= TIMINGS.MAX_RETRIES) return
-
-    const blocks = document.querySelectorAll(SELECTORS.PRE)
-    if (blocks.length === 0) {
-      setTimeout(() => this.initWithRetry(retryCount + 1), TIMINGS.RETRY_DELAY)
-      return
-    }
-
-    this.processExistingBlocks()
-    this.setupObserver()
-    this.setupPeriodicCheck()
-  }
-
-  setupObserver() {
-    const observer = new MutationObserver((mutations) => {
-      let shouldProcess = false
-      for (const mutation of mutations) {
-        if (
-          mutation.addedNodes.length > 0 ||
-          (mutation.type === "attributes" &&
-            mutation.attributeName === "data-is-streaming")
-        ) {
-          shouldProcess = true
-        }
-      }
-
-      if (shouldProcess) {
-        this.processExistingBlocks()
-      }
-    })
-
-    observer.observe(document.body, {
-      childList: true,
-      subtree: true,
-      attributes: true,
-      attributeFilter: ["data-is-streaming"],
-    })
-
-    this.observers.add(observer)
-  }
-
-  setupPeriodicCheck(): void {
-    setInterval(() => {
-      this.processExistingBlocks()
-    }, TIMINGS.CHECK_INTERVAL)
-  }
-
-  cleanup(): void {
-    this.observers.forEach((observer) => observer.disconnect())
-    this.observers.clear()
-  }
-}
-
-declare global {
-  interface Window {
-    codeBlockCollapser?: CodeBlockCollapser
-  }
-}
-
-CodeBlockCollapser.getInstance()
-
-document.addEventListener("DOMContentLoaded", () => {
-  if (!window.codeBlockCollapser) {
-    window.codeBlockCollapser = CodeBlockCollapser.getInstance()
-  }
-})
diff --git a/extensions/chrome/src/content/index.ts b/extensions/chrome/src/content/index.ts
index 8676ab1..7bae612 100644
--- a/extensions/chrome/src/content/index.ts
+++ b/extensions/chrome/src/content/index.ts
@@ -2,7 +2,7 @@ import "@/styles/globals.css"
 
 import { shouldInitialize } from "@/utils/url-utils"
 
-import { thinkingBlockManager } from "./react/thinking-block-manager"
+import { thinkingBlockManager } from "./thinking-block"
 
 const initializeExtension = async () => {
   console.log("[Thinking Claude] Starting extension initialization...")
@@ -36,7 +36,7 @@ const initializeExtension = async () => {
   const style = document.createElement("style")
   style.textContent = `
     /* Only hide elements that have our enhanced version */
-    pre > div:first-child:has(+ div.thinking-block-container) {
+    pre > div:first-child:has(+ div.tc-thinking-block-container) {
       position: absolute !important;
       opacity: 0 !important;
       pointer-events: none !important;
@@ -81,7 +81,7 @@ const initializeExtension = async () => {
     if (initialStyles) {
       // Fade blocks back in if our enhanced UI failed to mount
       initialStyles.textContent = `
-        pre > div:first-child:not(:has(+ div.thinking-block-container)) {
+        pre > div:first-child:not(:has(+ div.tc-thinking-block-container)) {
           opacity: 1;
           transition: opacity 0.2s ease-out;
         }
diff --git a/extensions/chrome/src/content/react/project_instruction.md b/extensions/chrome/src/content/react/project_instruction.md
deleted file mode 100644
index 9f13bdd..0000000
--- a/extensions/chrome/src/content/react/project_instruction.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# Thinking Claude Extension
-
-A Chrome extension that enhances Claude AI's thinking process visualization with dynamic React components.
-
-## Project Overview
-
-This extension enhances Claude.ai's thinking process blocks by making them more readable and interactive while maintaining the original UI aesthetics.
-
-### Core Features
-
-- Collapsible thinking process blocks
-- Dynamic streaming state handling
-- Auto-wrapping content with vertical scrolling
-- Preserved copy functionality
-- Original UI style maintenance
-
-## Technical Implementation
-
-### Core Architecture
-
-#### ThinkingBlockManager (Singleton)
-
-- Central manager for block detection and enhancement
-- Handles initialization, processing, and cleanup
-- Maintains state of processed blocks and React roots
-- Provides graceful fallback to original UI
-
-#### Block Processing
-
-- Unique block ID generation and tracking
-- React component mounting in DOM
-- Streaming state detection
-- Automatic cleanup of unmounted components
-
-#### CSS Management
-
-- Multi-stage CSS injection for FOUC prevention
-- Graceful fallback with smooth transitions
-- Original UI preservation for error cases
-- Scoped styles for enhanced components
-
-### Implementation Details
-
-#### Block Detection
-
-- Use data attributes for tracking (data-tc-block-id, data-tc-container-id)
-- Process blocks only once, with reprocessing capability
-- Handle streaming state changes via mutation observer
-- Clean up React roots and processed state on unmount
-
-#### CSS Strategy
-
-1. Initial styles (prevent FOUC)
-   - Hide blocks with transition
-   - Immediate injection on page load
-2. Enhanced UI styles
-   - Scoped to processed blocks
-   - Preserve original UI as fallback
-3. Transition handling
-   - Smooth transitions between states
-   - Graceful fallback if enhancement fails
-
-#### Resource Management
-
-- Disconnect mutation observer
-- Clear interval checks
-- Unmount React roots
-- Remove processed block tracking
-- Clean up injected styles
-
-### Best Practices
-
-#### Error Handling
-
-- Graceful fallback to original UI
-- Clear error logging
-- Resource cleanup on failure
-
-#### Performance
-
-- Minimal DOM operations
-- Efficient block tracking
-- Optimized mutation observer
-
-#### User Experience
-
-- Smooth transitions
-- No visual disruption
-- Consistent behavior
-
-### Known Limitations
-
-1. Initial page load may show brief flash of original UI
-2. Streaming state detection depends on Claude's DOM structure
-3. React component remounting during streaming responses
-
-### Future Improvements
-
-#### Performance
-
-- Optimize block detection
-- Reduce style recalculations
-- Improve streaming handling
-
-#### Features
-
-- User configuration options
-- Additional visual enhancements
-- More interactive elements
-
-#### Testing
-
-- Unit tests for core functionality
-- Integration tests for UI components
-- Performance benchmarking
-
-### Development Guidelines
-
-#### Code Organization
-
-- Keep ThinkingBlockManager as single source of truth
-- Use TypeScript for type safety
-- Follow React best practices
-- Use kebab-case for file names
-- Use descriptive variable names
-- Document code thoroughly for OSS contributors
-
-#### Style Management
-
-- Use Tailwind CSS to match Claude's UI
-- Copy exact class names where possible
-- Maintain clear CSS hierarchy
-- Use scoped styles when possible
-- Handle transitions smoothly
-
-#### Testing
-
-- Test error scenarios
-- Verify cleanup functionality
-- Check streaming state handling
-- Test with various content lengths
-- Verify component lifecycle
diff --git a/extensions/chrome/src/content/react/selectors.ts b/extensions/chrome/src/content/react/selectors.ts
deleted file mode 100644
index c898a56..0000000
--- a/extensions/chrome/src/content/react/selectors.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/** Sequence of selectors from outside to inside
- * 1. responseContainer: '[data-is-streaming]'
- *    The outermost container div with data-is-streaming attribute, containing the entire Claude response including thinking process, message, and footer. Has gradient background and rounded corners.
- *
- * 2. thinkingProcessBlock: 'pre, .text-text-300, .code-block__code'
- *    The 
 element, holds the entire thinking process section including header and content.
- *    Has rounded corners and specific styling.
- *
- * 3. thinkingLabel: '.text-text-300'
- *    The div containing the text label for the thinking process.
- *    Located at the top of the thinking process block.
- *
- * 4. thinkingProcessContentContainer: '.code-block__code'
- *    The div with class "code-block__code" that wraps the thinking process content.
- *    Has specific background color, padding, and code styling properties.
- *
- * 5. thinkingProcessContent: 'code'
- *    The innermost  element, containing the actual thinking process text.
- *    Content is wrapped in  elements.
- *
- * 6. mainContainer: '.relative.flex.flex-col'
- *    The main container div with specific styling and layout.
- *
- * 7. originalCopyBtn: '.pointer-events-none'
- *    The copy button with pointer-events-none.
- *    Located in the header area, includes an SVG icon and "Copy" text.
- *
- * 8. enhancedContainer: '[data-tc-container-id]'
- *    The container element with data-tc-container-id attribute.
- *
- * 9. processedElement: '[data-tc-processed]'
- *    The element with data-tc-processed attribute.
- */
-
-/** Selectors for finding and interacting with Claude's thinking blocks */
-export const THINKING_SELECTORS = {
-  // Container selectors
-  responseContainer: "[data-is-streaming]",
-  mainContainer: ".relative.flex.flex-col",
-
-  // Core thinking block selectors
-  thinkingProcessBlock:
-    "pre, .text-text-300, .code-block__code, [class*='text-text-'], [class*='code-block']",
-  thinkingLabel: ".text-text-300, [class*='text-text-']",
-  thinkingProcessContentContainer: ".code-block__code, [class*='code-block']",
-  thinkingProcessContent: "code",
-
-  // UI elements
-  originalCopyBtn: ".pointer-events-none",
-
-  // Enhanced elements
-  enhancedContainer: "[data-tc-container-id]",
-  processedElement: "[data-tc-processed]",
-} as const
-
-// Helper function to check if element is a thinking block
-export const isThinkingBlock = (
-  element: Element | null
-): element is HTMLDivElement => {
-  if (!element || !(element instanceof HTMLDivElement)) {
-    return false
-  }
-
-  // First check if the element itself is a thinking label or code container
-  if (
-    element.matches(THINKING_SELECTORS.thinkingLabel) ||
-    element.matches(THINKING_SELECTORS.thinkingProcessContentContainer)
-  ) {
-    return true
-  }
-
-  // Then check for child elements
-  const hasThinkingLabel = !!element.querySelector(
-    THINKING_SELECTORS.thinkingLabel
-  )
-  const hasCodeContainer = !!element.querySelector(
-    THINKING_SELECTORS.thinkingProcessContentContainer
-  )
-
-  return hasThinkingLabel || hasCodeContainer
-}
diff --git a/extensions/chrome/src/content/react/thinking-block-manager.tsx b/extensions/chrome/src/content/react/thinking-block-manager.tsx
deleted file mode 100644
index ca1f090..0000000
--- a/extensions/chrome/src/content/react/thinking-block-manager.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import { createRoot } from "react-dom/client"
-
-import { THINKING_SELECTORS } from "./selectors"
-import { ThinkingProcess } from "./thinking-block"
-
-/**
- * Manages the detection and enhancement of thinking process blocks in Claude's responses
- */
-class ThinkingBlockManager {
-  private observer: MutationObserver | null = null
-  private checkInterval: number | null = null
-  private processedBlocks = new Set()
-  private roots = new Map>()
-
-  /**
-   * Process a thinking block element by mounting a React component
-   */
-  private processBlock(block: Element) {
-    // Check if block needs reprocessing
-    const blockId = block.getAttribute("data-tc-block-id")
-    const existingContainer = blockId
-      ? document.querySelector(`[data-tc-container-id="${blockId}"]`)
-      : null
-
-    // If block was previously processed but container is missing, clean up
-    if (blockId && !existingContainer) {
-      this.roots.get(blockId)?.unmount()
-      this.roots.delete(blockId)
-      this.processedBlocks.delete(block)
-      // Remove processed attributes to allow reprocessing
-      block.querySelectorAll("[data-tc-processed]").forEach((el) => {
-        el.removeAttribute("data-tc-processed")
-        ;(el as HTMLElement).style.display = ""
-      })
-    }
-
-    // Skip if already processed and container exists
-    if (this.processedBlocks.has(block) && existingContainer) return
-
-    // Check for thinking label
-    const thinkingLabel = block.querySelector(THINKING_SELECTORS.thinkingLabel)
-    if (!thinkingLabel || thinkingLabel.textContent?.trim() !== "thinking")
-      return
-
-    // Create container for React component
-    const container = document.createElement("div")
-    container.className = "thinking-block-container"
-
-    // Get code container and content
-    const codeContainer = block.querySelector(
-      THINKING_SELECTORS.thinkingProcessContentContainer
-    )
-    const codeContent = block.querySelector(
-      THINKING_SELECTORS.thinkingProcessContent
-    )
-    if (!codeContainer || !codeContent) return
-
-    // Check if block is streaming
-    const isStreaming =
-      block
-        .closest(THINKING_SELECTORS.responseContainer)
-        ?.getAttribute("data-is-streaming") === "true"
-
-    // Mount React component
-    try {
-      // Generate unique ID for the block if it doesn't exist
-      const newBlockId =
-        blockId ||
-        `tc-block-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
-      if (!blockId) {
-        block.setAttribute("data-tc-block-id", newBlockId)
-      }
-      container.setAttribute("data-tc-container-id", newBlockId)
-
-      const root = createRoot(container)
-      this.roots.set(newBlockId, root)
-      root.render(
-        
-      )
-
-      // Insert React component
-      const mainContainer = block.querySelector(
-        THINKING_SELECTORS.mainContainer
-      )
-      if (mainContainer) {
-        mainContainer.insertBefore(container, codeContainer.parentElement)
-        this.processedBlocks.add(block)
-
-        // Mark elements as processed
-        const elementsToHide = [
-          block.querySelector(THINKING_SELECTORS.thinkingLabel),
-          block.querySelector(THINKING_SELECTORS.originalCopyBtn),
-          codeContainer,
-        ]
-        elementsToHide.forEach((el) => {
-          if (el) {
-            ;(el as HTMLElement).style.display = "none"
-            el.setAttribute("data-tc-processed", "true")
-          }
-        })
-
-        console.log("[TC Block] Successfully processed block:", {
-          blockId: newBlockId,
-          isStreaming,
-          hasThinkingLabel: !!block.querySelector(
-            THINKING_SELECTORS.thinkingLabel
-          ),
-          hasCodeContainer: !!codeContainer,
-          hasCodeContent: !!codeContent,
-          mountPoint: mainContainer.tagName,
-          containerClass: container.className,
-          timing: {
-            processedAt: new Date().toISOString(),
-            blockCount: this.processedBlocks.size,
-          },
-        })
-      }
-    } catch (error) {
-      console.error("Error mounting thinking block:", error)
-    }
-  }
-
-  /**
-   * Process all existing thinking blocks
-   */
-  private processExistingBlocks = () => {
-    const blocks = document.querySelectorAll(
-      THINKING_SELECTORS.thinkingProcessBlock
-    )
-    blocks.forEach((block) => this.processBlock(block))
-  }
-
-  /**
-   * Set up mutation observer to detect new thinking blocks
-   */
-  private setupObserver() {
-    if (this.observer) return
-
-    this.observer = new MutationObserver((mutations) => {
-      let shouldProcess = false
-      for (const mutation of mutations) {
-        // Check for added nodes
-        if (mutation.addedNodes.length > 0) {
-          shouldProcess = true
-          break
-        }
-        // Check for removed nodes that might be our containers
-        if (mutation.removedNodes.length > 0) {
-          for (const node of mutation.removedNodes) {
-            if (
-              node instanceof Element &&
-              node.matches(".thinking-block-container")
-            ) {
-              shouldProcess = true
-              break
-            }
-          }
-        }
-        // Check for streaming attribute changes
-        if (
-          mutation.type === "attributes" &&
-          mutation.attributeName === "data-is-streaming"
-        ) {
-          shouldProcess = true
-          break
-        }
-      }
-
-      if (shouldProcess) {
-        setTimeout(this.processExistingBlocks, 100) // Small delay to ensure DOM is ready
-      }
-    })
-
-    this.observer.observe(document.body, {
-      childList: true,
-      subtree: true,
-      attributes: true,
-      attributeFilter: ["data-is-streaming"],
-    })
-  }
-
-  /**
-   * Initialize with retry logic
-   */
-  private initWithRetry(retryCount = 0) {
-    const maxRetries = 10
-    const retryDelay = 1000
-
-    if (retryCount >= maxRetries) return
-
-    const blocks = document.querySelectorAll(
-      THINKING_SELECTORS.thinkingProcessBlock
-    )
-    if (blocks.length === 0) {
-      setTimeout(() => this.initWithRetry(retryCount + 1), retryDelay)
-      return
-    }
-
-    this.processExistingBlocks()
-    this.setupObserver()
-
-    // Set up periodic check
-    if (!this.checkInterval) {
-      this.checkInterval = window.setInterval(this.processExistingBlocks, 2000)
-    }
-  }
-
-  /**
-   * Initialize the block manager
-   */
-  initialize() {
-    console.log("Initializing thinking block manager...")
-    this.initWithRetry()
-  }
-
-  /**
-   * Clean up resources
-   */
-  cleanup() {
-    this.observer?.disconnect()
-    if (this.checkInterval) {
-      window.clearInterval(this.checkInterval)
-    }
-    this.roots.forEach((root) => root.unmount())
-    this.roots.clear()
-    this.processedBlocks.clear()
-  }
-}
-
-// Export a singleton instance
-const thinkingBlockManager = new ThinkingBlockManager()
-export { thinkingBlockManager }
diff --git a/extensions/chrome/src/content/thinking-block/README.md b/extensions/chrome/src/content/thinking-block/README.md
new file mode 100644
index 0000000..9472391
--- /dev/null
+++ b/extensions/chrome/src/content/thinking-block/README.md
@@ -0,0 +1,16 @@
+# Thinking Block Manager
+
+This directory contains the core functionality for managing Claude's thinking blocks.
+
+## Structure
+
+- `core/` - Core manager and types
+  - `manager.ts` - Main thinking block manager class
+  - `types.ts` - TypeScript interfaces and types
+- `utils/` - Utility functions
+  - `dom.ts` - DOM manipulation utilities
+  - `observer.ts` - MutationObserver setup and handling
+- `components/` - React components
+  - `ThinkingBlock.tsx` - Main thinking block component
+- `constants/` - Constants and configurations
+  - `selectors.ts` - DOM selectors
diff --git a/extensions/chrome/src/content/thinking-block/core/manager.tsx b/extensions/chrome/src/content/thinking-block/core/manager.tsx
new file mode 100644
index 0000000..7578611
--- /dev/null
+++ b/extensions/chrome/src/content/thinking-block/core/manager.tsx
@@ -0,0 +1,144 @@
+import { CLAUDE_ORIGINAL_SELECTORS } from "@/selectors"
+import { createRoot } from "react-dom/client"
+
+import { ThinkingBlock } from "@/components/thinking-block"
+
+import { ThinkingBlockRoots } from "../types"
+import {
+  createThinkingBlockContainer,
+  generateBlockId,
+  getThinkingBlockElements,
+  hideElement,
+  isStreaming,
+  isThinkingBlock,
+} from "../utils/dom"
+import { createMutationObserver, observeDOM } from "../utils/observer"
+
+class ThinkingBlockManager {
+  private observer: MutationObserver | null = null
+  private checkInterval: number | null = null
+  private processedBlocks = new Set()
+  private roots: ThinkingBlockRoots = {}
+
+  private processBlock(block: Element) {
+    // Check if block needs reprocessing
+    const blockId = block.getAttribute("data-tc-block-id")
+    const existingContainer = blockId
+      ? document.querySelector(`[data-tc-container-id="${blockId}"]`)
+      : null
+
+    // If block was previously processed but container is missing, clean up
+    if (blockId && !existingContainer) {
+      this.roots[blockId]?.unmount()
+      delete this.roots[blockId]
+      this.processedBlocks.delete(block)
+      block.querySelectorAll("[data-tc-processed]").forEach((el) => {
+        el.removeAttribute("data-tc-processed")
+        ;(el as HTMLElement).style.display = ""
+      })
+    }
+
+    // Skip if already processed and container exists
+    if (this.processedBlocks.has(block) && existingContainer) return
+
+    // Validate thinking block
+    if (!isThinkingBlock(block)) return
+
+    // Get DOM elements
+    const { codeContainer, codeContent, mainContainer } =
+      getThinkingBlockElements(block)
+
+    if (!codeContainer || !codeContent || !mainContainer) return
+
+    try {
+      // Generate or use existing block ID
+      const newBlockId = blockId || generateBlockId()
+
+      if (!blockId) {
+        block.setAttribute("data-tc-block-id", newBlockId)
+      }
+
+      // Create and setup container
+      const container = createThinkingBlockContainer(newBlockId)
+
+      // Create React root and render component
+      const root = createRoot(container)
+      // set root in map
+      this.roots[newBlockId] = root
+
+      root.render(
+        
+      )
+
+      // Insert React component and hide original elements
+      mainContainer.insertBefore(container, codeContainer.parentElement)
+      this.processedBlocks.add(block)
+
+      // Hide original elements
+      const elementsToHide = [
+        block.querySelector(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel),
+        block.querySelector(CLAUDE_ORIGINAL_SELECTORS.originalCopyBtn),
+        codeContainer,
+      ]
+      elementsToHide.forEach(hideElement)
+    } catch (error) {
+      console.error("Error mounting thinking block:", error)
+    }
+  }
+
+  private processExistingBlocks = () => {
+    const blocks = document.querySelectorAll(
+      CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessBlock
+    )
+    blocks.forEach((block) => this.processBlock(block))
+  }
+
+  private initWithRetry(retryCount = 0) {
+    const maxRetries = 10
+    const retryDelay = 1000
+
+    if (retryCount >= maxRetries) return
+
+    const blocks = document.querySelectorAll(
+      CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessBlock
+    )
+    if (blocks.length === 0) {
+      setTimeout(() => this.initWithRetry(retryCount + 1), retryDelay)
+      return
+    }
+
+    this.processExistingBlocks()
+
+    // Setup observer if not already setup
+    if (!this.observer) {
+      this.observer = createMutationObserver(this.processExistingBlocks)
+      observeDOM(this.observer)
+    }
+
+    // Set up periodic check if not already setup
+    if (!this.checkInterval) {
+      this.checkInterval = window.setInterval(this.processExistingBlocks, 2000)
+    }
+  }
+
+  initialize() {
+    console.log("Initializing thinking block manager...")
+    this.initWithRetry()
+  }
+
+  cleanup() {
+    this.observer?.disconnect()
+    if (this.checkInterval) {
+      window.clearInterval(this.checkInterval)
+    }
+    Object.values(this.roots).forEach((root) => root.unmount())
+    this.roots = {}
+    this.processedBlocks.clear()
+  }
+}
+
+// Export singleton instance
+export const thinkingBlockManager = new ThinkingBlockManager()
diff --git a/extensions/chrome/src/content/thinking-block/index.ts b/extensions/chrome/src/content/thinking-block/index.ts
new file mode 100644
index 0000000..96b6ae8
--- /dev/null
+++ b/extensions/chrome/src/content/thinking-block/index.ts
@@ -0,0 +1,2 @@
+export { thinkingBlockManager } from "./core/manager"
+export type { ThinkingBlockState, ProcessedBlock } from "./types"
diff --git a/extensions/chrome/src/content/thinking-block/types/index.ts b/extensions/chrome/src/content/thinking-block/types/index.ts
new file mode 100644
index 0000000..4763b5b
--- /dev/null
+++ b/extensions/chrome/src/content/thinking-block/types/index.ts
@@ -0,0 +1,22 @@
+import { Root } from "react-dom/client"
+
+export interface ThinkingBlockState {
+  isStreaming: boolean
+  blockId: string
+}
+
+export interface ProcessedBlock {
+  element: Element
+  state: ThinkingBlockState
+}
+
+export interface ThinkingBlockRoots {
+  [key: string]: Root
+}
+
+export interface DOMElements {
+  thinkingLabel: Element | null
+  codeContainer: Element | null
+  codeContent: Element | null
+  mainContainer: Element | null
+}
diff --git a/extensions/chrome/src/content/thinking-block/utils/dom.ts b/extensions/chrome/src/content/thinking-block/utils/dom.ts
new file mode 100644
index 0000000..f10fc97
--- /dev/null
+++ b/extensions/chrome/src/content/thinking-block/utils/dom.ts
@@ -0,0 +1,46 @@
+import { CLAUDE_ORIGINAL_SELECTORS } from "@/selectors"
+
+import { DOMElements } from "../types"
+
+export const generateBlockId = () =>
+  `tc-block-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
+
+export const getThinkingBlockElements = (block: Element): DOMElements => ({
+  thinkingLabel: block.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel
+  ),
+  codeContainer: block.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContentContainer
+  ),
+  codeContent: block.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContent
+  ),
+  mainContainer: block.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeMainContainer
+  ),
+})
+
+export const isThinkingBlock = (block: Element): boolean => {
+  const { thinkingLabel } = getThinkingBlockElements(block)
+  return thinkingLabel?.textContent?.trim().toLowerCase() === "thinking"
+}
+
+export const isStreaming = (block: Element): boolean =>
+  block
+    .closest(CLAUDE_ORIGINAL_SELECTORS.claudeResponseContainer)
+    ?.getAttribute("data-is-streaming") === "true"
+
+export const hideElement = (element: Element | null) => {
+  if (!element) return
+  ;(element as HTMLElement).style.display = "none"
+  element.setAttribute("data-tc-processed", "true")
+}
+
+export const createThinkingBlockContainer = (
+  blockId: string
+): HTMLDivElement => {
+  const container = document.createElement("div")
+  container.className = "tc-thinking-block-container"
+  container.setAttribute("data-tc-container-id", blockId)
+  return container
+}
diff --git a/extensions/chrome/src/content/thinking-block/utils/observer.ts b/extensions/chrome/src/content/thinking-block/utils/observer.ts
new file mode 100644
index 0000000..4783dcf
--- /dev/null
+++ b/extensions/chrome/src/content/thinking-block/utils/observer.ts
@@ -0,0 +1,51 @@
+export const createMutationObserver = (
+  onMutation: () => void,
+  delay: number = 100
+): MutationObserver => {
+  return new MutationObserver((mutations) => {
+    let shouldProcess = false
+
+    for (const mutation of mutations) {
+      // Check for added nodes
+      if (mutation.addedNodes.length > 0) {
+        shouldProcess = true
+        break
+      }
+
+      // Check for removed nodes that might be our containers
+      if (mutation.removedNodes.length > 0) {
+        for (const node of mutation.removedNodes) {
+          if (
+            node instanceof Element &&
+            node.matches(".tc-thinking-block-container")
+          ) {
+            shouldProcess = true
+            break
+          }
+        }
+      }
+
+      // Check for streaming attribute changes
+      if (
+        mutation.type === "attributes" &&
+        mutation.attributeName === "data-is-streaming"
+      ) {
+        shouldProcess = true
+        break
+      }
+    }
+
+    if (shouldProcess) {
+      setTimeout(onMutation, delay)
+    }
+  })
+}
+
+export const observeDOM = (observer: MutationObserver): void => {
+  observer.observe(document.body, {
+    childList: true,
+    subtree: true,
+    attributes: true,
+    attributeFilter: ["data-is-streaming"],
+  })
+}
diff --git a/extensions/chrome/src/selectors/helper.ts b/extensions/chrome/src/selectors/helper.ts
new file mode 100644
index 0000000..f216984
--- /dev/null
+++ b/extensions/chrome/src/selectors/helper.ts
@@ -0,0 +1,28 @@
+import { CLAUDE_ORIGINAL_SELECTORS } from "."
+
+// Helper function to check if element is a thinking block
+export const isThinkingBlock = (
+  element: Element | null
+): element is HTMLDivElement => {
+  if (!element || !(element instanceof HTMLDivElement)) {
+    return false
+  }
+
+  // First check if the element itself is a thinking label or code container
+  if (
+    element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel) ||
+    element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContent)
+  ) {
+    return true
+  }
+
+  // Then check for child elements
+  const hasThinkingLabel = !!element.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel
+  )
+  const hasCodeContainer = !!element.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContentContainer
+  )
+
+  return hasThinkingLabel || hasCodeContainer
+}
diff --git a/extensions/chrome/src/selectors/index.ts b/extensions/chrome/src/selectors/index.ts
new file mode 100644
index 0000000..4e4f6e5
--- /dev/null
+++ b/extensions/chrome/src/selectors/index.ts
@@ -0,0 +1,51 @@
+/** Selectors for finding and interacting with Claude's thinking blocks */
+export const TC_SELECTORS = {
+  // Enhanced elements
+  enhancedContainer: "[data-tc-container-id]",
+  processedElement: "[data-tc-processed]",
+} as const
+
+export const CLAUDE_ORIGINAL_SELECTORS = {
+  // Container selectors
+  claudeResponseContainer: "[data-is-streaming]",
+  // this is the main container for the thinking block directly after the 
+  claudeMainContainer: ".relative.flex.flex-col",
+
+  // Core thinking block selectors
+  claudeThinkingProcessBlock:
+    "pre, .text-text-300, .code-block__code, [class*='text-text-'], [class*='code-block']",
+  claudeThinkingLabel: ".text-text-300, [class*='text-text-']",
+  claudeThinkingProcessContentContainer:
+    ".code-block__code, [class*='code-block']",
+  claudeThinkingProcessContent: "code",
+
+  // UI elements
+  originalCopyBtn: ".pointer-events-none",
+} as const
+
+// Helper function to check if element is a thinking block
+export const isThinkingBlock = (
+  element: Element | null
+): element is HTMLDivElement => {
+  if (!element || !(element instanceof HTMLDivElement)) {
+    return false
+  }
+
+  // First check if the element itself is a thinking label or code container
+  if (
+    element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel) ||
+    element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContent)
+  ) {
+    return true
+  }
+
+  // Then check for child elements
+  const hasThinkingLabel = !!element.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel
+  )
+  const hasCodeContainer = !!element.querySelector(
+    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContentContainer
+  )
+
+  return hasThinkingLabel || hasCodeContainer
+}
diff --git a/extensions/chrome/src/utils/dom-utils.ts b/extensions/chrome/src/utils/dom-utils.ts
deleted file mode 100644
index 8d573e7..0000000
--- a/extensions/chrome/src/utils/dom-utils.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-export const waitForElement = (
-  selector: string,
-  timeout = 5000
-): Promise => {
-  return new Promise((resolve) => {
-    if (document.querySelector(selector)) {
-      return resolve(document.querySelector(selector))
-    }
-
-    const observer = new MutationObserver(() => {
-      if (document.querySelector(selector)) {
-        observer.disconnect()
-        resolve(document.querySelector(selector))
-      }
-    })
-
-    observer.observe(document.body, {
-      childList: true,
-      subtree: true,
-    })
-
-    setTimeout(() => {
-      observer.disconnect()
-      resolve(null)
-    }, timeout)
-  })
-}
-
-export const findElement = async (
-  selectors: string[]
-): Promise => {
-  for (const selector of selectors) {
-    const element = await waitForElement(selector)
-    if (element) {
-      console.log("[Thinking Claude] Found element using selector:", selector)
-      return element
-    }
-  }
-  console.log(
-    "[Thinking Claude] No matching element found for selectors:",
-    selectors
-  )
-  return null
-}

From a0cbfe9e8067fd458cce3f1374c8c3f53c3eb344 Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Tue, 26 Nov 2024 17:38:33 +0800
Subject: [PATCH 04/15] ref: updated chevron icon with transition effect

---
 .../chrome/src/components/thinking-block.tsx  | 25 +++----------------
 .../chrome/src/components/ui/skeleton.tsx     | 15 +++++++++++
 2 files changed, 18 insertions(+), 22 deletions(-)
 create mode 100644 extensions/chrome/src/components/ui/skeleton.tsx

diff --git a/extensions/chrome/src/components/thinking-block.tsx b/extensions/chrome/src/components/thinking-block.tsx
index d2104f0..57de3bd 100644
--- a/extensions/chrome/src/components/thinking-block.tsx
+++ b/extensions/chrome/src/components/thinking-block.tsx
@@ -69,11 +69,9 @@ export const ThinkingBlock: React.FC = ({
       
- - - -
- - -
- {content && ( -
- {content} -
- )} - {/* {codeElement && } */} -
-
- - ) -} - -// Icons -function ChevronUpIcon(props: React.SVGProps) { - return ( - - - - ) -} - -function CopyIcon(props: React.SVGProps) { - return ( - - - - ) -} - -function TickIcon(props: React.SVGProps) { - return ( - - - - ) -} diff --git a/extensions/chrome/src/constants/constants.ts b/extensions/chrome/src/constants/constants.ts index c4183b9..e69de29 100644 --- a/extensions/chrome/src/constants/constants.ts +++ b/extensions/chrome/src/constants/constants.ts @@ -1,94 +0,0 @@ -import { Icons, Selectors, Styles, Timings } from "@/types" - -export const selectors: Selectors = { - // Using data attribute for message container - messageContainer: "[data-is-streaming]", - - // Using combination of class and content for thinking label - thinkingLabel: 'div.text-xs:has(text="thinking")', - - // Using class and language attribute for code block - code: 'code[class*="language-thinking"]', - - // Using specific class combinations and structure - codeContainer: ".relative.flex.flex-col.rounded-lg", - - // Using specific pre within grid structure - pre: ".grid-cols-1.grid > pre", - - // Using specific class and container structure - thinkingProcess: '.code-block__code:has(>code[class*="language-thinking"])', -} - -export const timings: Timings = { - retryDelay: 1000, - mutationDelay: 100, - checkInterval: 2000, - copyFeedback: 2000, - maxRetries: 10, -} - -export const icons: Icons = { - arrow: ``, - tick: ``, - copy: ``, -} - -export const styles: Styles = { - animation: ` - @keyframes gradientWave { - 0% { background-position: 200% 50%; } - 100% { background-position: -200% 50%; } - } - - .thinking-animation { - background: linear-gradient( - 90deg, - rgba(156, 163, 175, 0.7) 0%, - rgba(209, 213, 219, 1) 25%, - rgba(156, 163, 175, 0.7) 50%, - rgba(209, 213, 219, 1) 75%, - rgba(156, 163, 175, 0.7) 100% - ); - background-size: 200% 100%; - animation: gradientWave 3s linear infinite; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - color: transparent; - } - - .thinking-header { - user-select: none; - display: flex; - align-items: center; - gap: 8px; - padding: 8px; - background-color: rgb(40, 44, 52); - border-radius: 6px 6px 0 0; - } - - .thinking-content { - transition: all 0.3s ease-in-out; - overflow-x: hidden; - overflow-y: auto; - max-height: 0; - opacity: 0; - padding: 0; - max-width: 100%; - display: block; - background-color: rgb(40, 44, 52); - border-radius: 0 0 6px 6px; - } - - .thinking-content code { - white-space: pre-wrap !important; - word-break: break-word !important; - overflow-wrap: break-word !important; - display: block !important; - max-width: 100% !important; - } - `, - buttonClass: "flex items-center text-text-500 hover:text-text-300", - labelClass: "font-medium text-sm", -} diff --git a/extensions/chrome/src/content/__managers/README.md b/extensions/chrome/src/content/__managers/README.md deleted file mode 100644 index 1a69cd6..0000000 --- a/extensions/chrome/src/content/__managers/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Thinking Process Manager Architecture - -This document outlines the architecture and workflow of the Thinking Process visualization feature in the Chrome extension. - -## Architecture Overview - -```mermaid -flowchart TB - subgraph Extension["Chrome Extension Content Script"] - Entry["index.tsx\nEntry Point"] - end - - subgraph ThinkingBlock["ThinkingBlockManager"] - direction TB - Init["Initialize"] - Process["Process Block"] - Cleanup["Cleanup Resources"] - end - - subgraph Managers["Manager Classes"] - direction LR - DOM["DOMObserverManager\n- Watch for new blocks\n- Periodic checks"] - UI["UIComponentManager\n- Create UI elements\n- Style components"] - Event["EventManager\n- Handle interactions\n- Manage UI state"] - Style["StyleManager\n- Inject styles\n- Manage animations"] - end - - Entry --> Init - Init --> Style - Init --> DOM - DOM --> Process - Process --> UI - Process --> Event - Event --> UI -``` - -## Component Workflow - -```mermaid -sequenceDiagram - participant Entry as index.tsx - participant TBM as ThinkingBlockManager - participant DOM as DOMObserverManager - participant UI as UIComponentManager - participant Event as EventManager - participant Style as StyleManager - - Entry->>TBM: Initialize - TBM->>Style: Inject Styles - TBM->>DOM: Initialize Observer - DOM->>DOM: Setup Periodic Check - - loop DOM Observation - DOM->>TBM: Process New Block - TBM->>UI: Create UI Components - TBM->>Event: Setup Event Handlers - Event-->>UI: Update UI State - end - - Note over TBM,Event: User Interactions - Event->>Event: Handle Toggle - Event->>Event: Handle Copy -``` - -## Component Responsibilities - -### ThinkingBlockManager - -- Central coordinator for the thinking process feature -- Initializes and manages other components -- Processes new thinking blocks -- Handles cleanup on unload - -### DOMObserverManager - -- Observes DOM for new thinking blocks -- Performs periodic checks for missed blocks -- Manages retry mechanism for initialization -- Handles cleanup of observers - -### UIComponentManager - -- Creates UI elements (buttons, containers) -- Applies consistent styling -- Manages component hierarchy -- Handles component updates - -### EventManager - -- Sets up event listeners -- Manages UI state transitions -- Handles copy functionality -- Provides user feedback - -### StyleManager - -- Injects required styles -- Manages animation styles -- Ensures single style injection -- Handles style cleanup - -## User Interaction Flow - -```mermaid -stateDiagram-v2 - [*] --> Hidden: Initial State - Hidden --> Visible: Click Toggle - Visible --> Hidden: Click Toggle - Visible --> Copied: Click Copy - Copied --> Visible: After Feedback - Hidden --> [*]: Cleanup - Visible --> [*]: Cleanup -``` - -## Installation and Usage - -The Thinking Process Manager is automatically initialized when the Chrome extension loads. It requires no manual setup and begins observing for thinking process blocks immediately. - -## Development - -To modify or extend the functionality: - -1. Each manager is designed to be independent and focused on a single responsibility -2. New features should be added to the appropriate manager -3. The ThinkingBlockManager coordinates all interactions between managers -4. Follow the established TypeScript types and interfaces - -## Error Handling - -- DOM observation includes retry mechanism -- Event handlers include error prevention -- Style injection prevents duplicates -- All cleanup is handled automatically diff --git a/extensions/chrome/src/content/__managers/index_managers.tsx b/extensions/chrome/src/content/__managers/index_managers.tsx deleted file mode 100644 index ad666a5..0000000 --- a/extensions/chrome/src/content/__managers/index_managers.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ThinkingBlockManager } from "./thinking-block-manager" - -/** - * Check if current URL matches Claude chat pattern - */ -const isChatURL = (): boolean => { - const url = window.location.href - return url.startsWith("https://claude.ai/chat/") -} - -/** - * Initialize the extension only on Claude chat pages - */ -const init = (): void => { - if (!isChatURL()) return - - const manager = new ThinkingBlockManager() - manager.init() -} - -// Initialize when DOM is ready -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init) -} else { - init() -} diff --git a/extensions/chrome/src/content/__managers/thinking-block-manager/events/event-manager.ts b/extensions/chrome/src/content/__managers/thinking-block-manager/events/event-manager.ts deleted file mode 100644 index 702883c..0000000 --- a/extensions/chrome/src/content/__managers/thinking-block-manager/events/event-manager.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { icons, timings } from "@/constants/constants" - -/** - * Manages event handling and UI state updates for thinking process blocks. - * Handles button click events, copy functionality, and UI state transitions. - */ -export class EventManager { - /** - * Sets up the toggle button click handler. - * @param toggleBtn - The button element that toggles the thinking process visibility - * @param container - The container element that holds the thinking process content - * @param updateUIState - Callback function to update the UI state - */ - static setupToggleButton( - toggleBtn: HTMLButtonElement, - container: HTMLElement, - updateUIState: (isOpen: boolean) => void - ): void { - toggleBtn.addEventListener("click", (e) => { - e.stopPropagation() - const currentState = container.dataset.isOpen === "true" - updateUIState(!currentState) - }) - } - - /** - * Sets up the copy button functionality. - * Handles click events, clipboard operations, and feedback animations. - * @param copyBtn - The button element that triggers the copy operation - * @param content - The element containing the content to be copied - */ - static setupCopyButton( - copyBtn: HTMLButtonElement, - content: Element | null - ): void { - copyBtn.addEventListener("click", (e) => { - e.stopPropagation() - if (content && !copyBtn.disabled) { - copyBtn.disabled = true - copyBtn.style.opacity = "0.5" - copyBtn.style.cursor = "default" - navigator.clipboard.writeText(content.textContent || "") - - const copyBtnText = copyBtn.querySelector("span.text-text-200") - const copyIcon = copyBtn.querySelector("span:first-child") - - if (copyBtnText) copyBtnText.textContent = "Copied!" - if (copyIcon) copyIcon.innerHTML = icons.tick - - setTimeout(() => { - if (copyBtnText) copyBtnText.textContent = "Copy" - if (copyIcon) copyIcon.innerHTML = icons.copy - copyBtn.disabled = false - copyBtn.style.opacity = "" - copyBtn.style.cursor = "" - }, timings.copyFeedback) - } - }) - } - - /** - * Updates the UI state of a thinking process block. - * Handles animations, text changes, and visibility states. - * @param container - The container element to update - * @param toggleBtn - The toggle button element to update - * @param isOpen - Whether the thinking process should be visible - */ - static updateUIState( - container: HTMLElement, - toggleBtn: HTMLButtonElement, - isOpen: boolean - ): void { - container.dataset.isOpen = isOpen.toString() - const arrow = toggleBtn.querySelector("svg") - const label = toggleBtn.querySelector("span") - - container.style.maxHeight = isOpen ? "50vh" : "0" - container.style.opacity = isOpen ? "1" : "0" - container.style.padding = isOpen ? "1em" : "0" - - if (label) { - label.textContent = isOpen - ? "Hide thinking process" - : "View thinking process" - } - - if (arrow) { - arrow.style.transform = `rotate(${isOpen ? 180 : 0}deg)` - } - } -} diff --git a/extensions/chrome/src/content/__managers/thinking-block-manager/index.ts b/extensions/chrome/src/content/__managers/thinking-block-manager/index.ts deleted file mode 100644 index 2b20f10..0000000 --- a/extensions/chrome/src/content/__managers/thinking-block-manager/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { selectors } from "@/constants/constants" - -import { EventManager } from "./events/event-manager" -import { DOMObserverManager } from "./observer/dom-observer-manager" -import { StyleManager } from "./styles/style-manager" -import { UIComponentManager } from "./ui/ui-component-manager" - -/** - * Manages the thinking process visualization feature in Claude's interface. - * Coordinates between UI components, DOM observation, events, and styles - * to create and maintain interactive thinking process blocks. - */ -export class ThinkingBlockManager { - private domObserver: DOMObserverManager - - /** - * Initializes the ThinkingBlockManager instance. - * Injects styles, sets up DOM observation, and adds an event listener for cleanup. - */ - constructor() { - StyleManager.injectStyles() - this.domObserver = new DOMObserverManager(this.processBlock.bind(this)) - this.domObserver.initWithRetry() - - window.addEventListener("unload", () => this.cleanupResources()) - } - - /** - * Processes a code block element to transform it into an interactive thinking process block. - * Creates and sets up the toggle button, copy functionality, and container styling. - * @param pre - The pre element containing the thinking process code - */ - private processBlock(pre: HTMLElement): void { - const container = pre.querySelector(selectors.code)?.parentElement - if (!container) return - - const outerDiv = pre.querySelector(selectors.codeContainer) - if (!outerDiv) return - - while (outerDiv.firstChild) { - outerDiv.removeChild(outerDiv.firstChild) - } - - container.dataset.isOpen = "true" - - const toggleBtn = UIComponentManager.createToggleButton() - const copyBtn = UIComponentManager.createCopyButton() - const header = UIComponentManager.createHeader(toggleBtn, copyBtn) - - UIComponentManager.setupContainer(container) - - outerDiv.appendChild(header) - outerDiv.appendChild(container) - - const updateUIState = (isOpen: boolean) => { - EventManager.updateUIState(container, toggleBtn, isOpen) - } - - EventManager.setupToggleButton(toggleBtn, container, updateUIState) - EventManager.setupCopyButton( - copyBtn, - container.querySelector(selectors.code) - ) - } - - /** - * Cleans up resources when the component is being destroyed. - * Disconnects DOM observers and removes event listeners. - */ - private cleanupResources(): void { - this.domObserver.cleanupObservers() - } - - // Add this method to resolve TypeScript error - init(): void { - // No additional initialization needed, as constructor handles it - } -} diff --git a/extensions/chrome/src/content/__managers/thinking-block-manager/observer/dom-observer-manager.ts b/extensions/chrome/src/content/__managers/thinking-block-manager/observer/dom-observer-manager.ts deleted file mode 100644 index 1e5aaa6..0000000 --- a/extensions/chrome/src/content/__managers/thinking-block-manager/observer/dom-observer-manager.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { timings } from "@/constants/constants" - -/** - * Manages DOM observation for thinking process blocks. - * Watches for new code blocks being added to the page and triggers processing. - * Also performs periodic checks for any blocks that might have been missed. - */ -export class DOMObserverManager { - private observers: Set - private processBlock: (pre: HTMLElement) => void - - /** - * Creates a new DOMObserverManager instance. - * @param processBlock - Callback function to process newly found code blocks - */ - constructor(processBlock: (pre: HTMLElement) => void) { - this.observers = new Set() - this.processBlock = processBlock - } - - /** - * Initializes the DOM observer with retry capability. - * Will attempt to retry setup if it fails, up to a maximum number of retries. - * @param retryCount - Current number of retry attempts - */ - initWithRetry(retryCount = 0): void { - try { - this.setupObserver() - } catch (error) { - console.error(error) - if (retryCount < timings.maxRetries) { - setTimeout(() => this.initWithRetry(retryCount + 1), timings.retryDelay) - } - } - } - - /** - * Cleans up all observers by disconnecting them and clearing the set. - */ - cleanupObservers(): void { - this.observers.forEach((observer) => observer.disconnect()) - this.observers.clear() - } - - /** - * Sets up the mutation observer to watch for DOM changes. - * Observes the entire document body for added nodes that might contain code blocks. - */ - private setupObserver(): void { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node instanceof HTMLElement) { - const pre = node.matches("pre") ? node : node.querySelector("pre") - if (pre) { - setTimeout(() => this.processBlock(pre), timings.mutationDelay) - } - } - }) - }) - }) - - observer.observe(document.body, { - childList: true, - subtree: true, - }) - - this.observers.add(observer) - this.setupPeriodicBlockCheck() - } - - /** - * Sets up periodic checking for code blocks that might have been missed. - * Runs at regular intervals defined in timing configuration. - */ - private setupPeriodicBlockCheck(): void { - setInterval(() => { - document.querySelectorAll("pre").forEach((pre) => { - if (!pre.querySelector(".thinking-header")) { - this.processBlock(pre as HTMLElement) - } - }) - }, timings.checkInterval) - } -} diff --git a/extensions/chrome/src/content/__managers/thinking-block-manager/styles/style-manager.ts b/extensions/chrome/src/content/__managers/thinking-block-manager/styles/style-manager.ts deleted file mode 100644 index c3c4eec..0000000 --- a/extensions/chrome/src/content/__managers/thinking-block-manager/styles/style-manager.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { styles } from "@/constants/constants" - -/** - * Manages the injection and removal of styles for thinking process blocks. - * Handles animation styles and ensures they are only injected once. - */ -export class StyleManager { - /** - * Injects the required styles for thinking process animations into the document head. - * Checks if styles are already present to avoid duplicate injection. - */ - static injectStyles(): void { - if (!document.getElementById("thinking-animation-styles")) { - const styleSheet = document.createElement("style") - styleSheet.id = "thinking-animation-styles" - styleSheet.textContent = styles.animation - document.head.appendChild(styleSheet) - } - } -} diff --git a/extensions/chrome/src/content/__managers/thinking-block-manager/ui/ui-component-manager.ts b/extensions/chrome/src/content/__managers/thinking-block-manager/ui/ui-component-manager.ts deleted file mode 100644 index f00d9ad..0000000 --- a/extensions/chrome/src/content/__managers/thinking-block-manager/ui/ui-component-manager.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { icons, selectors, styles } from "@/constants/constants" - -/** - * Manages the creation and styling of UI components for thinking process blocks. - * Provides factory methods for creating buttons, headers, and containers with consistent styling. - */ -export class UIComponentManager { - /** - * Creates a DOM element with optional class name and inner HTML. - * @param tag - The HTML tag name for the element - * @param className - Optional CSS class names to add - * @param innerHTML - Optional inner HTML content - * @returns The created HTML element - */ - static createElement( - tag: string, - className: string = "", - innerHTML: string = "" - ): HTMLElement { - const element = document.createElement(tag) - if (className) element.className = className - if (innerHTML) element.innerHTML = innerHTML - return element - } - - /** - * Creates a toggle button for showing/hiding thinking process content. - * @param isStreaming - Whether Claude is currently streaming a response - * @returns A styled button element with appropriate text and icon - */ - static createToggleButton(isStreaming = false): HTMLButtonElement { - const button = this.createElement( - "button", - "flex items-center text-text-500 hover:text-text-300" - ) - const labelText = isStreaming - ? "Claude is Thinking..." - : "Hide thinking process" - button.innerHTML = ` - ${icons.arrow} - ${labelText} - ` - return button as HTMLButtonElement - } - - /** - * Creates a copy button with appropriate styling and icon. - * @returns A styled button element for copying content - */ - static createCopyButton(): HTMLButtonElement { - const copyBtnContainer = this.createElement( - "div", - "from-bg-300/90 to-bg-300/70 pointer-events-auto rounded-md bg-gradient-to-b p-0.5 backdrop-blur-md" - ) - - const copyBtn = this.createElement( - "button", - "flex flex-row items-center gap-1 rounded-md p-1 py-0.5 text-xs transition-opacity delay-100 hover:bg-bg-200 opacity-60 hover:opacity-100" - ) as HTMLButtonElement - - const copyIcon = this.createElement("span", "", icons.copy) - const copyBtnText = this.createElement( - "span", - "text-text-200 pr-0.5", - "Copy" - ) - - copyBtn.appendChild(copyIcon) - copyBtn.appendChild(copyBtnText) - copyBtnContainer.appendChild(copyBtn) - - return copyBtn - } - - /** - * Creates a header element containing toggle and copy buttons. - * @param toggleBtn - The toggle button element - * @param copyBtn - The copy button element - * @returns A styled header element containing the buttons - */ - static createHeader( - toggleBtn: HTMLButtonElement, - copyBtn: HTMLButtonElement - ): HTMLElement { - const header = this.createElement("div", "thinking-header", "") - header.style.cssText = - "display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: var(--bg-300);" - - header.appendChild(toggleBtn) - header.appendChild(copyBtn.parentElement!) - - return header - } - - /** - * Sets up the container element with appropriate styling and classes. - * @param container - The container element to style - */ - static setupContainer(container: HTMLElement): void { - container.className = - "code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed" - container.style.cssText = - "transition: 0.3s ease-in-out; overflow: hidden auto; max-height: 50vh; opacity: 1; padding: 1em; max-width: 100%; display: block;" - - const content = container.querySelector(selectors.code) - if (content instanceof HTMLElement) { - content.style.cssText = - "white-space: pre-wrap !important; word-break: break-word !important; overflow-wrap: break-word !important; display: block !important; max-width: 100% !important;" - } - } -} diff --git a/extensions/chrome/src/content/__thinking-block/README.md b/extensions/chrome/src/content/__thinking-block/README.md deleted file mode 100644 index 9472391..0000000 --- a/extensions/chrome/src/content/__thinking-block/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Thinking Block Manager - -This directory contains the core functionality for managing Claude's thinking blocks. - -## Structure - -- `core/` - Core manager and types - - `manager.ts` - Main thinking block manager class - - `types.ts` - TypeScript interfaces and types -- `utils/` - Utility functions - - `dom.ts` - DOM manipulation utilities - - `observer.ts` - MutationObserver setup and handling -- `components/` - React components - - `ThinkingBlock.tsx` - Main thinking block component -- `constants/` - Constants and configurations - - `selectors.ts` - DOM selectors diff --git a/extensions/chrome/src/content/__thinking-block/core/manager.tsx b/extensions/chrome/src/content/__thinking-block/core/manager.tsx deleted file mode 100644 index e3cd970..0000000 --- a/extensions/chrome/src/content/__thinking-block/core/manager.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { CLAUDE_ORIGINAL_SELECTORS } from "@/selectors" -import { createRoot } from "react-dom/client" - -import { ThinkingBlock } from "@/components/thinking-block" - -import { ThinkingBlockRoots } from "../types" -import { - createThinkingBlockContainer, - generateBlockId, - getThinkingBlockElements, - isStreaming, - isThinkingBlock, -} from "../utils/dom" -import { createMutationObserver, observeDOM } from "../utils/observer" - -class ThinkingBlockManager { - private observer: MutationObserver | null = null - private checkInterval: number | null = null - private processedBlocks = new Set() - private roots: ThinkingBlockRoots = {} - - private processBlock(block: Element) { - // Skip if the block is already being processed - if (block.hasAttribute("data-tc-processing")) { - return - } - - // Check if block needs reprocessing - const blockId = block.getAttribute("data-tc-block-id") - const existingContainer = blockId - ? document.querySelector(`[data-tc-container-id="${blockId}"]`) - : null - - // Skip if already processed and container exists - if (this.processedBlocks.has(block) && existingContainer) { - return - } - - // Mark block as being processed - block.setAttribute("data-tc-processing", "true") - - try { - // Validate thinking block - if (!isThinkingBlock(block)) { - return - } - - // Get DOM elements - const { codeContainer, codeContent, mainContainer } = - getThinkingBlockElements(block) - - if (!codeContainer || !codeContent || !mainContainer) return - - // Generate or use existing block ID - const newBlockId = blockId || generateBlockId() - - if (!blockId) { - block.setAttribute("data-tc-block-id", newBlockId) - } - - // Only create new container and root if they don't exist - if (!existingContainer) { - // Create and setup container - const container = createThinkingBlockContainer(newBlockId) - - // Insert our container at the start of the flex container, before the thinking header - const thinkingHeader = block.querySelector( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel - ) - thinkingHeader?.parentElement?.insertBefore(container, thinkingHeader) - - // Create React root and render component - const root = createRoot(container) - this.roots[newBlockId] = root - - // Mark as processed - this.processedBlocks.add(block) - - // Render component - root.render( - - ) - } - } finally { - // Always remove processing marker - block.removeAttribute("data-tc-processing") - } - } - - private processExistingBlocks = () => { - const blocks = document.querySelectorAll( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessBlock - ) - blocks.forEach((block) => this.processBlock(block)) - } - - private initWithRetry(retryCount = 0) { - const maxRetries = 10 - const retryDelay = 1000 - - if (retryCount >= maxRetries) return - - const blocks = document.querySelectorAll( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessBlock - ) - if (blocks.length === 0) { - setTimeout(() => this.initWithRetry(retryCount + 1), retryDelay) - return - } - - this.processExistingBlocks() - - // Setup observer if not already setup - if (!this.observer) { - this.observer = createMutationObserver(this.processExistingBlocks) - observeDOM(this.observer) - } - - // Set up periodic check if not already setup - if (!this.checkInterval) { - this.checkInterval = window.setInterval(this.processExistingBlocks, 2000) - } - } - - initialize() { - console.log("Initializing thinking block manager...") - this.initWithRetry() - } - - cleanup() { - this.observer?.disconnect() - if (this.checkInterval) { - window.clearInterval(this.checkInterval) - } - Object.values(this.roots).forEach((root) => root.unmount()) - this.roots = {} - this.processedBlocks.clear() - } -} - -// Export singleton instance -export const thinkingBlockManager = new ThinkingBlockManager() diff --git a/extensions/chrome/src/content/__thinking-block/index copy.ts b/extensions/chrome/src/content/__thinking-block/index copy.ts deleted file mode 100644 index 5b5bd5f..0000000 --- a/extensions/chrome/src/content/__thinking-block/index copy.ts +++ /dev/null @@ -1,148 +0,0 @@ -import "@/styles/globals.css" - -import { shouldInitialize } from "@/utils/url-utils" - -import { thinkingBlockManager } from "./core/manager" - -// Track initialization state -let isInitialized = false - -const initializeExtension = async () => { - // Prevent multiple initializations - if (isInitialized) { - console.log("[Thinking Claude] Already initialized, skipping...") - return - } - - console.log("[Thinking Claude] Starting extension initialization...") - - // Skip initialization for unsupported pages - if (!shouldInitialize(window.location.href)) { - console.log( - "[Thinking Claude] Skipping initialization for unsupported page" - ) - return - } - - console.log("[Thinking Claude] Page supported, continuing initialization") - - // Initialize extension - const init = async () => { - console.log("[Content] Initializing content script") - - // // Immediately inject initial CSS to prevent FOUC - // const initialStyle = document.createElement("style") - // initialStyle.id = "tc-initial-styles" - // initialStyle.textContent = ` - // /* Initially hide all thinking blocks with transition */ - // pre > div:first-child { - // opacity: 0 !important; - // } - // ` - // document.head.appendChild(initialStyle) - console.log("[Content] Added initial styles") - - // Add a small delay to ensure DOM is fully loaded - await new Promise((resolve) => setTimeout(resolve, 500)) - console.log("[Content] DOM load delay completed") - - // Inject main CSS for enhanced UI - // const style = document.createElement("style") - // style.textContent = ` - // /* Only hide elements that have our enhanced version */ - // pre > div:first-child:has(+ div.tc-thinking-block-container) { - // position: absolute !important; - // opacity: 0 !important; - // pointer-events: none !important; - // z-index: -1 !important; - // clip: rect(0 0 0 0) !important; - // clip-path: inset(50%) !important; - // height: 1px !important; - // width: 1px !important; - // margin: -1px !important; - // overflow: hidden !important; - // } - - // /* Only hide elements after we've processed them */ - // pre .text-text-300[data-tc-processed="true"], - // .code-block__code[data-tc-processed="true"] { - // visibility: hidden !important; - // height: 0 !important; - // overflow: hidden !important; - // } - - // /* Shimmer animation for streaming state */ - // @keyframes gradientWave { - // 0% { background-position: 200% 50%; } - // 100% { background-position: -200% 50%; } - // } - - // /* Ensure code block has proper styling */ - // pre { - // background: none !important; - // } - // ` - // document.head.appendChild(style) - console.log("[Thinking Claude] Injected CSS styles") - - // Add immediate style to hide elements - // const hideStyle = document.createElement("style") - // hideStyle.textContent = ` - // .code-block__code[data-tc-processed], - // .text-text-300[data-tc-processed], - // .pointer-events-none[data-tc-processed] { - // display: none !important; - // } - // ` - // document.head.appendChild(hideStyle) - - // Initialize block manager - console.log("[Thinking Claude] Starting block manager initialization...") - thinkingBlockManager.initialize() - - // Remove initial styles after successful initialization - setTimeout(() => { - const initialStyles = document.getElementById("tc-initial-styles") - if (initialStyles) { - // Fade blocks back in if our enhanced UI failed to mount - initialStyles.textContent = ` - pre > div:first-child:not(:has(+ div.tc-thinking-block-container)) { - opacity: 1; - transition: opacity 0.2s ease-out; - } - ` - // Remove initial styles after transition - setTimeout(() => initialStyles.remove(), 250) - } - }, 500) - - // Add cleanup on unload - window.addEventListener("unload", () => { - thinkingBlockManager.cleanup() - document.getElementById("tc-initial-styles")?.remove() - }) - } - - await init() - isInitialized = true -} - -// Initialize when DOM is ready -if (document.readyState === "loading") { - console.log( - "[Thinking Claude] Document loading, waiting for DOMContentLoaded" - ) - document.addEventListener("DOMContentLoaded", initializeExtension) -} else { - console.log( - "[Thinking Claude] Document already loaded, initializing immediately" - ) - initializeExtension() -} - -// Cleanup on unload -window.addEventListener("unload", () => { - console.log("[Thinking Claude] Extension unloading, starting cleanup...") - thinkingBlockManager.cleanup() - document.getElementById("tc-initial-styles")?.remove() -}) diff --git a/extensions/chrome/src/content/__thinking-block/index.ts b/extensions/chrome/src/content/__thinking-block/index.ts deleted file mode 100644 index 96b6ae8..0000000 --- a/extensions/chrome/src/content/__thinking-block/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { thinkingBlockManager } from "./core/manager" -export type { ThinkingBlockState, ProcessedBlock } from "./types" diff --git a/extensions/chrome/src/content/__thinking-block/types/index.ts b/extensions/chrome/src/content/__thinking-block/types/index.ts deleted file mode 100644 index 4763b5b..0000000 --- a/extensions/chrome/src/content/__thinking-block/types/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Root } from "react-dom/client" - -export interface ThinkingBlockState { - isStreaming: boolean - blockId: string -} - -export interface ProcessedBlock { - element: Element - state: ThinkingBlockState -} - -export interface ThinkingBlockRoots { - [key: string]: Root -} - -export interface DOMElements { - thinkingLabel: Element | null - codeContainer: Element | null - codeContent: Element | null - mainContainer: Element | null -} diff --git a/extensions/chrome/src/content/__thinking-block/utils/dom.ts b/extensions/chrome/src/content/__thinking-block/utils/dom.ts deleted file mode 100644 index b0f6d99..0000000 --- a/extensions/chrome/src/content/__thinking-block/utils/dom.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CLAUDE_ORIGINAL_SELECTORS } from "@/selectors" - -import { DOMElements } from "../types" - -export const generateBlockId = () => - `tc-block-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` - -export const getThinkingBlockElements = (block: Element): DOMElements => ({ - thinkingLabel: block.querySelector( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel - ), - codeContainer: block.querySelector( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContentContainer - ), - codeContent: block.querySelector( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContent - ), - mainContainer: block.querySelector( - CLAUDE_ORIGINAL_SELECTORS.claudeMainContainer - ), -}) - -export const isThinkingBlock = (block: Element): boolean => { - const { thinkingLabel } = getThinkingBlockElements(block) - return thinkingLabel?.textContent?.trim().toLowerCase() === "thinking" -} - -export const isStreaming = (block: Element): boolean => - block - .closest(CLAUDE_ORIGINAL_SELECTORS.claudeResponseContainer) - ?.getAttribute("data-is-streaming") === "true" - -export const createThinkingBlockContainer = ( - blockId: string -): HTMLDivElement => { - const container = document.createElement("div") - container.className = "tc-thinking-block-container" - container.setAttribute("data-tc-container-id", blockId) - return container -} diff --git a/extensions/chrome/src/content/__thinking-block/utils/observer.ts b/extensions/chrome/src/content/__thinking-block/utils/observer.ts deleted file mode 100644 index 13459b8..0000000 --- a/extensions/chrome/src/content/__thinking-block/utils/observer.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { CLAUDE_ORIGINAL_SELECTORS } from "@/selectors" - -export const createMutationObserver = ( - onMutation: () => void, - delay: number = 100 -): MutationObserver => { - console.log("[Observer] Creating new mutation observer") - let processingTimeout: NodeJS.Timeout | null = null - - return new MutationObserver((mutations) => { - let shouldProcess = false - - // Clear any pending processing - if (processingTimeout) { - clearTimeout(processingTimeout) - } - - for (const mutation of mutations) { - // Skip mutations caused by our own changes - if ( - mutation.target instanceof Element && - mutation.target.closest(".tc-thinking-block-container") - ) { - continue - } - - // Check for added nodes that might contain streaming content - if (mutation.addedNodes.length > 0) { - for (const node of mutation.addedNodes) { - if ( - node instanceof Element && - node.closest(".tc-thinking-block-container") - ) { - continue - } - - // If this is a text node or element with text content in a streaming container - if ( - node.textContent && - node instanceof Element && - node - .closest(CLAUDE_ORIGINAL_SELECTORS.claudeResponseContainer) - ?.getAttribute("data-is-streaming") === "true" - ) { - shouldProcess = true - console.log("[Observer] Streaming content update detected") - break - } - } - } - - // Check for removed nodes that might be our containers - if (mutation.removedNodes.length > 0) { - for (const node of mutation.removedNodes) { - if ( - node instanceof Element && - node.matches(".tc-thinking-block-container") - ) { - shouldProcess = true - break - } - } - } - - // Check for streaming attribute changes - if ( - mutation.type === "attributes" && - mutation.attributeName === "data-is-streaming" && - mutation.target instanceof Element && - !mutation.target.closest(".tc-thinking-block-container") - ) { - shouldProcess = true - console.log("[Observer] Streaming state change detected") - break - } - } - - if (shouldProcess) { - // Debounce processing - processingTimeout = setTimeout(onMutation, delay) - } - }) -} - -export const observeDOM = (observer: MutationObserver): void => { - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["data-is-streaming"], - }) -} diff --git a/extensions/chrome/src/content/index.ts b/extensions/chrome/src/content/index.ts index c4e6297..167eb16 100644 --- a/extensions/chrome/src/content/index.ts +++ b/extensions/chrome/src/content/index.ts @@ -2,113 +2,13 @@ import "@/styles/globals.css" import { shouldInitialize } from "@/utils/url-utils" -function addCodeBlockToggle() { - console.log("[TC] Setting up code block toggle") - - let timeout: NodeJS.Timeout - const observer = new MutationObserver(() => { - clearTimeout(timeout) - timeout = setTimeout(() => { - const headers = document.querySelectorAll( - ".text-text-300.absolute.pl-3.pt-2\\.5.text-xs:not(:empty), div[data-is-streaming='true'] .text-text-300.absolute.pl-3.pt-2\\.5.text-xs:not(:empty), .pointer-events-none.sticky" - ) - console.log("[TC] Processing headers after mutations:", headers.length) - - headers.forEach((header) => { - if (header.hasAttribute("data-tc-processed")) return - header.setAttribute("data-tc-processed", "true") - - const codeBlock = header - .closest("pre") - ?.querySelector(".code-block__code") - if (!codeBlock) { - console.log("[TC] No code block found for header") - return - } - - // Make the element clickable - ;(header as HTMLElement).style.cssText = - "cursor: pointer; pointer-events: auto !important" - - const clickHandler = () => { - console.log("[TC] Element clicked!") - codeBlock.classList.toggle("collapsed") - console.log( - "[TC] Code block collapsed state:", - codeBlock.classList.contains("collapsed") - ) - } - - // Add collapsed class by default - // codeBlock.classList.add("collapsed") - - // If this is the copy button container - if (header.classList.contains("pointer-events-none")) { - const buttonContainer = header.querySelector( - ".from-bg-300\\/90" - ) as HTMLElement - const copyButton = header.querySelector("button") - - if (buttonContainer && copyButton) { - buttonContainer.style.cssText = - "pointer-events: auto; user-select: none" - header.addEventListener("click", clickHandler) - - copyButton.addEventListener( - "click", - async (e) => { - e.stopPropagation() - console.log("[TC] Copy button clicked") - - const codeElement = codeBlock.querySelector("code") - if (!codeElement) return - - try { - await navigator.clipboard.writeText( - codeElement.textContent || "" - ) - - // Update button text and SVG - const textSpan = copyButton.querySelector("span") - const svg = copyButton.querySelector("svg") - if (textSpan && svg) { - const originalText = textSpan.textContent - const originalSvgPath = svg.innerHTML - - textSpan.textContent = "Copied" - svg.innerHTML = - '' - - setTimeout(() => { - textSpan.textContent = originalText - svg.innerHTML = originalSvgPath - }, 2000) - } - } catch (err) { - console.error("[TC] Failed to copy:", err) - } - }, - true - ) - } - } else { - header.addEventListener("click", clickHandler) - } - }) - }, 100) - }) - - observer.observe(document.body, { - childList: true, - subtree: true, - }) -} +import { addThinkingBlockToggle } from "./v3/features/thinking-block" // Only initialize on appropriate pages if (shouldInitialize(window.location.href)) { if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", addCodeBlockToggle) + document.addEventListener("DOMContentLoaded", addThinkingBlockToggle) } else { - addCodeBlockToggle() + addThinkingBlockToggle() } } diff --git a/extensions/chrome/src/content/v3/features/thinking-block/index.ts b/extensions/chrome/src/content/v3/features/thinking-block/index.ts new file mode 100644 index 0000000..cfadc24 --- /dev/null +++ b/extensions/chrome/src/content/v3/features/thinking-block/index.ts @@ -0,0 +1 @@ +export { addThinkingBlockToggle } from "./thinking-block-toggle" diff --git a/extensions/chrome/src/content/v3/features/thinking-block/setup-controls.ts b/extensions/chrome/src/content/v3/features/thinking-block/setup-controls.ts new file mode 100644 index 0000000..a48e33a --- /dev/null +++ b/extensions/chrome/src/content/v3/features/thinking-block/setup-controls.ts @@ -0,0 +1,59 @@ +export function setupControls( + control: HTMLElement, + thinkingBlock: Element, + resContainer: Element +) { + const copyButton = control.querySelector("button") + + if (!copyButton) return + copyButton.classList.add("tc-select-none") + copyButton.addEventListener( + "click", + async (e) => { + e.stopPropagation() + await handleCopyClick(copyButton, thinkingBlock) + }, + true + ) + + control.addEventListener("click", () => { + const currentState = resContainer.getAttribute("data-thinking-block-state") + + // Update the collapse state of thinking block in the response container + const newState = currentState === "expanded" ? "collapsed" : "expanded" + resContainer.setAttribute("data-thinking-block-state", newState) + + // Toggle the collapse state of the thinking block as fallback + thinkingBlock.classList.toggle("collapsed") + }) +} + +async function handleCopyClick(copyButton: Element, codeBlock: Element) { + const codeElement = codeBlock.querySelector("code") + if (!codeElement) return + + try { + await navigator.clipboard.writeText(codeElement.textContent || "") + updateCopyButtonUI(copyButton) + } catch (err) { + console.error("[TC] Failed to copy:", err) + } +} + +function updateCopyButtonUI(copyButton: Element) { + const textSpan = copyButton.querySelector("span") + const svg = copyButton.querySelector("svg") + if (!textSpan || !svg) return + + const originalText = textSpan.textContent + const originalSvgPath = svg.innerHTML + + textSpan.textContent = "Copied" + svg.innerHTML = + '' + + setTimeout(() => { + textSpan.textContent = originalText + svg.innerHTML = originalSvgPath + }, 2000) +} diff --git a/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts b/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts new file mode 100644 index 0000000..9f34877 --- /dev/null +++ b/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts @@ -0,0 +1,34 @@ +import { THINKING_BLOCK_CONTROLS_SELECTORS } from "@/selectors" +import { mutationObserver } from "@/services/mutation-observer" + +import { setupControls } from "./setup-controls" + +export function addThinkingBlockToggle() { + mutationObserver.initialize() + mutationObserver.subscribe(processThinkingBlocks) +} + +function processThinkingBlocks() { + const thinkingBlockControls = document.querySelectorAll( + THINKING_BLOCK_CONTROLS_SELECTORS + ) + + thinkingBlockControls.forEach(processControl) +} + +function processControl(control: Element) { + if (control.hasAttribute("data-tc-processed")) return + control.setAttribute("data-tc-processed", "true") + + const resContainer = control.closest("div[data-is-streaming]") as HTMLElement + const thinkingBlock = control + .closest("pre") + ?.querySelector(".code-block__code") + if (!thinkingBlock) return + + if (!resContainer.hasAttribute("data-thinking-block-state")) { + resContainer.setAttribute("data-thinking-block-state", "expanded") + } + + setupControls(control as HTMLElement, thinkingBlock, resContainer) +} diff --git a/extensions/chrome/src/selectors/helper.ts b/extensions/chrome/src/selectors/helper.ts deleted file mode 100644 index f216984..0000000 --- a/extensions/chrome/src/selectors/helper.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CLAUDE_ORIGINAL_SELECTORS } from "." - -// Helper function to check if element is a thinking block -export const isThinkingBlock = ( - element: Element | null -): element is HTMLDivElement => { - if (!element || !(element instanceof HTMLDivElement)) { - return false - } - - // First check if the element itself is a thinking label or code container - if ( - element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel) || - element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContent) - ) { - return true - } - - // Then check for child elements - const hasThinkingLabel = !!element.querySelector( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel - ) - const hasCodeContainer = !!element.querySelector( - CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContentContainer - ) - - return hasThinkingLabel || hasCodeContainer -} diff --git a/extensions/chrome/src/selectors/index.ts b/extensions/chrome/src/selectors/index.ts index 68a80dc..ffd0348 100644 --- a/extensions/chrome/src/selectors/index.ts +++ b/extensions/chrome/src/selectors/index.ts @@ -1,63 +1,4 @@ -/** Original HTML -
-  
-
thinking
-
copy button
-
-
content
-
-
-
- */ - -/** Selectors for finding and interacting with Claude's thinking blocks */ -export const TC_SELECTORS = { - // Enhanced elements - enhancedContainer: "[data-tc-container-id]", - processedElement: "[data-tc-processed]", -} as const - -export const CLAUDE_ORIGINAL_SELECTORS = { - // Container selectors - claudeResponseContainer: "[data-is-streaming]", - // this is the main container for the thinking block directly after the
-  claudeMainContainer: ".relative.flex.flex-col",
-
-  // Core thinking block selectors
-  claudeThinkingProcessBlock:
-    "pre, .text-text-300, .code-block__code, [class*='text-text-'], [class*='code-block']",
-  claudeThinkingLabel: ".text-text-300, [class*='text-text-']",
-  claudeThinkingProcessContentContainer:
-    ".code-block__code, [class*='code-block']",
-  claudeThinkingProcessContent: "code",
-
-  // UI elements
-  originalCopyBtn: ".pointer-events-none",
-} as const
-
-// Helper function to check if element is a thinking block
-export const isThinkingBlock = (
-  element: Element | null
-): element is HTMLDivElement => {
-  if (!element || !(element instanceof HTMLDivElement)) {
-    return false
-  }
-
-  // First check if the element itself is a thinking label or code container
-  if (
-    element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel) ||
-    element.matches(CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContent)
-  ) {
-    return true
-  }
-
-  // Then check for child elements
-  const hasThinkingLabel = !!element.querySelector(
-    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingLabel
-  )
-  const hasCodeContainer = !!element.querySelector(
-    CLAUDE_ORIGINAL_SELECTORS.claudeThinkingProcessContentContainer
-  )
-
-  return hasThinkingLabel || hasCodeContainer
-}
+export const THINKING_BLOCK_CONTROLS_SELECTORS = [
+  ".text-text-300.absolute",
+  ".pointer-events-none.sticky",
+].join(", ")
diff --git a/extensions/chrome/src/services/mutation-observer.ts b/extensions/chrome/src/services/mutation-observer.ts
new file mode 100644
index 0000000..efc8c29
--- /dev/null
+++ b/extensions/chrome/src/services/mutation-observer.ts
@@ -0,0 +1,52 @@
+type ObserverCallback = () => void
+
+class MutationObserverService {
+  private observer: MutationObserver | null = null
+  private callbacks: Set = new Set()
+  private timeouts: Map = new Map()
+  private isProcessing = false
+
+  initialize() {
+    if (this.observer) return
+
+    this.observer = new MutationObserver(() => {
+      if (this.isProcessing) return
+
+      this.isProcessing = true
+      this.callbacks.forEach((callback) => {
+        const existingTimeout = this.timeouts.get(callback)
+        if (existingTimeout) {
+          clearTimeout(existingTimeout)
+        }
+
+        const timeout = setTimeout(() => {
+          callback()
+          this.isProcessing = false
+        }, 200) // Slightly increased debounce time
+
+        this.timeouts.set(callback, timeout)
+      })
+    })
+
+    this.observer.observe(document.body, {
+      childList: true,
+      subtree: true,
+    })
+  }
+
+  subscribe(callback: ObserverCallback) {
+    this.callbacks.add(callback)
+    return () => this.unsubscribe(callback)
+  }
+
+  private unsubscribe(callback: ObserverCallback) {
+    this.callbacks.delete(callback)
+    const timeout = this.timeouts.get(callback)
+    if (timeout) {
+      clearTimeout(timeout)
+      this.timeouts.delete(callback)
+    }
+  }
+}
+
+export const mutationObserver = new MutationObserverService()
diff --git a/extensions/chrome/src/styles/globals.css b/extensions/chrome/src/styles/globals.css
index 4c5ab8e..63d0cf3 100644
--- a/extensions/chrome/src/styles/globals.css
+++ b/extensions/chrome/src/styles/globals.css
@@ -24,14 +24,6 @@
     --tc-destructive-foreground: 210 40% 98%;
     --tc-ring: 215 20.2% 65.1%;
     --tc-radius: 0.5rem;
-    --tc-thinking-gradient: linear-gradient(
-      90deg,
-      hsl(var(--tc-muted)) 0%,
-      hsl(var(--tc-muted-foreground)) 25%,
-      hsl(var(--tc-muted)) 50%,
-      hsl(var(--tc-muted-foreground)) 75%,
-      hsl(var(--tc-muted)) 100%
-    );
   }
 
   .dark {
@@ -56,50 +48,3 @@
     --tc-ring: 216 34% 17%;
   }
 }
-
-@layer components {
-  .tc-animate-shimmer {
-    background: var(--tc-thinking-gradient);
-    background-size: 200% 100%;
-    animation: tc-shimmer 3s linear infinite;
-    -webkit-background-clip: text;
-    -webkit-text-fill-color: transparent;
-    background-clip: text;
-    color: transparent;
-  }
-
-  .tc-collapsible-content[data-state="open"] {
-    animation: collapsible-slide-down 0.25s ease-out;
-  }
-
-  .tc-collapsible-content[data-state="closed"] {
-    animation: collapsible-slide-up 0.2s ease-out;
-  }
-}
-
-@keyframes tc-shimmer {
-  from {
-    background-position: 0% center;
-  }
-  to {
-    background-position: -200% center;
-  }
-}
-
-@keyframes collapsible-slide-down {
-  from {
-    height: 0;
-  }
-  to {
-    height: var(--radix-collapsible-content-height);
-  }
-}
-
-@keyframes collapsible-slide-up {
-  from {
-    height: var(--radix-collapsible-content-height);
-  }
-  to {
-    height: 0;
-  }
-}
diff --git a/extensions/chrome/src/types/index.ts b/extensions/chrome/src/types/index.ts
deleted file mode 100644
index d8e8c1a..0000000
--- a/extensions/chrome/src/types/index.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-export interface Selectors {
-  pre: string
-  thinkingProcess: string
-  messageContainer: string
-  thinkingLabel: string
-  codeContainer: string
-  code: string
-}
-
-export interface Timings {
-  retryDelay: number
-  mutationDelay: number
-  checkInterval: number
-  copyFeedback: number
-  maxRetries: number
-}
-
-export interface Icons {
-  arrow: string
-  tick: string
-  copy: string
-}
-
-export interface Styles {
-  animation: string
-  buttonClass: string
-  labelClass: string
-}
diff --git a/extensions/chrome/tailwind.config.cjs b/extensions/chrome/tailwind.config.cjs
index a841cac..996fdd6 100644
--- a/extensions/chrome/tailwind.config.cjs
+++ b/extensions/chrome/tailwind.config.cjs
@@ -2,6 +2,7 @@
  * Our own Tailwind CSS is not being bundled into the build.
  * TODO: Figure out why and fix this later.
  */
+const colors = require("tailwindcss/colors")
 
 /** @type {import('tailwindcss').Config} */
 module.exports = {
@@ -11,6 +12,8 @@ module.exports = {
     "./src/**/*.{ts,tsx}",
     "./src/components/**/*.{ts,tsx}",
     "./src/content/**/*.{ts,tsx}",
+    "./src/**/*.{ts,tsx,html,js,jsx}",
+    "./public/**/*.html",
   ],
   theme: {
     extend: {
@@ -48,6 +51,29 @@ module.exports = {
           DEFAULT: "hsl(var(--tc-card))",
           foreground: "hsl(var(--tc-card-foreground))",
         },
+        // Include Tailwind's default colors
+        slate: colors.slate,
+        gray: colors.gray,
+        zinc: colors.zinc,
+        neutral: colors.neutral,
+        stone: colors.stone,
+        red: colors.red,
+        orange: colors.orange,
+        amber: colors.amber,
+        yellow: colors.yellow,
+        lime: colors.lime,
+        green: colors.green,
+        emerald: colors.emerald,
+        teal: colors.teal,
+        cyan: colors.cyan,
+        sky: colors.sky,
+        blue: colors.blue,
+        indigo: colors.indigo,
+        violet: colors.violet,
+        purple: colors.purple,
+        fuchsia: colors.fuchsia,
+        pink: colors.pink,
+        rose: colors.rose,
       },
       //   we can inherit the original border radius form claude here
       //   borderRadius: {
diff --git a/extensions/chrome/webpack/webpack.common.js b/extensions/chrome/webpack/webpack.common.js
index f8b5d62..1cfcc23 100644
--- a/extensions/chrome/webpack/webpack.common.js
+++ b/extensions/chrome/webpack/webpack.common.js
@@ -29,7 +29,23 @@ export default {
       },
       {
         test: /\.css$/,
-        use: ["style-loader", "css-loader", "postcss-loader"],
+        use: [
+          "style-loader",
+          {
+            loader: "css-loader",
+            options: {
+              importLoaders: 1,
+            },
+          },
+          {
+            loader: "postcss-loader",
+            options: {
+              postcssOptions: {
+                config: path.resolve(__dirname, "..", "postcss.config.cjs"),
+              },
+            },
+          },
+        ],
       },
     ],
   },

From 18ed577bf913a9ebfac7a9164ba62bd7104f3df7 Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Wed, 27 Nov 2024 21:17:30 +0800
Subject: [PATCH 08/15] ci: bump the version to v3.1.0

build: update the readme for extension development
---
 extensions/changelog.md                | 22 ++++++++++++++++++++++
 extensions/chrome/README.md            | 17 +++++++++++++++++
 extensions/chrome/package.json         |  2 +-
 extensions/chrome/public/manifest.json |  2 +-
 4 files changed, 41 insertions(+), 2 deletions(-)

diff --git a/extensions/changelog.md b/extensions/changelog.md
index 323b129..dc57f7c 100644
--- a/extensions/changelog.md
+++ b/extensions/changelog.md
@@ -1,5 +1,27 @@
 ## Changelog of the extensions
 
+### feat/fix/ref: - 11/27/2024 - @lumpinif
+
+#### Performance & Code Quality
+
+- Extremely streamline code structure and implementation approach
+- Much optimized performance
+- Streamline and organize code for thinking-block implementation
+
+#### Bug Fixes
+
+- Fix flash of unstyled content (FOUC)
+- Fix stutter when submitting new replies
+- Fix FOUC and streaming issues for thinking-block implementation
+
+#### UI Improvements
+
+- Update chevron icon with transition effect
+
+#### Architecture
+
+- Implement ultimate approach with simplest and most effective implementation after experimentation
+
 ### fix: - 11/17/2024 - @lumpinif
 
 #### Observer Management and Memory Leak Prevention
diff --git a/extensions/chrome/README.md b/extensions/chrome/README.md
index 7b0cc45..3efce83 100644
--- a/extensions/chrome/README.md
+++ b/extensions/chrome/README.md
@@ -223,6 +223,23 @@ We use several tools to maintain code quality:
 - **ESLint**: Finds and fixes JavaScript problems
 - **Prettier**: Formats your code consistently
 
+### Version Control & Releases
+
+The project uses automated version bumping through CI:
+
+- **Automatic Version Bumping**: When code is merged to main, the CI will:
+
+  - Auto-increment the patch version (e.g., 1.0.0 -> 1.0.1)
+  - Create a new release with the bumped version
+  - Skip version bump for major versions (x.0.0)
+
+- **Manual Version Control**:
+  - Developers can manually set both versions in `package.json` and `manifest.json`
+  - Major version changes (x.0.0) must be set manually
+  - Manual versions will be respected by CI
+
+> **Note**: If you need to manually set a version, update both `package.json` and `manifest.json` before merging to main.
+
 ### Continuous Integration
 
 Our GitHub Actions setup automatically:
diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json
index 499155a..7e2da8f 100644
--- a/extensions/chrome/package.json
+++ b/extensions/chrome/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thinking-claude",
-  "version": "3.0.0",
+  "version": "3.1.0",
   "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 701e3dc..f01c2df 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.0.0",
+  "version": "3.1.0",
   "description": "Chrome extension for letting Claude think like a real human",
   "content_scripts": [
     {

From 3e8b6d722b4e5eda26b7c3240dab4b572bc97968 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" 
Date: Wed, 27 Nov 2024 13:18:26 +0000
Subject: [PATCH 09/15] chore: bump version to 3.1.1 [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 7e2da8f..e5b72b6 100644
--- a/extensions/chrome/package.json
+++ b/extensions/chrome/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thinking-claude",
-  "version": "3.1.0",
+  "version": "3.1.1",
   "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 f01c2df..fa3de67 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.0",
+  "version": "3.1.1",
   "description": "Chrome extension for letting Claude think like a real human",
   "content_scripts": [
     {

From 115c8eb26a3e80b7c859359ed1e043817308263c Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Wed, 27 Nov 2024 21:21:58 +0800
Subject: [PATCH 10/15] ci: test the github ci version bumping

---
 .github/workflows/chrome-extension-ci.yml | 40 ++++++++++++++---------
 extensions/chrome/package.json            |  2 +-
 extensions/chrome/public/manifest.json    |  2 +-
 3 files changed, 27 insertions(+), 17 deletions(-)

diff --git a/.github/workflows/chrome-extension-ci.yml b/.github/workflows/chrome-extension-ci.yml
index 93c21cc..88f2a23 100644
--- a/.github/workflows/chrome-extension-ci.yml
+++ b/.github/workflows/chrome-extension-ci.yml
@@ -71,25 +71,35 @@ jobs:
         id: check_tag
         run: |
           current_version=$(node -p "require('./package.json').version")
+          # Get latest tag from remote
+          latest_tag=$(git ls-remote --tags origin 'refs/tags/v*' | sort -V | tail -n1 | sed 's/.*refs\/tags\/v//')
+
           if [[ "$current_version" =~ ^[0-9]+\.0\.0$ ]]; then
             # Don't increment major versions (x.0.0)
             echo "version=$current_version" >> $GITHUB_OUTPUT
             echo "version_changed=false" >> $GITHUB_OUTPUT
-          elif git ls-remote --tags origin refs/tags/v$current_version >/dev/null; then
-            # If tag exists and it's not a major version, increment patch
-            IFS='.' read -r major minor patch <<< "$current_version"
-            new_version="$major.$minor.$((patch + 1))"
-            echo "version=$new_version" >> $GITHUB_OUTPUT
-            echo "version_changed=true" >> $GITHUB_OUTPUT
-            # Update package.json with new version
-            sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" package.json
-            # Update manifest.json with new version
-            sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" public/manifest.json
-            git config --global user.email "github-actions[bot]@users.noreply.github.com"
-            git config --global user.name "github-actions[bot]"
-            git add package.json public/manifest.json
-            git commit -m "chore: bump version to $new_version [skip ci]"
-            git push
+          elif [[ -n "$latest_tag" ]]; then
+            # Compare versions properly
+            if [[ "$(printf '%s\n' "$latest_tag" "$current_version" | sort -V | head -n1)" == "$latest_tag" ]]; then
+              # Current version is newer than latest tag, use it as is
+              echo "version=$current_version" >> $GITHUB_OUTPUT
+              echo "version_changed=false" >> $GITHUB_OUTPUT
+            else
+              # Increment patch of current version
+              IFS='.' read -r major minor patch <<< "$current_version"
+              new_version="$major.$minor.$((patch + 1))"
+              echo "version=$new_version" >> $GITHUB_OUTPUT
+              echo "version_changed=true" >> $GITHUB_OUTPUT
+              # Update package.json with new version
+              sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" package.json
+              # Update manifest.json with new version
+              sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" public/manifest.json
+              git config --global user.email "github-actions[bot]@users.noreply.github.com"
+              git config --global user.name "github-actions[bot]"
+              git add package.json public/manifest.json
+              git commit -m "chore: bump version to $new_version [skip ci]"
+              git push
+            fi
           else
             echo "version=$current_version" >> $GITHUB_OUTPUT
             echo "version_changed=false" >> $GITHUB_OUTPUT
diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json
index e5b72b6..499155a 100644
--- a/extensions/chrome/package.json
+++ b/extensions/chrome/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thinking-claude",
-  "version": "3.1.1",
+  "version": "3.0.0",
   "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 fa3de67..701e3dc 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.1",
+  "version": "3.0.0",
   "description": "Chrome extension for letting Claude think like a real human",
   "content_scripts": [
     {

From 1324481d42e2b25f028c13cbd3aa65d06b1c1a95 Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Wed, 27 Nov 2024 21:26:02 +0800
Subject: [PATCH 11/15] ci: test github ci bumping

---
 extensions/chrome/test.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/extensions/chrome/test.txt b/extensions/chrome/test.txt
index 45f8b44..83e7956 100644
--- a/extensions/chrome/test.txt
+++ b/extensions/chrome/test.txt
@@ -1 +1 @@
-Test file to verify github actions auto bumping
+Version Bumping Test File

From 791826c25b06653e1e1a7e382e6776428fb4b7d7 Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Wed, 27 Nov 2024 21:31:33 +0800
Subject: [PATCH 12/15] ci: bumping version test

---
 .github/workflows/chrome-extension-ci.yml | 15 ++++++++-------
 extensions/chrome/test.txt                |  1 +
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/chrome-extension-ci.yml b/.github/workflows/chrome-extension-ci.yml
index 88f2a23..1e02b75 100644
--- a/.github/workflows/chrome-extension-ci.yml
+++ b/.github/workflows/chrome-extension-ci.yml
@@ -69,30 +69,30 @@ jobs:
       # Only increment patch version for non-major versions
       - name: Check existing tag
         id: check_tag
+        working-directory: ./extensions/chrome
         run: |
           current_version=$(node -p "require('./package.json').version")
-          # Get latest tag from remote
           latest_tag=$(git ls-remote --tags origin 'refs/tags/v*' | sort -V | tail -n1 | sed 's/.*refs\/tags\/v//')
 
+          echo "Current version: $current_version"
+          echo "Latest tag: $latest_tag"  # Add debugging output
+
           if [[ "$current_version" =~ ^[0-9]+\.0\.0$ ]]; then
-            # Don't increment major versions (x.0.0)
+            echo "Major version detected: $current_version"
             echo "version=$current_version" >> $GITHUB_OUTPUT
             echo "version_changed=false" >> $GITHUB_OUTPUT
           elif [[ -n "$latest_tag" ]]; then
-            # Compare versions properly
             if [[ "$(printf '%s\n' "$latest_tag" "$current_version" | sort -V | head -n1)" == "$latest_tag" ]]; then
-              # Current version is newer than latest tag, use it as is
+              echo "Current version is newer than latest tag"
               echo "version=$current_version" >> $GITHUB_OUTPUT
               echo "version_changed=false" >> $GITHUB_OUTPUT
             else
-              # Increment patch of current version
+              echo "Incrementing patch version"
               IFS='.' read -r major minor patch <<< "$current_version"
               new_version="$major.$minor.$((patch + 1))"
               echo "version=$new_version" >> $GITHUB_OUTPUT
               echo "version_changed=true" >> $GITHUB_OUTPUT
-              # Update package.json with new version
               sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" package.json
-              # Update manifest.json with new version
               sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" public/manifest.json
               git config --global user.email "github-actions[bot]@users.noreply.github.com"
               git config --global user.name "github-actions[bot]"
@@ -101,6 +101,7 @@ jobs:
               git push
             fi
           else
+            echo "No existing tags found"
             echo "version=$current_version" >> $GITHUB_OUTPUT
             echo "version_changed=false" >> $GITHUB_OUTPUT
           fi
diff --git a/extensions/chrome/test.txt b/extensions/chrome/test.txt
index 83e7956..5488dee 100644
--- a/extensions/chrome/test.txt
+++ b/extensions/chrome/test.txt
@@ -1 +1,2 @@
 Version Bumping Test File
+v3.0.0
\ No newline at end of file

From 22426ad3b9d6c026fccd3e55e0f5a7eadda5d148 Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Wed, 27 Nov 2024 21:43:16 +0800
Subject: [PATCH 13/15] ci: bump the verison to 3.1.0 manually

---
 .github/workflows/chrome-extension-ci.yml | 43 +++++++++--------------
 extensions/chrome/package.json            |  2 +-
 extensions/chrome/public/manifest.json    |  2 +-
 extensions/chrome/test.txt                |  2 +-
 4 files changed, 19 insertions(+), 30 deletions(-)

diff --git a/.github/workflows/chrome-extension-ci.yml b/.github/workflows/chrome-extension-ci.yml
index 1e02b75..93c21cc 100644
--- a/.github/workflows/chrome-extension-ci.yml
+++ b/.github/workflows/chrome-extension-ci.yml
@@ -69,39 +69,28 @@ jobs:
       # Only increment patch version for non-major versions
       - name: Check existing tag
         id: check_tag
-        working-directory: ./extensions/chrome
         run: |
           current_version=$(node -p "require('./package.json').version")
-          latest_tag=$(git ls-remote --tags origin 'refs/tags/v*' | sort -V | tail -n1 | sed 's/.*refs\/tags\/v//')
-
-          echo "Current version: $current_version"
-          echo "Latest tag: $latest_tag"  # Add debugging output
-
           if [[ "$current_version" =~ ^[0-9]+\.0\.0$ ]]; then
-            echo "Major version detected: $current_version"
+            # Don't increment major versions (x.0.0)
             echo "version=$current_version" >> $GITHUB_OUTPUT
             echo "version_changed=false" >> $GITHUB_OUTPUT
-          elif [[ -n "$latest_tag" ]]; then
-            if [[ "$(printf '%s\n' "$latest_tag" "$current_version" | sort -V | head -n1)" == "$latest_tag" ]]; then
-              echo "Current version is newer than latest tag"
-              echo "version=$current_version" >> $GITHUB_OUTPUT
-              echo "version_changed=false" >> $GITHUB_OUTPUT
-            else
-              echo "Incrementing patch version"
-              IFS='.' read -r major minor patch <<< "$current_version"
-              new_version="$major.$minor.$((patch + 1))"
-              echo "version=$new_version" >> $GITHUB_OUTPUT
-              echo "version_changed=true" >> $GITHUB_OUTPUT
-              sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" package.json
-              sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" public/manifest.json
-              git config --global user.email "github-actions[bot]@users.noreply.github.com"
-              git config --global user.name "github-actions[bot]"
-              git add package.json public/manifest.json
-              git commit -m "chore: bump version to $new_version [skip ci]"
-              git push
-            fi
+          elif git ls-remote --tags origin refs/tags/v$current_version >/dev/null; then
+            # If tag exists and it's not a major version, increment patch
+            IFS='.' read -r major minor patch <<< "$current_version"
+            new_version="$major.$minor.$((patch + 1))"
+            echo "version=$new_version" >> $GITHUB_OUTPUT
+            echo "version_changed=true" >> $GITHUB_OUTPUT
+            # Update package.json with new version
+            sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" package.json
+            # Update manifest.json with new version
+            sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" public/manifest.json
+            git config --global user.email "github-actions[bot]@users.noreply.github.com"
+            git config --global user.name "github-actions[bot]"
+            git add package.json public/manifest.json
+            git commit -m "chore: bump version to $new_version [skip ci]"
+            git push
           else
-            echo "No existing tags found"
             echo "version=$current_version" >> $GITHUB_OUTPUT
             echo "version_changed=false" >> $GITHUB_OUTPUT
           fi
diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json
index 499155a..7e2da8f 100644
--- a/extensions/chrome/package.json
+++ b/extensions/chrome/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thinking-claude",
-  "version": "3.0.0",
+  "version": "3.1.0",
   "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 701e3dc..f01c2df 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.0.0",
+  "version": "3.1.0",
   "description": "Chrome extension for letting Claude think like a real human",
   "content_scripts": [
     {
diff --git a/extensions/chrome/test.txt b/extensions/chrome/test.txt
index 5488dee..7001271 100644
--- a/extensions/chrome/test.txt
+++ b/extensions/chrome/test.txt
@@ -1,2 +1,2 @@
 Version Bumping Test File
-v3.0.0
\ No newline at end of file
+v3.1.0
\ No newline at end of file

From caee226bbfde65862312c7b4ecc658a1abc42c89 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" 
Date: Wed, 27 Nov 2024 13:44:08 +0000
Subject: [PATCH 14/15] chore: bump version to 3.1.1 [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 7e2da8f..e5b72b6 100644
--- a/extensions/chrome/package.json
+++ b/extensions/chrome/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thinking-claude",
-  "version": "3.1.0",
+  "version": "3.1.1",
   "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 f01c2df..fa3de67 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.0",
+  "version": "3.1.1",
   "description": "Chrome extension for letting Claude think like a real human",
   "content_scripts": [
     {

From 688a35f5d9b6b5137fa2ab6a85d0ed56d8c5719b Mon Sep 17 00:00:00 2001
From: Felix Lu 
Date: Wed, 27 Nov 2024 21:49:44 +0800
Subject: [PATCH 15/15] ci: fix and test the auto bumping

---
 .github/workflows/chrome-extension-ci.yml | 2 +-
 extensions/chrome/package.json            | 2 +-
 extensions/chrome/public/manifest.json    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/chrome-extension-ci.yml b/.github/workflows/chrome-extension-ci.yml
index 93c21cc..4f71459 100644
--- a/.github/workflows/chrome-extension-ci.yml
+++ b/.github/workflows/chrome-extension-ci.yml
@@ -75,7 +75,7 @@ jobs:
             # Don't increment major versions (x.0.0)
             echo "version=$current_version" >> $GITHUB_OUTPUT
             echo "version_changed=false" >> $GITHUB_OUTPUT
-          elif git ls-remote --tags origin refs/tags/v$current_version >/dev/null; then
+          elif git ls-remote --tags origin | grep -q "refs/tags/v$current_version"; then
             # If tag exists and it's not a major version, increment patch
             IFS='.' read -r major minor patch <<< "$current_version"
             new_version="$major.$minor.$((patch + 1))"
diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json
index e5b72b6..7e2da8f 100644
--- a/extensions/chrome/package.json
+++ b/extensions/chrome/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thinking-claude",
-  "version": "3.1.1",
+  "version": "3.1.0",
   "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 fa3de67..f01c2df 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.1",
+  "version": "3.1.0",
   "description": "Chrome extension for letting Claude think like a real human",
   "content_scripts": [
     {