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/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/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..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": {
@@ -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.cjs
similarity index 74%
rename from extensions/chrome/postcss.config.mjs
rename to extensions/chrome/postcss.config.cjs
index 2aa7205..33ad091 100644
--- a/extensions/chrome/postcss.config.mjs
+++ b/extensions/chrome/postcss.config.cjs
@@ -1,6 +1,6 @@
-export default {
+module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
-};
+}
diff --git a/extensions/chrome/public/manifest.json b/extensions/chrome/public/manifest.json
index a73e4eb..f01c2df 100644
--- a/extensions/chrome/public/manifest.json
+++ b/extensions/chrome/public/manifest.json
@@ -1,12 +1,13 @@
{
"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": [
{
"matches": ["https://*.claude.ai/*"],
- "js": ["content.js"]
+ "js": ["content.js"],
+ "css": ["styles.css"]
}
],
"icons": {
diff --git a/extensions/chrome/public/styles.css b/extensions/chrome/public/styles.css
new file mode 100644
index 0000000..d2e6d5d
--- /dev/null
+++ b/extensions/chrome/public/styles.css
@@ -0,0 +1,188 @@
+/* Overwrite original claude thinking block content styles */
+.code-block__code {
+ background: none !important;
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ overflow-y: auto !important;
+ overflow-x: hidden !important;
+ color: var(--text-text-200) !important;
+ padding: 1em !important;
+ max-width: 100%;
+ display: block;
+ height: auto !important;
+ min-height: 0vh !important;
+ max-height: 50vh !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+ transition: all 0.3s ease-out !important;
+}
+
+/* collapsed states */
+
+/* Collapsed state */
+.code-block__code.collapsed {
+ height: 0 !important;
+ padding: 0 !important;
+ visibility: hidden !important;
+ opacity: 0 !important;
+}
+
+/* Collapsed state */
+[data-thinking-block-state="collapsed"] .code-block__code {
+ height: 0 !important;
+ padding: 0 !important;
+ visibility: hidden !important;
+ opacity: 0 !important;
+}
+
+/* Expanded state */
+/* [data-thinking-block-state="expanded"] .code-block__code {
+ height: 50vh !important;
+ padding: 1em !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+} */
+
+code {
+ background: none !important;
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ text-wrap: balance !important;
+ color: hsl(var(--text-300)) !important;
+ font-size: 0.875rem !important;
+ display: block !important;
+ max-width: 100% !important;
+ text-shadow: none !important;
+}
+
+/* Add selection styles */
+code span::selection {
+ background: hsl(var(--clay) / var(--tw-text-opacity)) !important;
+ color: hsl(var(--text-100)) !important;
+}
+
+code span::-moz-selection {
+ background: hsl(var(--clay) / var(--tw-text-opacity)) !important;
+ color: hsl(var(--text-100)) !important;
+}
+
+code span:hover {
+ transition: color 0.4s ease;
+ color: hsl(var(--text-100));
+}
+
+/* --------------------------------- */
+
+/* Copy button container */
+div[data-is-streaming] .pointer-events-none.sticky {
+ cursor: pointer !important;
+ pointer-events: auto !important;
+}
+
+/* Copy button container */
+div[data-is-streaming] .from-bg-300\\/90 {
+ pointer-events: auto !important;
+ user-select: none !important;
+}
+
+/* --------------------------------- */
+
+/* Update the header text */
+/* This is the original header text */
+pre .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty) {
+ font-size: 0; /* Hide original text */
+ pointer-events: auto !important; /* Make sure it's clickable */
+ cursor: pointer !important;
+ display: inline-flex;
+ align-items: center;
+ font-family: var(--font-user-message);
+}
+
+/* Update the text of the header */
+[data-thinking-block-state="collapsed"]
+ pre
+ .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::after {
+ content: "Open Claude's thinking";
+ font-size: 0.875rem; /* Restore font size */
+ cursor: pointer;
+ font-family: var(--font-user-message);
+ transition: color 0.15s ease-in-out;
+}
+
+pre .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::after {
+ content: "Claude's thinking";
+ font-size: 0.875rem; /* Restore font size */
+ cursor: pointer;
+ font-family: var(--font-user-message);
+ transition: color 0.15s ease-in-out;
+}
+
+/* Hover state */
+[data-thinking-block-state="expanded"]
+ pre
+ .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty):hover::after {
+ color: hsl(var(--text-100));
+ content: "Hide Claude's thinking";
+}
+
+/* Streaming state styles */
+div[data-is-streaming="true"]
+ pre
+ .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::after {
+ content: "Claude is thinking...";
+ 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;
+ cursor: pointer;
+ font-family: var(--font-user-message);
+}
+
+/* Chevron-down icon */
+pre .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::before {
+ content: "";
+ width: 15px;
+ height: 15px;
+ margin-right: 0.25rem;
+ -webkit-mask-image: url('data:image/svg+xml;utf8,');
+ mask-image: url('data:image/svg+xml;utf8,');
+ -webkit-mask-size: contain;
+ mask-size: contain;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+ background-color: hsl(var(--text-500));
+ transform: translateY(-1px);
+ transition: transform 0.25s ease-out;
+}
+
+/* --------------------------------- */
+
+/* Shimmer animation for streaming state */
+@keyframes gradientWave {
+ 0% {
+ background-position: 200% 50%;
+ }
+ 100% {
+ background-position: -200% 50%;
+ }
+}
+
+/* --------------------------------- */
+
+/* Chevron animation */
+
+[data-thinking-block-state="collapsed"]
+ pre
+ .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::before {
+ transform: rotate(180deg);
+}
diff --git a/extensions/chrome/src/components/sample-ui.tsx b/extensions/chrome/src/components/sample-ui.tsx
new file mode 100644
index 0000000..4201e62
--- /dev/null
+++ b/extensions/chrome/src/components/sample-ui.tsx
@@ -0,0 +1,9 @@
+import React from "react"
+
+export const SampleApp: React.FC = () => {
+ return (
+
+ )
+}
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/skeleton.tsx b/extensions/chrome/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..d7e45f7
--- /dev/null
+++ b/extensions/chrome/src/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
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/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/index.ts b/extensions/chrome/src/content/index.ts
new file mode 100644
index 0000000..167eb16
--- /dev/null
+++ b/extensions/chrome/src/content/index.ts
@@ -0,0 +1,14 @@
+import "@/styles/globals.css"
+
+import { shouldInitialize } from "@/utils/url-utils"
+
+import { addThinkingBlockToggle } from "./v3/features/thinking-block"
+
+// Only initialize on appropriate pages
+if (shouldInitialize(window.location.href)) {
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", addThinkingBlockToggle)
+ } else {
+ addThinkingBlockToggle()
+ }
+}
diff --git a/extensions/chrome/src/content/index.tsx b/extensions/chrome/src/content/index.tsx
deleted file mode 100644
index 124e7d3..0000000
--- a/extensions/chrome/src/content/index.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { ThinkingBlockManager } from "./managers/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/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/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/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/index.ts b/extensions/chrome/src/selectors/index.ts
new file mode 100644
index 0000000..ffd0348
--- /dev/null
+++ b/extensions/chrome/src/selectors/index.ts
@@ -0,0 +1,4 @@
+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 d8fade3..63d0cf3 100644
--- a/extensions/chrome/src/styles/globals.css
+++ b/extensions/chrome/src/styles/globals.css
@@ -4,87 +4,47 @@
@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;
}
.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%;
- }
-}
-
-/* @layer base {
- * {
- @apply border-border;
- }
- body {
- @apply font-sans antialiased bg-background text-foreground;
- }
-}
-
-
-@layer base {
- :root {
- font-family: Inter, system-ui, sans-serif;
- }
-} */
-
-@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;
- }
-
- .btn-primary {
- @apply bg-primary-600 text-white hover:bg-primary-700;
- }
-
- .btn-secondary {
- @apply bg-bg-200 text-text-700 hover:bg-bg-300;
- }
-
- .btn-outline {
- @apply border border-bg-300 bg-transparent text-text-700 hover:bg-bg-100;
- }
-}
-
-@layer utilities {
- .text-gradient {
- @apply bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent;
+ --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%;
}
}
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/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
-}
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..996fdd6 100644
--- a/extensions/chrome/tailwind.config.cjs
+++ b/extensions/chrome/tailwind.config.cjs
@@ -1,50 +1,114 @@
+/**Know Issue for now:
+ * 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 = {
- 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}",
+ "./src/**/*.{ts,tsx,html,js,jsx}",
+ "./public/**/*.html",
+ ],
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))",
},
+ // 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,
},
- 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/test.txt b/extensions/chrome/test.txt
index 45f8b44..7001271 100644
--- a/extensions/chrome/test.txt
+++ b/extensions/chrome/test.txt
@@ -1 +1,2 @@
-Test file to verify github actions auto bumping
+Version Bumping Test File
+v3.1.0
\ No newline at end of file
diff --git a/extensions/chrome/webpack/webpack.common.js b/extensions/chrome/webpack/webpack.common.js
index a285e3b..1cfcc23 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: [
@@ -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"),
+ },
+ },
+ },
+ ],
},
],
},