diff --git a/.github/workflows/chrome-extension-ci.yml b/.github/workflows/chrome-extension-ci.yml new file mode 100644 index 0000000..93c21cc --- /dev/null +++ b/.github/workflows/chrome-extension-ci.yml @@ -0,0 +1,106 @@ +name: chrome-extension CI + +on: + push: + branches: [main] + paths: + - "extensions/chrome/**" + pull_request: + branches: [main] + paths: + - "extensions/chrome/**" + +defaults: + run: + working-directory: ./extensions/chrome + +jobs: + lint-and-format: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run ESLint and format fix + run: bun run fix + + - name: Run markdown lint + # DON't TOUCH THIS LINE BELOW # + run: bunx markdownlint-cli2 "./**/*.md" --config .markdownlint-cli2.jsonc + + test-and-build: + needs: lint-and-format + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test + + - name: Build extension + run: bun run build + + - name: Zip Extension + run: zip -r chrome-extension.zip dist/ + + # Automatically increments the patch version (e.g., 1.0.0 -> 1.0.1) + # and creates a release + # Only increment patch version for non-major versions + - name: Check existing tag + id: check_tag + run: | + current_version=$(node -p "require('./package.json').version") + 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 + else + echo "version=$current_version" >> $GITHUB_OUTPUT + echo "version_changed=false" >> $GITHUB_OUTPUT + fi + - name: Create Release + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: softprops/action-gh-release@v1 + with: + name: Chrome Extension v${{ steps.check_tag.outputs.version }} + tag_name: v${{ steps.check_tag.outputs.version }} + files: extensions/chrome/chrome-extension.zip + generate_release_notes: true + token: ${{ secrets.GITHUB_TOKEN }} + fail_on_unmatched_files: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ad9107 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Dependencies +node_modules/ +.pnp/ +.pnp.js +bun.lockb + +# Testing +coverage/ +.nyc_output/ + +# Production & Build files +dist/ +build/ +*.tsbuildinfo + +# Development & IDE +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.idea/ +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +debug.log +*.log + +# Cache directories +.npm/ +.eslintcache +.stylelintcache +.prettiercache +.cache/ + +# Chrome Extension specific +*.crx +*.pem +*.zip + +# Temporary files +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/.husky/post-commit b/.husky/post-commit new file mode 100644 index 0000000..cfd78ec --- /dev/null +++ b/.husky/post-commit @@ -0,0 +1,15 @@ +#!/usr/bin/env sh + +# DON'T TOUCH THIS FILE IF YOU DON'T KNOW WHAT YOU ARE DOING + +echo "Checking for changes in chrome..." +if git log -1 --name-only --pretty=format: | grep "^extensions/chrome" > /dev/null; then + echo "Changes detected, fetching..." + git fetch + echo "Fetch complete" + echo "Pulling changes..." + git pull + echo "Pull complete" +fi + +# TODO: config this for other extensions too diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..08b0e42 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +# DON'T TOUCH THIS FILE IF YOU DON'T KNOW WHAT YOU ARE DOING + +# Check if there are changes in extensions directory +if ! git diff --cached --name-only | grep "^extensions/chrome" > /dev/null; then + echo "No changes in chrome" + exit 0 +fi + +if ! cd extensions/chrome; then + echo "Failed to change directory" + exit 1 +fi + +if ! bun run lint:staged; then + echo "Lint failed" + exit 1 +fi + +# TODO: config this for other extensions too \ No newline at end of file diff --git a/README.md b/README.md index 6881789..f530403 100644 --- a/README.md +++ b/README.md @@ -6,71 +6,93 @@ Let Claude think comprehensively before responding! > Thinking claude **is not aimed for benchmarks or huge leaps in math or something**, since those are pre-determined by the base model (new Claude-3.5 Sonnet). > I only want to explore how further we could reach with Claude's "deep mindset". That said, when using it in your daily tasks, you will find Claude's inner monolog (thinking process) very very fun and interesting. -## Demo: - +## Demo https://github.com/user-attachments/assets/88ff0c75-c51b-42b9-a042-00d47053795a - ## Overview This project consists of two main components: + 1. **Thinking Protocol**: A comprehensive set of instructions that guides Claude to think deeply and systematically before responding 2. **Browser Extension**: A tool that makes Claude's thinking process more readable and manageable in the browser interface ## Project Structure - thinking-claude/ - ├── extension/ - │ ├── .vscode/ - │ ├── chrome/ - │ ├── firefox/ - │ └── changelog.md - ├── model_instructions/ - │ ├── changelog.md - │ ├── v3.5-20241113.md - │ ├── v4-20241118.md - │ └── v4-lite-20241118.md - ├── LICENSE - └── README.md -The project is organized into two main directories: -- `extension/`: Contains browser extension implementations -- `model_instructions/`: Contains thinking protocols for different versions - -Each directory maintains its own changelog for version tracking. + +```bash +thinking-claude/ +├── extensions/ +│ ├── chrome/ # Current version of Chrome extension +│ ├── chrome_v0/ # Legacy Chrome extension (deprecated) +│ ├── firefox/ # Firefox extension (in development) +│ └── changelog.md +├── model_instructions/ +│ ├── changelog.md +│ ├── v5-Exp-20241123.md +│ ├── v4-20241118.md +│ ├── v4-lite-20241118.md +│ └── v3.5-20241113.md +├── .github/ # GitHub configurations and workflows +├── .husky/ # Git hooks for development +├── LICENSE +└── README.md +``` + +The project is organized into two main components: + +- `extensions/`: Browser extension implementations + + - `chrome/`: Current version with modern architecture and features + - `chrome_v0/`: Legacy version (deprecated) + - `firefox/`: Firefox version (in development) + +- `model_instructions/`: Thinking protocols for different versions + - Contains versioned instruction sets + - Each version brings improvements to Claude's thinking process + ## Thinking Protocol The thinking protocol instructs Claude to follow a natural, thorough thought process before providing responses. ## Browser Extension -The browser extension enhances the Claude interface by making the thinking process more manageable: +The browser extension makes Claude's thinking process easier to read and use! It automatically organizes Claude's thoughts into neat, collapsible sections. ### Features -- 🔄 Collapsible thinking process sections -- 📋 Easy copy functionality -- 🎯 Clean and intuitive interface -- ⚡ Automatic processing of new messages -### Installation +- 🎯 Makes Claude's thinking process easy to read +- 🔄 Fold and unfold different parts of Claude's thoughts +- 📋 Copy any part with just one click +- ⚡ Works automatically with new messages +- 🎨 Clean, modern design that's easy on the eyes + +### 🚀 Quick Install Guide -#### Chrome +1. **Chrome Users (Recommended)** -**Quick Install:** -Install directly from the [Chrome Web Store](https://chromewebstore.google.com/detail/thinking-claude/ncjafpbbndpggfhfgjngkcimeaciahpo) + - Install directly from the [Chrome Web Store](https://chromewebstore.google.com/detail/thinking-claude/ncjafpbbndpggfhfgjngkcimeaciahpo) -**Manual Installation:** -1. Clone the repository: - ```bash - git clone https://github.com/yourusername/thinking-claude.git -2. Open Chrome and navigate to `chrome://extensions/` -3. Enable "Developer mode" -4. Click "Load unpacked" and select the `extension/chrome` folder +2. **Manual Installation** + - Download the latest version from our [Releases Page](https://github.com/richards199999/Thinking-Claude/releases) + - Unzip the file + - Open Chrome and go to `chrome://extensions/` + - Turn on "Developer mode" (top right corner) + - Click "Load unpacked" and select the unzipped folder -#### Firefox -1. Clone this repository -2. Open Firefox and navigate to `about:debugging#/runtime/this-firefox` -3. Click "Load Temporary Add-on" -4. Navigate to the repository and select the `extension/firefox/manifest.json` file +👉 Want more details? Check out our [Extension Guide](extensions/chrome/README.md) for: + +- Step-by-step installation instructions +- Development setup +- Advanced features and usage +- Troubleshooting tips + +### 🎉 Getting Started + +Once installed, just: + +1. Visit [Claude.ai](https://claude.ai) +2. Start chatting with Claude +3. That's it! The extension will automatically make Claude's thinking process more readable ## Usage @@ -79,11 +101,12 @@ Install directly from the [Chrome Web Store](https://chromewebstore.google.com/d 1. Copy the latest version in `model_instructions` folder 2. Start a new Project in Claude.ai 3. Paste the instructions to the Custom Instructions section -3. Claude will now follow the thinking protocol for all subsequent interactions +4. Claude will now follow the thinking protocol for all subsequent interactions ### Using the Extension Once installed, the extension automatically: + - Detects Claude's thinking process blocks - Adds collapse/expand functionality - Provides a copy button for each block @@ -98,6 +121,7 @@ Once installed, the extension automatically: ## Contributing Contributions are welcome! Feel free to: + - Submit bug reports - Propose new features - Create pull requests diff --git a/extension/changelog.md b/extensions/changelog.md similarity index 100% rename from extension/changelog.md rename to extensions/changelog.md diff --git a/extensions/chrome/.gitignore b/extensions/chrome/.gitignore new file mode 100644 index 0000000..250ee83 --- /dev/null +++ b/extensions/chrome/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Package manager files +bun.lockb + +# Build output +dist/ +build/ + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Environment variables +.env +.env.local +.env.*.local + +# Debug logs +debug.log + +# Test coverage +coverage/ diff --git a/extensions/chrome/.markdownlint-cli2.jsonc b/extensions/chrome/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..15db2a5 --- /dev/null +++ b/extensions/chrome/.markdownlint-cli2.jsonc @@ -0,0 +1,19 @@ +{ + "config": { + "default": true, + // MD013/line-length : Line length + "MD013": false, + // MD033/no-inline-html : Inline HTML + "MD033": false, + // MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading + "MD041": false, + // MD032/blanks-around-lists : Lists should be surrounded by blank lines + "MD032": true, + // MD024/no-duplicate-heading : Multiple headings with the same content + "MD024": false, + // MD040/fenced-code-language : Fenced code blocks should have a language specified + "MD040": false, + }, + "ignores": ["node_modules/**", "dist/**", ".git/**"], + "globs": ["**/*.md"], +} diff --git a/extensions/chrome/.markdownlintignore b/extensions/chrome/.markdownlintignore new file mode 100644 index 0000000..f1d6b92 --- /dev/null +++ b/extensions/chrome/.markdownlintignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.git/ diff --git a/extensions/chrome/CHANGELOG.md b/extensions/chrome/CHANGELOG.md new file mode 100644 index 0000000..b639290 --- /dev/null +++ b/extensions/chrome/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +## 2024-11-23 + +### Important Notice 🔄 + +The original Chrome extension (now referred to as `chrome_v0`) has been deprecated and its development has been discontinued. This changelog tracks the new rewritten version (`chrome`) which offers improved architecture, better performance, and enhanced maintainability. + +### Changed + +- Complete rewrite of the extension with modern tech stack +- Improved CI/CD pipeline with automatic version syncing +- Added git pull to post-commit hook for auto-sync + +## 2024-11-22 + +### Added + +- Initial project structure and configuration setup for complete rewrite +- Deprecated old extension (`chrome_v0`) and started fresh development +- GitHub Actions workflow for Chrome extension +- Husky git hooks for code quality +- Automatic version bumping and release workflow +- Automatic GitHub release creation + +### Changed + +- Reorganized extension directory structure +- Improved development environment configuration + +### Technical + +- Set up manifest.json with required permissions and content scripts +- Configured extension icons +- Implemented storage permission for future feature development diff --git a/extensions/chrome/README.md b/extensions/chrome/README.md new file mode 100644 index 0000000..7b0cc45 --- /dev/null +++ b/extensions/chrome/README.md @@ -0,0 +1,238 @@ +# Thinking Claude Chrome Extension + +A Chrome extension that enhances Claude's thinking process, making it more human-like and transparent. + +> **Important Notice**: The original Chrome extension (`chrome_v0`) has been deprecated. This is the new rewritten version (`chrome`) with improved architecture and modern tech stack. If you're using the old version, please update to this new version for better performance and continued support. + +## How to Use 🚀 + +### Option 1: Direct Installation (Recommended) + +1. **Download the Extension** + + - Go to [Latest Releases](https://github.com/richards199999/Thinking-Claude/releases) + - Download the latest version (e.g., `thinking-claude-v1.0.2.zip`) + - Extract the ZIP file + +2. **Install in Chrome** + + - Open Chrome and go to `chrome://extensions/` + - Enable "Developer mode" in the top right + - Click "Load unpacked" + - Select the `dist` folder in the extracted folder + +3. **Start Using** + - Visit [Claude.ai](https://claude.ai) + - Start a new conversation or refresh an existing one + - The extension will automatically enhance Claude's thinking process + +### Option 2: Build Locally (For Development) + +1. **Quick Setup** + + ```bash + # Clone the repository + git clone https://github.com/richards199999/Thinking-Claude.git + cd Thinking-Claude/extensions/chrome + + # Install dependencies + bun install + + # Build the extension + bun run build + ``` + +2. **Load in Chrome** + + - Open Chrome and go to `chrome://extensions/` + - Enable "Developer mode" in the top right + - Click "Load unpacked" + - Select the `dist` folder (created after building) + +3. **Development Mode** + + ```bash + # Start development server with hot reload + bun run start + + # Watch for changes + bun run watch + ``` + +## Tech Stack 🛠️ + +### Core Technologies + +- **Language & Type Safety** + + - [TypeScript](https://www.typescriptlang.org/) - Strongly typed programming language + - [ESLint](https://eslint.org/) - Code linting and standards + - [Prettier](https://prettier.io/) - Code formatting + +- **Frontend** + - [React](https://react.dev/) - UI library + - [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework + - [shadcn/ui](https://ui.shadcn.com/) - Best UI components + - [Chrome Extension API](https://developer.chrome.com/docs/extensions/) - Browser extension development + +### Development Tools + +- **Build & Bundle** + + - [Bun](https://bun.sh) - JavaScript all-in-one toolkit + - [Webpack](https://webpack.js.org/) - Module bundler + - [PostCSS](https://postcss.org/) - CSS processing + +- **Testing & Quality** + + - [Vitest](https://vitest.dev/) - Unit testing framework + - [Husky](https://typicode.github.io/husky/) - Git hooks + - [lint-staged](https://github.com/okonet/lint-staged) - Staged files linter + +- **Development Environment** + - [Node.js](https://nodejs.org/) - JavaScript runtime + - [Chrome DevTools](https://developer.chrome.com/docs/devtools/) - Browser debugging + +## Getting Started with development 🚀 + +### What You'll Need + +Required tools: + +- [Bun](https://bun.sh) - A fast all-in-one JavaScript runtime & toolkit +- [Node.js](https://nodejs.org/) (v18 or higher) - JavaScript runtime environment +- [Git](https://git-scm.com/downloads) - For version control +- [Google Chrome](https://www.google.com/chrome/) - The browser we're building for + +This extension uses: + +- [TypeScript](https://www.typescriptlang.org/) - Type-safe JavaScript +- [React](https://react.dev/) - UI framework +- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework +- [Webpack](https://webpack.js.org/) - Module bundler + +#### Installing Node.js + +1. Download Node.js from [nodejs.org](https://nodejs.org/) +2. Choose the LTS (Long Term Support) version +3. Run the installer +4. Verify installation: + + ```bash + node --version + npm --version + ``` + +#### Installing Bun + +Bun is required to run this project. Here's how to install it: + +**Windows Users:** + +1. First, install Windows Subsystem for Linux (WSL): + + ```powershell + # Open PowerShell as Administrator and run: + wsl --install + ``` + + After installation, restart your computer. + +2. Install Bun through WSL: + + ```bash + # Open WSL terminal and run: + curl -fsSL https://bun.sh/install | bash + ``` + +**macOS or Linux Users:** + +```bash +# Open terminal and run: +curl -fsSL https://bun.sh/install | bash +``` + +To verify installation, run: + +```bash +bun --version +``` + +### Setting Up Your Development Environment + +1. Get the code: + + ```bash + # Clone this repository to your computer + git clone https://github.com/richards199999/Thinking-Claude.git + + # Go to the extension directory + cd extensions/chrome + + # Install project dependencies + bun install + ``` + +### Development Commands + +Here are the main commands you'll use during development: + +```bash +# Build the extension for production +bun run build + +# Start development mode with auto-reload +bun run start + +# Watch for file changes +bun run watch + +# Run tests +bun run test + +# Fix code style and formatting +bun run fix +``` + +### Installing the Extension in Chrome + +1. Open Chrome and type `chrome://extensions/` in the address bar +2. Turn on "Developer mode" using the switch in the top right corner +3. Click "Load unpacked" and select the `dist (visible after running bun run build)` folder from this project + +## Project Organization 📁 + +``` +chrome/ +├── src/ # Your source code goes here +├── public/ # Built extension (created after running build) +│ ├── manifest.json # Extension configuration +│ ├── content.js # Main extension script +│ └── icons/ # Extension icons +├── package.json # Project configuration and scripts +└── CHANGELOG.md # Version history and changes +``` + +## Development Workflow 🔄 + +### Code Quality Tools + +We use several tools to maintain code quality: + +- **Husky**: Automatically checks your code before commits +- **ESLint**: Finds and fixes JavaScript problems +- **Prettier**: Formats your code consistently + +### Continuous Integration + +Our GitHub Actions setup automatically: + +- Builds the extension +- Updates version numbers +- Creates new releases + +## Need Help? 🤔 + +- Check the [CHANGELOG.md](./CHANGELOG.md) for recent updates +- Visit our [GitHub Issues](https://github.com/richards199999/Thinking-Claude/issues) for known problems or to report new ones +- Feel free to ask questions in our GitHub Discussions diff --git a/extensions/chrome/components.json b/extensions/chrome/components.json new file mode 100644 index 0000000..23aac84 --- /dev/null +++ b/extensions/chrome/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/styles/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/extensions/chrome/eslint.config.cjs b/extensions/chrome/eslint.config.cjs new file mode 100644 index 0000000..640b2bc --- /dev/null +++ b/extensions/chrome/eslint.config.cjs @@ -0,0 +1,92 @@ +/** @type {import('eslint').Config[]} */ +module.exports = [ + { + ignores: [ + "**/dist/**", + "**/node_modules/**", + "**/coverage/**", + "**/*.config.js", + ], + }, + require("@eslint/js").configs.recommended, + { + files: ["**/*.{ts,tsx,js,jsx}"], + languageOptions: { + ecmaVersion: 2021, + sourceType: "module", + parser: require("@typescript-eslint/parser"), + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + chrome: "readonly", + console: "readonly", + MutationObserver: "readonly", + window: "readonly", + document: "readonly", + HTMLElement: "readonly", + Element: "readonly", + setTimeout: "readonly", + navigator: "readonly", + setInterval: "readonly", + Node: "readonly", + HTMLButtonElement: "readonly", + }, + }, + plugins: { + "@typescript-eslint": require("@typescript-eslint/eslint-plugin"), + react: require("eslint-plugin-react"), + "react-hooks": require("eslint-plugin-react-hooks"), + }, + rules: { + ...require("@typescript-eslint/eslint-plugin").configs.recommended.rules, + ...require("eslint-plugin-react").configs.recommended.rules, + ...require("eslint-plugin-react-hooks").configs.recommended.rules, + "react/react-in-jsx-scope": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "no-console": "off", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "no-inline-styles": "off", + }, + settings: { + react: { + version: "detect", + }, + }, + }, + // Config for test files + { + files: ["**/__tests__/**/*", "**/*.test.*"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "no-console": "off", + }, + }, + // Config for configuration files + { + files: ["*.config.js", "*.config.cjs", "webpack/**/*.js"], + languageOptions: { + globals: { + module: "readonly", + require: "readonly", + __dirname: "readonly", + }, + }, + rules: { + "@typescript-eslint/no-require-imports": "off", + "no-undef": "off", + }, + }, + require("eslint-config-prettier"), +] diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json new file mode 100644 index 0000000..7c1de56 --- /dev/null +++ b/extensions/chrome/package.json @@ -0,0 +1,70 @@ +{ + "name": "thinking-claude", + "version": "3.0.0", + "description": "Chrome extension for letting Claude think like a real human", + "type": "module", + "scripts": { + "watch": "webpack --config webpack/webpack.dev.js --watch", + "build": "webpack --config webpack/webpack.prod.js", + "start": "webpack serve --config webpack/webpack.dev.js", + "test": "bun test", + "test:watch": "bun test --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "lint:staged": "lint-staged", + "lint:types": "tsc --noEmit", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,css,md}\"", + "fix": "bun run format && bun run lint:fix", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.460.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwind-merge": "^1.14.0" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@ianvs/prettier-plugin-sort-imports": "^3.7.2", + "@types/chrome": "^0.0.246", + "@types/node": "^20.8.2", + "@types/react": "^18.2.24", + "@types/react-dom": "^18.2.8", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", + "@vitest/runner": "^2.1.5", + "autoprefixer": "^10.4.16", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "eslint": "^9.15.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "husky": "^9.1.7", + "identity-obj-proxy": "^3.0.0", + "lint-staged": "^14.0.1", + "markdownlint-cli2": "^0.15.0", + "postcss": "^8.4.31", + "postcss-loader": "^7.3.3", + "postcss-nesting": "^12.0.1", + "style-loader": "^3.3.3", + "tailwindcss": "^3.3.3", + "tailwindcss-animate": "^1.0.7", + "ts-loader": "^9.4.4", + "typescript": "^5.2.2", + "vitest": "^2.1.5", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1", + "webpack-merge": "^5.9.0" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc --fix" + } +} diff --git a/extensions/chrome/postcss.config.mjs b/extensions/chrome/postcss.config.mjs new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/extensions/chrome/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/extensions/chrome/prettier.config.cjs b/extensions/chrome/prettier.config.cjs new file mode 100644 index 0000000..1c386ce --- /dev/null +++ b/extensions/chrome/prettier.config.cjs @@ -0,0 +1,33 @@ +/** @type {import('prettier').Config} */ +module.exports = { + endOfLine: "lf", + semi: false, + singleQuote: false, + tabWidth: 2, + trailingComma: "es5", + importOrder: [ + "^(react/(.*)$)|^(react$)", + "^(next/(.*)$)|^(next$)", + "", + "", + "^types$", + "^@/types/(.*)$", + "^@/config/(.*)$", + "^@/lib/(.*)$", + "^@/hooks/(.*)$", + "^@/components/ui/(.*)$", + "^@/components/(.*)$", + "^@/registry/(.*)$", + "^@/styles/(.*)$", + "^@/app/(.*)$", + "", + "^[./]", + ], + importOrderSeparation: true, + importOrderSortSpecifiers: true, + importOrderBuiltinModulesToTop: true, + importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + plugins: ["@ianvs/prettier-plugin-sort-imports"], +} diff --git a/extensions/chrome/public/icons/thinking-claude-128.png b/extensions/chrome/public/icons/thinking-claude-128.png new file mode 100644 index 0000000..042c921 Binary files /dev/null and b/extensions/chrome/public/icons/thinking-claude-128.png differ diff --git a/extensions/chrome/public/icons/thinking-claude-16.png b/extensions/chrome/public/icons/thinking-claude-16.png new file mode 100644 index 0000000..06e6c0c Binary files /dev/null and b/extensions/chrome/public/icons/thinking-claude-16.png differ diff --git a/extensions/chrome/public/icons/thinking-claude-48.png b/extensions/chrome/public/icons/thinking-claude-48.png new file mode 100644 index 0000000..a4be7f3 Binary files /dev/null and b/extensions/chrome/public/icons/thinking-claude-48.png differ diff --git a/extensions/chrome/public/manifest.json b/extensions/chrome/public/manifest.json new file mode 100644 index 0000000..a73e4eb --- /dev/null +++ b/extensions/chrome/public/manifest.json @@ -0,0 +1,26 @@ +{ + "manifest_version": 3, + "name": "Thinking Claude", + "version": "3.0.0", + "description": "Chrome extension for letting Claude think like a real human", + "content_scripts": [ + { + "matches": ["https://*.claude.ai/*"], + "js": ["content.js"] + } + ], + "icons": { + "16": "icons/thinking-claude-16.png", + "48": "icons/thinking-claude-48.png", + "128": "icons/thinking-claude-128.png" + }, + "action": { + "default_icon": { + "16": "icons/thinking-claude-16.png", + "48": "icons/thinking-claude-48.png", + "128": "icons/thinking-claude-128.png" + }, + "default_title": "Thinking Claude" + }, + "permissions": ["storage"] +} diff --git a/extensions/chrome/src/__tests__/sample.test.ts b/extensions/chrome/src/__tests__/sample.test.ts new file mode 100644 index 0000000..b64d198 --- /dev/null +++ b/extensions/chrome/src/__tests__/sample.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi, type Mock } from "vitest" + +interface ChromeMock { + runtime: { + sendMessage: Mock + onMessage: { + addListener: Mock + } + } + storage: { + local: { + get: Mock + set: Mock + } + } +} + +// Mock chrome API +const mockChrome = { + runtime: { + sendMessage: vi.fn(), + onMessage: { + addListener: vi.fn(), + }, + }, + storage: { + local: { + get: vi.fn().mockImplementation(() => Promise.resolve({})), + set: vi.fn().mockImplementation(() => Promise.resolve()), + }, + }, +} as unknown as ChromeMock + +// Add chrome to global +;(globalThis as any).chrome = mockChrome + +describe("Chrome Extension Basic Tests", () => { + beforeEach(() => { + // Clear all mocks before each test + vi.clearAllMocks() + }) + + it("should send messages correctly", () => { + const message = { type: "TEST_MESSAGE", data: "test data" } + chrome.runtime.sendMessage(message) + + expect(mockChrome.runtime.sendMessage).toHaveBeenCalledWith(message) + }) + + it("should handle storage operations", async () => { + const testData = { key: "value" } + + // Setup the mock to return our test data + mockChrome.storage.local.get.mockImplementationOnce(() => + Promise.resolve(testData) + ) + + const result = await chrome.storage.local.get("key") + expect(result).toEqual(testData) + }) + + it("should add message listeners", () => { + const messageListener = (message: any) => console.log(message) + chrome.runtime.onMessage.addListener(messageListener) + + expect(mockChrome.runtime.onMessage.addListener).toHaveBeenCalledWith( + messageListener + ) + }) +}) diff --git a/extensions/chrome/src/constants/constants.ts b/extensions/chrome/src/constants/constants.ts new file mode 100644 index 0000000..c4183b9 --- /dev/null +++ b/extensions/chrome/src/constants/constants.ts @@ -0,0 +1,94 @@ +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.tsx b/extensions/chrome/src/content/index.tsx new file mode 100644 index 0000000..124e7d3 --- /dev/null +++ b/extensions/chrome/src/content/index.tsx @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..1a69cd6 --- /dev/null +++ b/extensions/chrome/src/content/managers/README.md @@ -0,0 +1,133 @@ +# 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 new file mode 100644 index 0000000..702883c --- /dev/null +++ b/extensions/chrome/src/content/managers/thinking-block-manager/events/event-manager.ts @@ -0,0 +1,91 @@ +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 new file mode 100644 index 0000000..2b20f10 --- /dev/null +++ b/extensions/chrome/src/content/managers/thinking-block-manager/index.ts @@ -0,0 +1,78 @@ +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 new file mode 100644 index 0000000..1e5aaa6 --- /dev/null +++ b/extensions/chrome/src/content/managers/thinking-block-manager/observer/dom-observer-manager.ts @@ -0,0 +1,85 @@ +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 new file mode 100644 index 0000000..c3c4eec --- /dev/null +++ b/extensions/chrome/src/content/managers/thinking-block-manager/styles/style-manager.ts @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..f00d9ad --- /dev/null +++ b/extensions/chrome/src/content/managers/thinking-block-manager/ui/ui-component-manager.ts @@ -0,0 +1,113 @@ +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/lib/utils.ts b/extensions/chrome/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/extensions/chrome/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/extensions/chrome/src/styles/globals.css b/extensions/chrome/src/styles/globals.css new file mode 100644 index 0000000..d8fade3 --- /dev/null +++ b/extensions/chrome/src/styles/globals.css @@ -0,0 +1,90 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@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; + } + + .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; + } +} diff --git a/extensions/chrome/src/types/css.d.ts b/extensions/chrome/src/types/css.d.ts new file mode 100644 index 0000000..001522b --- /dev/null +++ b/extensions/chrome/src/types/css.d.ts @@ -0,0 +1,28 @@ +/** Extension for typescript if we need to import css files + * what this does is that it tells typescript that the styles object is an object with string keys and values + * example usage + * + * import styles from './styles.css'; + * Now TypeScript knows that styles is an object with string keys and values element.className = styles.someClass; + * + */ + +declare module "*.css" { + const content: { [className: string]: string } + export default content +} + +declare module "*.scss" { + const content: { [className: string]: string } + export default content +} + +declare module "*.sass" { + const content: { [className: string]: string } + export default content +} + +declare module "*.less" { + const content: { [className: string]: string } + export default content +} diff --git a/extensions/chrome/src/types/index.ts b/extensions/chrome/src/types/index.ts new file mode 100644 index 0000000..d8e8c1a --- /dev/null +++ b/extensions/chrome/src/types/index.ts @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..8d573e7 --- /dev/null +++ b/extensions/chrome/src/utils/dom-utils.ts @@ -0,0 +1,44 @@ +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/tailwind.config.cjs b/extensions/chrome/tailwind.config.cjs new file mode 100644 index 0000000..53bc121 --- /dev/null +++ b/extensions/chrome/tailwind.config.cjs @@ -0,0 +1,50 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ['class'], + 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))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: `var(--radius)`, + md: `calc(var(--radius) - 2px)`, + sm: 'calc(var(--radius) - 4px)', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; diff --git a/extensions/chrome/test.txt b/extensions/chrome/test.txt new file mode 100644 index 0000000..45f8b44 --- /dev/null +++ b/extensions/chrome/test.txt @@ -0,0 +1 @@ +Test file to verify github actions auto bumping diff --git a/extensions/chrome/tsconfig.json b/extensions/chrome/tsconfig.json new file mode 100644 index 0000000..a43bacc --- /dev/null +++ b/extensions/chrome/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": false, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "typeRoots": ["node_modules/@types", "src/types"], + "types": ["node", "chrome"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build", "dist"] +} diff --git a/extensions/chrome/vitest.config.ts b/extensions/chrome/vitest.config.ts new file mode 100644 index 0000000..9ea1a8b --- /dev/null +++ b/extensions/chrome/vitest.config.ts @@ -0,0 +1,10 @@ +/// +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + include: ["src/**/*.{test,spec}.{ts,tsx}"], + environment: "node", + runner: "vitest", + }, +}) diff --git a/extensions/chrome/webpack/webpack.common.js b/extensions/chrome/webpack/webpack.common.js new file mode 100644 index 0000000..a285e3b --- /dev/null +++ b/extensions/chrome/webpack/webpack.common.js @@ -0,0 +1,57 @@ +import path from "path" +import { fileURLToPath } from "url" + +import CopyPlugin from "copy-webpack-plugin" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +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"), + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: [ + { + loader: "ts-loader", + options: { + configFile: path.resolve(__dirname, "..", "tsconfig.json"), + transpileOnly: true, + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader", "postcss-loader"], + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + alias: { + "@": path.resolve(__dirname, "..", "src"), + }, + }, + output: { + path: path.resolve(__dirname, "..", "dist"), + filename: "[name].js", + clean: true, + }, + plugins: [ + new CopyPlugin({ + patterns: [ + { + from: path.resolve(__dirname, "..", "public"), + to: path.resolve(__dirname, "..", "dist"), + }, + ], + }), + ], +} diff --git a/extensions/chrome/webpack/webpack.dev.js b/extensions/chrome/webpack/webpack.dev.js new file mode 100644 index 0000000..b5f64fd --- /dev/null +++ b/extensions/chrome/webpack/webpack.dev.js @@ -0,0 +1,12 @@ +import { merge } from "webpack-merge" + +import common from "./webpack.common.js" + +export default merge(common, { + mode: "development", + devtool: "inline-source-map", + devServer: { + static: "./dist", + hot: true, + }, +}) diff --git a/extensions/chrome/webpack/webpack.prod.js b/extensions/chrome/webpack/webpack.prod.js new file mode 100644 index 0000000..c069fca --- /dev/null +++ b/extensions/chrome/webpack/webpack.prod.js @@ -0,0 +1,8 @@ +import { merge } from "webpack-merge" + +import common from "./webpack.common.js" + +export default merge(common, { + mode: "production", + devtool: false, +}) diff --git a/extension/.vscode/extensions.json b/extensions/chrome_v0/.vscode/extensions.json similarity index 100% rename from extension/.vscode/extensions.json rename to extensions/chrome_v0/.vscode/extensions.json diff --git a/extensions/chrome_v0/README.md b/extensions/chrome_v0/README.md new file mode 100644 index 0000000..d8dd563 --- /dev/null +++ b/extensions/chrome_v0/README.md @@ -0,0 +1,38 @@ +# ⚠️ Deprecated: Thinking Claude Chrome Extension (v0) + +> **Important**: This version of the Chrome extension (`chrome_v0`) has been deprecated and is no longer being maintained. Please use our new version for continued support and improvements. + +## Migration Notice 🔄 + +### Why are we deprecating this version? + +We've completely rewritten the extension with: + +- Modern architecture and tech stack +- Improved performance +- Better maintainability +- Enhanced developer experience + +### How to Update? + +Please switch to our new version (`chrome`): + +1. Uninstall this version (`chrome_v0`) from Chrome +2. Visit our [Latest Releases](https://github.com/richards199999/Thinking-Claude/releases) or refer to the [README.md](https://github.com/richards199999/Thinking-Claude/tree/main/extensions/chrome/README.md) in `extensions/chrome` +3. Download and install the new version + +For installation instructions and documentation, see: + +- [New Extension Documentation](https://github.com/richards199999/Thinking-Claude/tree/main/extensions/chrome/README.md) +- [Changelog](https://github.com/richards199999/Thinking-Claude/tree/main/extensions/chrome/CHANGELOG.md) + +## Support + +- This version will remain available for historical reference +- No new features or bug fixes will be added +- For any issues, please use the new version +- Report problems with the new version in our [GitHub Issues](https://github.com/richards199999/Thinking-Claude/issues) + +--- + +Thank you for using Thinking Claude! We're committed to providing the best possible experience, which is why we've moved to a new, improved version. diff --git a/extension/chrome/content.js b/extensions/chrome_v0/content.js similarity index 100% rename from extension/chrome/content.js rename to extensions/chrome_v0/content.js diff --git a/extension/chrome/manifest.json b/extensions/chrome_v0/manifest.json similarity index 92% rename from extension/chrome/manifest.json rename to extensions/chrome_v0/manifest.json index 3656176..38c77a7 100644 --- a/extension/chrome/manifest.json +++ b/extensions/chrome_v0/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Thinking Claude", - "version": "2.1", + "version": "0.2.1", "description": "Let Claude think. Makes Claude's thinking process expandable and collapsible.", "content_scripts": [ { diff --git a/extension/firefox/content.js b/extensions/firefox/content.js similarity index 100% rename from extension/firefox/content.js rename to extensions/firefox/content.js diff --git a/extension/firefox/manifest.json b/extensions/firefox/manifest.json similarity index 100% rename from extension/firefox/manifest.json rename to extensions/firefox/manifest.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa3d76f --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "thinking-claude-root", + "private": true, + "scripts": { + "prepare": "husky" + }, + "devDependencies": { + "husky": "^9.1.7" + } +}