[
  {
    "path": ".cargo/config.toml",
    "content": "[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"\n\n[env]\nPKG_CONFIG_ALLOW_CROSS = \"1\"\n"
  },
  {
    "path": ".github/workflows/build-linux.yml",
    "content": "name: Build Linux\n\non:\n  workflow_call:\n  workflow_dispatch:\n  push:\n    branches: [main]\n\njobs:\n  build:\n    name: Build Linux x86_64\n    runs-on: ubuntu-latest\n    \n    steps:\n      - uses: actions/checkout@v4\n      \n      - name: Install system dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y \\\n            pkg-config \\\n            libwebkit2gtk-4.1-dev \\\n            libgtk-3-dev \\\n            libssl-dev \\\n            libayatana-appindicator3-dev \\\n            librsvg2-dev\n      \n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: x86_64-unknown-linux-gnu\n      \n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: src-tauri\n      \n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n      \n      - name: Install dependencies\n        run: bun install\n      \n      - name: Build Tauri app\n        run: bun run tauri build --target x86_64-unknown-linux-gnu\n      \n      - name: Create artifacts directory\n        run: |\n          mkdir -p dist/linux-x86_64\n          cp src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb dist/linux-x86_64/ || true\n          cp src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage dist/linux-x86_64/ || true\n          \n          # Generate checksums\n          cd dist/linux-x86_64\n          sha256sum * > checksums.txt\n      \n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux-x86_64\n          path: dist/linux-x86_64/*\n"
  },
  {
    "path": ".github/workflows/build-macos.yml",
    "content": "name: Build macOS\n\non:\n  workflow_call:\n    secrets:\n      APPLE_CERTIFICATE:\n        required: true\n      APPLE_CERTIFICATE_PASSWORD:\n        required: true\n      KEYCHAIN_PASSWORD:\n        required: true\n      APPLE_SIGNING_IDENTITY:\n        required: true\n      APPLE_ID:\n        required: true\n      APPLE_TEAM_ID:\n        required: true\n      APPLE_PASSWORD:\n        required: true\n  workflow_dispatch:\n    inputs:\n      skip_build:\n        description: 'Skip build and use artifacts from a previous run'\n        required: false\n        default: false\n        type: boolean\n      run_id:\n        description: 'Run ID to download artifacts from (leave empty for latest)'\n        required: false\n        type: string\n  push:\n    branches: [main]\n\njobs:\n  build:\n    name: Build macOS ${{ matrix.target }}\n    if: ${{ !inputs.skip_build }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        include:\n          - os: macos-13  # Intel\n            target: x86_64-apple-darwin\n            arch: x86_64\n          - os: macos-14  # Apple Silicon\n            target: aarch64-apple-darwin\n            arch: aarch64\n    \n    steps:\n      - uses: actions/checkout@v4\n      \n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n      \n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: src-tauri\n      \n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n      \n      - name: Install dependencies\n        run: bun install\n      \n      - name: Import Apple certificates\n        env:\n          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}\n          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}\n          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}\n        run: |\n          # Create variables\n          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12\n          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db\n          \n          # Import certificate from secrets\n          echo -n \"$APPLE_CERTIFICATE\" | base64 --decode -o $CERTIFICATE_PATH\n          \n          # Create temporary keychain\n          security create-keychain -p \"$KEYCHAIN_PASSWORD\" $KEYCHAIN_PATH\n          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH\n          security unlock-keychain -p \"$KEYCHAIN_PASSWORD\" $KEYCHAIN_PATH\n          \n          # Import certificate to keychain\n          security import $CERTIFICATE_PATH -P \"$APPLE_CERTIFICATE_PASSWORD\" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH\n          security set-key-partition-list -S apple-tool:,apple: -k \"$KEYCHAIN_PASSWORD\" $KEYCHAIN_PATH\n          security list-keychain -d user -s $KEYCHAIN_PATH\n      \n      - name: Build native\n        env:\n          CI: true\n        run: bun run tauri build\n      \n      - name: Upload architecture-specific artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: macos-${{ matrix.arch }}\n          path: |\n            src-tauri/target/release/bundle/macos/opcode.app\n            src-tauri/target/release/bundle/dmg/*.dmg\n          retention-days: 1\n  \n  universal:\n    name: Create Universal Binary\n    needs: [build]\n    if: ${{ !cancelled() && (needs.build.result == 'success' || needs.build.result == 'skipped') }}\n    runs-on: macos-latest\n    steps:\n      - uses: actions/checkout@v4\n      \n      - name: Download artifacts from current workflow\n        if: ${{ !inputs.skip_build }}\n        uses: actions/download-artifact@v4\n        with:\n          pattern: macos-*\n          path: artifacts\n      \n      - name: Download artifacts from specific run\n        if: ${{ inputs.skip_build && inputs.run_id != '' }}\n        uses: dawidd6/action-download-artifact@v3\n        with:\n          workflow: build-macos.yml\n          run_id: ${{ inputs.run_id }}\n          name: macos-*\n          path: artifacts\n      \n      - name: Download artifacts from latest run\n        if: ${{ inputs.skip_build && inputs.run_id == '' }}\n        uses: dawidd6/action-download-artifact@v3\n        with:\n          workflow: build-macos.yml\n          workflow_conclusion: success\n          name: macos-*\n          path: artifacts\n      \n      - name: List downloaded artifacts\n        run: |\n          echo \"📁 Artifact structure:\"\n          find artifacts -type f -name \"*.app\" -o -name \"*.dmg\" | head -20\n          echo \"\"\n          echo \"📁 Full directory structure:\"\n          ls -la artifacts/\n          ls -la artifacts/macos-aarch64/ || echo \"macos-aarch64 directory not found\"\n          ls -la artifacts/macos-x86_64/ || echo \"macos-x86_64 directory not found\"\n      \n      - name: Import Apple certificates\n        env:\n          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}\n          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}\n          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}\n        run: |\n          # Create variables\n          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12\n          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db\n          \n          # Import certificate from secrets\n          echo -n \"$APPLE_CERTIFICATE\" | base64 --decode -o $CERTIFICATE_PATH\n          \n          # Create temporary keychain\n          security create-keychain -p \"$KEYCHAIN_PASSWORD\" $KEYCHAIN_PATH\n          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH\n          security unlock-keychain -p \"$KEYCHAIN_PASSWORD\" $KEYCHAIN_PATH\n          \n          # Import certificate to keychain\n          security import $CERTIFICATE_PATH -P \"$APPLE_CERTIFICATE_PASSWORD\" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH\n          security set-key-partition-list -S apple-tool:,apple: -k \"$KEYCHAIN_PASSWORD\" $KEYCHAIN_PATH\n          security list-keychain -d user -s $KEYCHAIN_PATH\n      \n      - name: Create universal app\n        run: |\n          # Create temp directory\n          mkdir -p dmg_temp\n          \n          # Extract zip files if they exist\n          if [ -f \"artifacts/macos-aarch64.zip\" ]; then\n            echo \"📦 Extracting macos-aarch64.zip...\"\n            unzip -q artifacts/macos-aarch64.zip -d artifacts/macos-aarch64/\n          fi\n          \n          if [ -f \"artifacts/macos-x86_64.zip\" ]; then\n            echo \"📦 Extracting macos-x86_64.zip...\"\n            unzip -q artifacts/macos-x86_64.zip -d artifacts/macos-x86_64/\n          fi\n          \n          # Find the actual app paths\n          AARCH64_APP=$(find artifacts/macos-aarch64 -name \"opcode.app\" -type d | head -1)\n          X86_64_APP=$(find artifacts/macos-x86_64 -name \"opcode.app\" -type d | head -1)\n          \n          if [ -z \"$AARCH64_APP\" ] || [ -z \"$X86_64_APP\" ]; then\n            echo \"❌ Could not find app bundles\"\n            echo \"AARCH64_APP: $AARCH64_APP\"\n            echo \"X86_64_APP: $X86_64_APP\"\n            exit 1\n          fi\n          \n          echo \"✅ Found app bundles:\"\n          echo \"  ARM64: $AARCH64_APP\"\n          echo \"  x86_64: $X86_64_APP\"\n          \n          # Copy ARM64 app as base\n          cp -R \"$AARCH64_APP\" dmg_temp/\n          \n          # Create universal binary using lipo\n          lipo -create -output dmg_temp/opcode.app/Contents/MacOS/opcode \\\n            \"$AARCH64_APP/Contents/MacOS/opcode\" \\\n            \"$X86_64_APP/Contents/MacOS/opcode\"\n          \n          # Ensure executable permissions are set\n          chmod +x dmg_temp/opcode.app/Contents/MacOS/opcode\n          \n          echo \"✅ Universal binary created\"\n          lipo -info dmg_temp/opcode.app/Contents/MacOS/opcode\n      \n      - name: Sign app bundle\n        env:\n          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}\n        run: |\n          codesign --sign \"$APPLE_SIGNING_IDENTITY\" \\\n            --timestamp \\\n            --options runtime \\\n            --force \\\n            --deep \\\n            --entitlements src-tauri/entitlements.plist \\\n            dmg_temp/opcode.app\n      \n      - name: Create DMG\n        run: |\n          hdiutil create -volname \"opcode Installer\" \\\n            -srcfolder dmg_temp \\\n            -ov -format UDZO opcode.dmg\n      \n      - name: Sign DMG\n        env:\n          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}\n        run: |\n          codesign --sign \"$APPLE_SIGNING_IDENTITY\" \\\n            --timestamp \\\n            --force opcode.dmg\n      \n      - name: Notarize DMG\n        env:\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}\n        run: |\n          # Store notarization credentials\n          xcrun notarytool store-credentials \"notarytool-profile\" \\\n            --apple-id \"$APPLE_ID\" \\\n            --team-id \"$APPLE_TEAM_ID\" \\\n            --password \"$APPLE_PASSWORD\"\n          \n          # Submit for notarization\n          xcrun notarytool submit opcode.dmg \\\n            --keychain-profile \"notarytool-profile\" \\\n            --wait\n      \n      - name: Staple notarization\n        run: xcrun stapler staple opcode.dmg\n      \n      - name: Verify DMG\n        run: |\n          spctl -a -t open -vvv --context context:primary-signature opcode.dmg\n          echo \"✅ DMG verification complete\"\n      \n      - name: Create artifacts directory\n        run: |\n          mkdir -p dist/macos-universal\n          cp opcode.dmg dist/macos-universal/\n          \n          # Also save the app bundle using ditto to preserve permissions and signatures\n          ditto -c -k --sequesterRsrc --keepParent \\\n            dmg_temp/opcode.app dist/macos-universal/opcode.app.zip\n          \n          # Generate checksum\n          shasum -a 256 dist/macos-universal/* > dist/macos-universal/checksums.txt\n      \n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: macos-universal\n          path: dist/macos-universal/*\n      \n      - name: Cleanup\n        if: always()\n        run: |\n          echo \"🧹 Cleaning up temporary directories...\"\n          rm -rf dmg_temp temp_x86 artifacts\n          \n          # Clean up keychain\n          if [ -n \"$RUNNER_TEMP\" ] && [ -f \"$RUNNER_TEMP/app-signing.keychain-db\" ]; then\n            security delete-keychain \"$RUNNER_TEMP/app-signing.keychain-db\" || true\n          fi\n          \n          echo \"✅ Cleanup complete\"\n"
  },
  {
    "path": ".github/workflows/build-test.yml",
    "content": "name: Build Test\n\n# Trigger on every push and pull request\non:\n  push:\n    branches: [ main, develop, 'release/**', 'feature/**' ]\n  pull_request:\n    branches: [ main, develop ]\n    types: [opened, synchronize, reopened]\n\n# Cancel in-progress workflows when a new commit is pushed\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\nenv:\n  CARGO_TERM_COLOR: always\n  RUST_BACKTRACE: 1\n\njobs:\n  build-test:\n    name: Build Test (${{ matrix.platform.name }})\n    \n    strategy:\n      fail-fast: false\n      matrix:\n        platform:\n          - name: Linux\n            os: ubuntu-latest\n            rust-target: x86_64-unknown-linux-gnu\n          - name: Linux ARM64\n            os: ubuntu-24.04-arm64\n            rust-target: aarch64-unknown-linux-gnu\n          - name: Windows\n            os: windows-latest\n            rust-target: x86_64-pc-windows-msvc\n          - name: macOS\n            os: macos-latest\n            rust-target: x86_64-apple-darwin\n    \n    runs-on: ${{ matrix.platform.os }}\n    \n    steps:\n      # Checkout the repository\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      # Install system dependencies for Linux\n      - name: Install Linux dependencies\n        if: matrix.platform.os == 'ubuntu-latest'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y \\\n            libwebkit2gtk-4.1-dev \\\n            libgtk-3-dev \\\n            libayatana-appindicator3-dev \\\n            librsvg2-dev \\\n            libssl-dev \\\n            libglib2.0-dev \\\n            libjavascriptcoregtk-4.1-dev \\\n            libsoup-3.0-dev \\\n            libxdo-dev \\\n            libxcb-shape0-dev \\\n            libxcb-xfixes0-dev\n\n      # Setup Rust with caching\n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ matrix.platform.rust-target }}\n\n      # Cache Rust dependencies\n      - name: Cache Rust dependencies\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: './src-tauri -> target'\n          key: ${{ matrix.platform.os }}-rust-${{ hashFiles('**/Cargo.lock') }}\n          \n      # Setup Bun\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      # Cache Bun dependencies\n      - name: Cache Bun dependencies\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.bun\n            node_modules\n          key: ${{ matrix.platform.os }}-bun-${{ hashFiles('bun.lockb', 'package.json') }}\n          restore-keys: |\n            ${{ matrix.platform.os }}-bun-\n      \n      # Install frontend dependencies\n      - name: Install frontend dependencies\n        run: bun install --frozen-lockfile\n\n      # Build frontend\n      - name: Build frontend\n        run: bun run build\n\n      # Build Tauri application\n      - name: Build Tauri application\n        run: bun run tauri build --no-bundle -d\n        env:\n          TAURI_SIGNING_PRIVATE_KEY: \"\"\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: \"\"\n\n      # Upload build artifacts for debugging (optional)\n      - name: Upload build logs on failure\n        if: failure()\n        uses: actions/upload-artifact@v4\n        with:\n          name: build-logs-${{ matrix.platform.name }}\n          path: |\n            src-tauri/target/release/build/*/output\n            src-tauri/target/debug/build/*/output\n          retention-days: 3\n\n  # Summary job to ensure all builds pass\n  build-test-summary:\n    name: Build Test Summary\n    runs-on: ubuntu-latest\n    needs: [build-test]\n    if: always()\n    \n    steps:\n      - name: Check build results\n        run: |\n          if [[ \"${{ needs.build-test.result }}\" == \"failure\" ]]; then\n            echo \"❌ One or more build tests failed\"\n            exit 1\n          elif [[ \"${{ needs.build-test.result }}\" == \"cancelled\" ]]; then\n            echo \"⚠️ Build tests were cancelled\"\n            exit 1\n          else\n            echo \"✅ All build tests passed successfully\"\n          fi\n\n      - name: Create status comment (PR only)\n        if: github.event_name == 'pull_request'\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const result = '${{ needs.build-test.result }}';\n            const emoji = result === 'success' ? '✅' : '❌';\n            const status = result === 'success' ? 'All build tests passed!' : 'Build tests failed';\n            \n            // Create a comment summarizing the build status\n            const comment = `## ${emoji} Build Test Results\n            \n            **Status**: ${status}\n            **Commit**: ${{ github.event.pull_request.head.sha || github.sha }}\n            \n            | Platform | Status |\n            |----------|--------|\n            | Linux    | ${{ contains(needs.build-test.result, 'success') && '✅' || '❌' }} |\n            | Windows  | ${{ contains(needs.build-test.result, 'success') && '✅' || '❌' }} |\n            | macOS    | ${{ contains(needs.build-test.result, 'success') && '✅' || '❌' }} |\n            \n            [View full workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`;\n            \n            // Only post comment if it's a PR\n            if (context.eventName === 'pull_request') {\n              await github.rest.issues.createComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n                body: comment\n              });\n            } \n"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "content": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize]\n    # Optional: Only run on specific file changes\n    # paths:\n    #   - \"src/**/*.ts\"\n    #   - \"src/**/*.tsx\"\n    #   - \"src/**/*.js\"\n    #   - \"src/**/*.jsx\"\n\njobs:\n  claude-review:\n    # Optional: Filter by PR author\n    # if: |\n    #   github.event.pull_request.user.login == 'external-contributor' ||\n    #   github.event.pull_request.user.login == 'new-developer' ||\n    #   github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'\n\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code Review\n        id: claude-review\n        uses: anthropics/claude-code-action@v1\n        with:\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}\n          prompt: |\n            REPO: ${{ github.repository }}\n            PR NUMBER: ${{ github.event.pull_request.number }}\n\n            Please review this pull request and provide feedback on:\n            - Code quality and best practices\n            - Potential bugs or issues\n            - Performance considerations\n            - Security concerns\n            - Test coverage\n\n            Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.\n\n            Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.\n\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options\n          claude_args: '--allowed-tools \"Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)\"'\n\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}\n\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n\n          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.\n          # prompt: 'Update the pull request description to include a summary of changes.'\n\n          # Optional: Add claude_args to customize behavior and configuration\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options\n          # claude_args: '--allowed-tools Bash(gh pr:*)'\n\n"
  },
  {
    "path": ".github/workflows/pr-check.yml",
    "content": "name: PR Checks (bun run check)\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n  contents: read\n  pull-requests: read\n\nconcurrency:\n  group: pr-check-${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}\n  cancel-in-progress: true\n\njobs:\n  check:\n    name: bun run check\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Cache Bun dependencies\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.bun/install/cache\n            node_modules\n          key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-bun-\n\n      - name: Install JS/TS dependencies\n        run: bun install --frozen-lockfile\n\n      - name: Set up Rust (stable)\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Cache Rust dependencies\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: |\n            src-tauri -> src-tauri/target\n          cache-on-failure: true\n\n      - name: Run checks\n        run: bun run check\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to release (e.g., v1.0.0)'\n        required: true\n        type: string\n\npermissions:\n  contents: write\n\njobs:\n  # Build jobs for each platform\n  build-linux:\n    uses: ./.github/workflows/build-linux.yml\n    secrets: inherit\n  \n  build-macos:\n    uses: ./.github/workflows/build-macos.yml\n    secrets: inherit\n  \n\n  # Create release after all builds complete\n  create-release:\n    name: Create Release\n    needs: [build-linux, build-macos]\n    runs-on: ubuntu-latest\n    \n    steps:\n      - uses: actions/checkout@v4\n      \n      - name: Determine version\n        id: version\n        run: |\n          if [ \"${{ github.event_name }}\" = \"push\" ]; then\n            VERSION=\"${GITHUB_REF#refs/tags/}\"\n          else\n            VERSION=\"${{ inputs.version }}\"\n          fi\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"Version: $VERSION\"\n      \n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n      \n      - name: Prepare release assets\n        run: |\n          mkdir -p release-assets\n          \n          # Linux artifacts\n          if [ -d \"artifacts/linux-x86_64\" ]; then\n            cp artifacts/linux-x86_64/*.deb release-assets/opcode_${{ steps.version.outputs.version }}_linux_x86_64.deb || true\n            cp artifacts/linux-x86_64/*.AppImage release-assets/opcode_${{ steps.version.outputs.version }}_linux_x86_64.AppImage || true\n          fi\n          \n          # macOS artifacts\n          if [ -d \"artifacts/macos-universal\" ]; then\n            cp artifacts/macos-universal/opcode.dmg release-assets/opcode_${{ steps.version.outputs.version }}_macos_universal.dmg || true\n            cp artifacts/macos-universal/opcode.app.zip release-assets/opcode_${{ steps.version.outputs.version }}_macos_universal.app.tar.gz || true\n          fi\n          \n          # Create source code archives\n          # Clean version without 'v' prefix for archive names\n          CLEAN_VERSION=\"${{ steps.version.outputs.version }}\"\n          CLEAN_VERSION=\"${CLEAN_VERSION#v}\"\n          \n          # Create source code archives (excluding .git and other unnecessary files)\n          echo \"Creating source code archives...\"\n          \n          # Create a clean export of the repository\n          git archive --format=tar.gz --prefix=opcode-${CLEAN_VERSION}/ -o release-assets/opcode-${CLEAN_VERSION}.tar.gz HEAD\n          git archive --format=zip --prefix=opcode-${CLEAN_VERSION}/ -o release-assets/opcode-${CLEAN_VERSION}.zip HEAD\n\n          # Generate signatures for all files\n          cd release-assets\n          for file in *; do\n            if [ -f \"$file\" ]; then\n              sha256sum \"$file\" > \"$file.sha256\"\n            fi\n          done\n          cd ..\n      \n      - name: Create Release\n        uses: softprops/action-gh-release@v1\n        with:\n          tag_name: ${{ steps.version.outputs.version }}\n          name: opcode ${{ steps.version.outputs.version }}\n          draft: true\n          prerelease: false\n          generate_release_notes: true\n          files: release-assets/*\n          body: |\n            <div align=\"center\">\n              <img src=\"https://raw.githubusercontent.com/${{ github.repository }}/${{ steps.version.outputs.version }}/src-tauri/icons/icon.png\" alt=\"opcode Logo\" width=\"128\" height=\"128\">\n            </div>\n\n            ## opcode ${{ steps.version.outputs.version }}\n\n            This release was built and signed by CI. Artifacts for macOS and Linux are attached below.\n\n            - Auto-generated release notes are included below (commits, PRs, and contributors).\n            - Checksums (`.sha256`) are provided for all assets.\n\n            ### Downloads\n\n            - macOS: `.dmg`, `.app.tar.gz` (Universal: Apple Silicon + Intel)\n            - Linux: `.AppImage`, `.deb`\n\n            ### Installation\n\n            - macOS: Open the `.dmg` and drag opcode to Applications.\n            - Linux: `chmod +x` the `.AppImage` and run it, or install the `.deb` on Debian/Ubuntu.\n            \n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n*.bun-build\n\n# Tauri binaries (built executables)\nsrc-tauri/binaries/\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\ntemp_lib/\n\n.cursor/\nAGENTS.md\nCLAUDE.md\n*_TASK.md\n\n# Claude project-specific files\n.claude/\n\n.env"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Welcome Contributors\n\nWe welcome contributions to enhance opcode's capabilities and improve its performance. To report bugs, create a [GitHub issue](https://github.com/getAsterisk/opcode/issues).\n\n> Before contributing, read through the existing issues and pull requests to see if someone else is already working on something similar. That way you can avoid duplicating efforts.\n\nTo contribute, please follow these steps:\n\n1. Fork the opcode repository on GitHub.\n2. Create a new branch for your feature or bug fix.\n3. Make your changes and ensure that the code passes all tests.\n4. Submit a pull request describing your changes and their benefits.\n\n## Pull Request Guidelines\n\nWhen submitting a pull request, please follow these guidelines:\n\n1. **Title**: Please include following prefixes:\n   - `Feature:` for new features\n   - `Fix:` for bug fixes\n   - `Docs:` for documentation changes\n   - `Refactor:` for code refactoring\n   - `Improve:` for performance improvements\n   - `Other:` for other changes\n\n   For example:\n   - `Feature: added custom agent timeout configuration`\n   - `Fix: resolved session list scrolling issue`\n\n2. **Description**: Provide a clear and detailed description of your changes in the pull request. Explain the problem you are solving, the approach you took, and any potential side effects or limitations of your changes.\n\n3. **Documentation**: Update the relevant documentation to reflect your changes. This includes the README file, code comments, and any other relevant documentation.\n\n4. **Dependencies**: If your changes require new dependencies, ensure that they are properly documented and added to the `package.json` or `Cargo.toml` files.\n\n5. If the pull request does not meet the above guidelines, it may be closed without merging.\n\n**Note**: Please ensure that you have the latest version of the code before creating a pull request. If you have an existing fork, just sync your fork with the latest version of the opcode repository.\n\n## Coding Standards\n\n### Frontend (React/TypeScript)\n- Use TypeScript for all new code\n- Follow functional components with hooks\n- Use Tailwind CSS for styling\n- Add JSDoc comments for exported functions and components\n\n### Backend (Rust)\n- Follow Rust standard conventions\n- Use `cargo fmt` for formatting\n- Use `cargo clippy` for linting\n- Handle all `Result` types explicitly\n- Add comprehensive documentation with `///` comments\n\n### Security Requirements\n- Validate all inputs from the frontend\n- Use prepared statements for database operations\n- Never log sensitive data (tokens, passwords, etc.)\n- Use secure defaults for all configurations\n\n## Testing\n- Add tests for new functionality\n- Ensure all existing tests pass\n- Run `cargo test` for Rust code\n- Test the application manually before submitting\n\nPlease adhere to the coding conventions, maintain clear documentation, and provide thorough testing for your contributions. \n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "\n<div align=\"center\">\n  <img src=\"src-tauri/icons/icon.png\" alt=\"opcode Logo\" width=\"120\" height=\"120\">\n\n  <h1>opcode</h1>\n  \n  <p>\n    <strong>A powerful GUI app and Toolkit for Claude Code</strong>\n  </p>\n  <p>\n    <strong>Create custom agents, manage interactive Claude Code sessions, run secure background agents, and more.</strong>\n  </p>\n  \n  <p>\n    <a href=\"#features\"><img src=\"https://img.shields.io/badge/Features-✨-blue?style=for-the-badge\" alt=\"Features\"></a>\n    <a href=\"#installation\"><img src=\"https://img.shields.io/badge/Install-🚀-green?style=for-the-badge\" alt=\"Installation\"></a>\n    <a href=\"#usage\"><img src=\"https://img.shields.io/badge/Usage-📖-purple?style=for-the-badge\" alt=\"Usage\"></a>\n    <a href=\"#development\"><img src=\"https://img.shields.io/badge/Develop-🛠️-orange?style=for-the-badge\" alt=\"Development\"></a>\n    <a href=\"https://discord.com/invite/KYwhHVzUsY\"><img src=\"https://img.shields.io/badge/Discord-Join-5865F2?style=for-the-badge&logo=discord&logoColor=white\" alt=\"Discord\"></a>\n  </p>\n</div>\n\n![457013521-6133a738-d0cb-4d3e-8746-c6768c82672c](https://github.com/user-attachments/assets/a028de9e-d881-44d8-bae5-7326ab3558b9)\n\n\n\nhttps://github.com/user-attachments/assets/6bceea0f-60b6-4c3e-a745-b891de00b8d0\n\n\n\n> [!TIP]\n> **⭐ Star the repo and follow [@getAsterisk](https://x.com/getAsterisk) on X for early access to `asteria-swe-v0`**.\n\n> [!NOTE]\n> This project is not affiliated with, endorsed by, or sponsored by Anthropic. Claude is a trademark of Anthropic, PBC. This is an independent developer project using Claude.\n\n## 🌟 Overview\n\n**opcode** is a powerful desktop application that transforms how you interact with Claude Code. Built with Tauri 2, it provides a beautiful GUI for managing your Claude Code sessions, creating custom agents, tracking usage, and much more.\n\nThink of opcode as your command center for Claude Code - bridging the gap between the command-line tool and a visual experience that makes AI-assisted development more intuitive and productive.\n\n## 📋 Table of Contents\n\n- [🌟 Overview](#-overview)\n- [✨ Features](#-features)\n  - [🗂️ Project & Session Management](#️-project--session-management)\n  - [🤖 CC Agents](#-cc-agents)\n  \n  - [📊 Usage Analytics Dashboard](#-usage-analytics-dashboard)\n  - [🔌 MCP Server Management](#-mcp-server-management)\n  - [⏰ Timeline & Checkpoints](#-timeline--checkpoints)\n  - [📝 CLAUDE.md Management](#-claudemd-management)\n- [📖 Usage](#-usage)\n  - [Getting Started](#getting-started)\n  - [Managing Projects](#managing-projects)\n  - [Creating Agents](#creating-agents)\n  - [Tracking Usage](#tracking-usage)\n  - [Working with MCP Servers](#working-with-mcp-servers)\n- [🚀 Installation](#-installation)\n- [🔨 Build from Source](#-build-from-source)\n- [🛠️ Development](#️-development)\n- [🔒 Security](#-security)\n- [🤝 Contributing](#-contributing)\n- [📄 License](#-license)\n- [🙏 Acknowledgments](#-acknowledgments)\n\n## ✨ Features\n\n### 🗂️ **Project & Session Management**\n- **Visual Project Browser**: Navigate through all your Claude Code projects in `~/.claude/projects/`\n- **Session History**: View and resume past coding sessions with full context\n- **Smart Search**: Find projects and sessions quickly with built-in search\n- **Session Insights**: See first messages, timestamps, and session metadata at a glance\n\n### 🤖 **CC Agents**\n- **Custom AI Agents**: Create specialized agents with custom system prompts and behaviors\n- **Agent Library**: Build a collection of purpose-built agents for different tasks\n- **Background Execution**: Run agents in separate processes for non-blocking operations\n- **Execution History**: Track all agent runs with detailed logs and performance metrics\n\n\n\n### 📊 **Usage Analytics Dashboard**\n- **Cost Tracking**: Monitor your Claude API usage and costs in real-time\n- **Token Analytics**: Detailed breakdown by model, project, and time period\n- **Visual Charts**: Beautiful charts showing usage trends and patterns\n- **Export Data**: Export usage data for accounting and analysis\n\n### 🔌 **MCP Server Management**\n- **Server Registry**: Manage Model Context Protocol servers from a central UI\n- **Easy Configuration**: Add servers via UI or import from existing configs\n- **Connection Testing**: Verify server connectivity before use\n- **Claude Desktop Import**: Import server configurations from Claude Desktop\n\n### ⏰ **Timeline & Checkpoints**\n- **Session Versioning**: Create checkpoints at any point in your coding session\n- **Visual Timeline**: Navigate through your session history with a branching timeline\n- **Instant Restore**: Jump back to any checkpoint with one click\n- **Fork Sessions**: Create new branches from existing checkpoints\n- **Diff Viewer**: See exactly what changed between checkpoints\n\n### 📝 **CLAUDE.md Management**\n- **Built-in Editor**: Edit CLAUDE.md files directly within the app\n- **Live Preview**: See your markdown rendered in real-time\n- **Project Scanner**: Find all CLAUDE.md files in your projects\n- **Syntax Highlighting**: Full markdown support with syntax highlighting\n\n## 📖 Usage\n\n### Getting Started\n\n1. **Launch opcode**: Open the application after installation\n2. **Welcome Screen**: Choose between CC Agents or Projects\n3. **First Time Setup**: opcode will automatically detect your `~/.claude` directory\n\n### Managing Projects\n\n```\nProjects → Select Project → View Sessions → Resume or Start New\n```\n\n- Click on any project to view its sessions\n- Each session shows the first message and timestamp\n- Resume sessions directly or start new ones\n\n### Creating Agents\n\n```\nCC Agents → Create Agent → Configure → Execute\n```\n\n1. **Design Your Agent**: Set name, icon, and system prompt\n2. **Configure Model**: Choose between available Claude models\n3. **Set Permissions**: Configure file read/write and network access\n4. **Execute Tasks**: Run your agent on any project\n\n### Tracking Usage\n\n```\nMenu → Usage Dashboard → View Analytics\n```\n\n- Monitor costs by model, project, and date\n- Export data for reports\n- Set up usage alerts (coming soon)\n\n### Working with MCP Servers\n\n```\nMenu → MCP Manager → Add Server → Configure\n```\n\n- Add servers manually or via JSON\n- Import from Claude Desktop configuration\n- Test connections before using\n\n## 🚀 Installation\n\n### Prerequisites\n\n- **Claude Code CLI**: Install from [Claude's official site](https://claude.ai/code)\n\n### Release Executables Will Be Published Soon\n\n## 🔨 Build from Source\n\n### Prerequisites\n\nBefore building opcode from source, ensure you have the following installed:\n\n#### System Requirements\n\n- **Operating System**: Windows 10/11, macOS 11+, or Linux (Ubuntu 20.04+)\n- **RAM**: Minimum 4GB (8GB recommended)\n- **Storage**: At least 1GB free space\n\n#### Required Tools\n\n1. **Rust** (1.70.0 or later)\n   ```bash\n   # Install via rustup\n   curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n   ```\n\n2. **Bun** (latest version)\n   ```bash\n   # Install bun\n   curl -fsSL https://bun.sh/install | bash\n   ```\n\n3. **Git**\n   ```bash\n   # Usually pre-installed, but if not:\n   # Ubuntu/Debian: sudo apt install git\n   # macOS: brew install git\n   # Windows: Download from https://git-scm.com\n   ```\n\n4. **Claude Code CLI**\n   - Download and install from [Claude's official site](https://claude.ai/code)\n   - Ensure `claude` is available in your PATH\n\n#### Platform-Specific Dependencies\n\n**Linux (Ubuntu/Debian)**\n```bash\n# Install system dependencies\nsudo apt update\nsudo apt install -y \\\n  libwebkit2gtk-4.1-dev \\\n  libgtk-3-dev \\\n  libayatana-appindicator3-dev \\\n  librsvg2-dev \\\n  patchelf \\\n  build-essential \\\n  curl \\\n  wget \\\n  file \\\n  libssl-dev \\\n  libxdo-dev \\\n  libsoup-3.0-dev \\\n  libjavascriptcoregtk-4.1-dev\n```\n\n**macOS**\n```bash\n# Install Xcode Command Line Tools\nxcode-select --install\n\n# Install additional dependencies via Homebrew (optional)\nbrew install pkg-config\n```\n\n**Windows**\n- Install [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)\n- Install [WebView2](https://developer.microsoft.com/microsoft-edge/webview2/) (usually pre-installed on Windows 11)\n\n### Build Steps\n\n1. **Clone the Repository**\n   ```bash\n   git clone https://github.com/getAsterisk/opcode.git\n   cd opcode\n   ```\n\n2. **Install Frontend Dependencies**\n   ```bash\n   bun install\n   ```\n\n3. **Build the Application**\n   \n   **For Development (with hot reload)**\n   ```bash\n   bun run tauri dev\n   ```\n   \n   **For Production Build**\n   ```bash\n   # Build the application\n   bun run tauri build\n   \n   # The built executable will be in:\n   # - Linux: src-tauri/target/release/\n   # - macOS: src-tauri/target/release/\n   # - Windows: src-tauri/target/release/\n   ```\n\n4. **Platform-Specific Build Options**\n   \n   **Debug Build (faster compilation, larger binary)**\n   ```bash\n   bun run tauri build --debug\n   ```\n   \n   **Universal Binary for macOS (Intel + Apple Silicon)**\n   ```bash\n   bun run tauri build --target universal-apple-darwin\n   ```\n\n### Troubleshooting\n\n#### Common Issues\n\n1. **\"cargo not found\" error**\n   - Ensure Rust is installed and `~/.cargo/bin` is in your PATH\n   - Run `source ~/.cargo/env` or restart your terminal\n\n2. **Linux: \"webkit2gtk not found\" error**\n   - Install the webkit2gtk development packages listed above\n   - On newer Ubuntu versions, you might need `libwebkit2gtk-4.0-dev`\n\n3. **Windows: \"MSVC not found\" error**\n   - Install Visual Studio Build Tools with C++ support\n   - Restart your terminal after installation\n\n4. **\"claude command not found\" error**\n   - Ensure Claude Code CLI is installed and in your PATH\n   - Test with `claude --version`\n\n5. **Build fails with \"out of memory\"**\n   - Try building with fewer parallel jobs: `cargo build -j 2`\n   - Close other applications to free up RAM\n\n#### Verify Your Build\n\nAfter building, you can verify the application works:\n\n```bash\n# Run the built executable directly\n# Linux/macOS\n./src-tauri/target/release/opcode\n\n# Windows\n./src-tauri/target/release/opcode.exe\n```\n\n### Build Artifacts\n\nThe build process creates several artifacts:\n\n- **Executable**: The main opcode application\n- **Installers** (when using `tauri build`):\n  - `.deb` package (Linux)\n  - `.AppImage` (Linux)\n  - `.dmg` installer (macOS)\n  - `.msi` installer (Windows)\n  - `.exe` installer (Windows)\n\nAll artifacts are located in `src-tauri/target/release/`.\n\n## 🛠️ Development\n\n### Tech Stack\n\n- **Frontend**: React 18 + TypeScript + Vite 6\n- **Backend**: Rust with Tauri 2\n- **UI Framework**: Tailwind CSS v4 + shadcn/ui\n- **Database**: SQLite (via rusqlite)\n- **Package Manager**: Bun\n\n### Project Structure\n\n```\nopcode/\n├── src/                   # React frontend\n│   ├── components/        # UI components\n│   ├── lib/               # API client & utilities\n│   └── assets/            # Static assets\n├── src-tauri/             # Rust backend\n│   ├── src/\n│   │   ├── commands/      # Tauri command handlers\n│   │   ├── checkpoint/    # Timeline management\n│   │   └── process/       # Process management\n│   └── tests/             # Rust test suite\n└── public/                # Public assets\n```\n\n### Development Commands\n\n```bash\n# Start development server\nbun run tauri dev\n\n# Run frontend only\nbun run dev\n\n# Type checking\nbunx tsc --noEmit\n\n# Run Rust tests\ncd src-tauri && cargo test\n\n# Format code\ncd src-tauri && cargo fmt\n```\n\n## 🔒 Security\n\nopcode prioritizes your privacy and security:\n\n1. **Process Isolation**: Agents run in separate processes\n2. **Permission Control**: Configure file and network access per agent\n3. **Local Storage**: All data stays on your machine\n4. **No Telemetry**: No data collection or tracking\n5. **Open Source**: Full transparency through open source code\n\n## 🤝 Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n\n### Areas for Contribution\n\n- 🐛 Bug fixes and improvements\n- ✨ New features and enhancements\n- 📚 Documentation improvements\n- 🎨 UI/UX enhancements\n- 🧪 Test coverage\n- 🌐 Internationalization\n\n## 📄 License\n\nThis project is licensed under the AGPL License - see the [LICENSE](LICENSE) file for details.\n\n## 🙏 Acknowledgments\n\n- Built with [Tauri](https://tauri.app/) - The secure framework for building desktop apps\n- [Claude](https://claude.ai) by Anthropic\n\n---\n\n<div align=\"center\">\n  <p>\n    <strong>Made with ❤️ by the <a href=\"https://asterisk.so/\">Asterisk</a></strong>\n  </p>\n  <p>\n    <a href=\"https://github.com/getAsterisk/opcode/issues\">Report Bug</a>\n    ·\n    <a href=\"https://github.com/getAsterisk/opcode/issues\">Request Feature</a>\n  </p>\n</div>\n\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=getAsterisk/opcode&type=Date)](https://www.star-history.com/#getAsterisk/opcode&Date)\n"
  },
  {
    "path": "cc_agents/README.md",
    "content": "# 🤖 opcode CC Agents\n\n<div align=\"center\">\n  <p>\n    <strong>Pre-built AI agents for opcode powered by Claude Code</strong>\n  </p>\n  <p>\n    <a href=\"#available-agents\">Browse Agents</a> •\n    <a href=\"#importing-agents\">Import Guide</a> •\n    <a href=\"#exporting-agents\">Export Guide</a> •\n    <a href=\"#contributing\">Contribute</a>\n  </p>\n</div>\n\n---\n\n## 📦 Available Agents\n\n| Agent | Model | Description | Default Task |\n|-------|-------|-------------|--------------|\n| **🎯 Git Commit Bot**<br/>🤖 `bot` | <img src=\"https://img.shields.io/badge/Sonnet-blue?style=flat-square\" alt=\"Sonnet\"> | **Automate your Git workflow with intelligent commit messages**<br/><br/>Analyzes Git repository changes, generates detailed commit messages following Conventional Commits specification, and pushes changes to remote repository. | \"Push all changes.\" |\n| **🛡️ Security Scanner**<br/>🛡️ `shield` | <img src=\"https://img.shields.io/badge/Opus-purple?style=flat-square\" alt=\"Opus\"> | **Advanced AI-powered Static Application Security Testing (SAST)**<br/><br/>Performs comprehensive security audits by spawning specialized sub-agents for: codebase intelligence gathering, threat modeling (STRIDE), vulnerability scanning (OWASP Top 10, CWE), exploit validation, remediation design, and professional report generation. | \"Review the codebase for security issues.\" |\n| **🧪 Unit Tests Bot**<br/>💻 `code` | <img src=\"https://img.shields.io/badge/Opus-purple?style=flat-square\" alt=\"Opus\"> | **Automated comprehensive unit test generation for any codebase**<br/><br/>Analyzes codebase and generates comprehensive unit tests by: analyzing code structure, creating test plans, writing tests matching your style, verifying execution, optimizing coverage (>80% overall, 100% critical paths), and generating documentation. | \"Generate unit tests for this codebase.\" |\n\n### Available Icons\n\nChoose from these icon options when creating agents:\n- `bot` - 🤖 General purpose\n- `shield` - 🛡️ Security related\n- `code` - 💻 Development\n- `terminal` - 🖥️ System/CLI\n- `database` - 🗄️ Data operations\n- `globe` - 🌐 Network/Web\n- `file-text` - 📄 Documentation\n- `git-branch` - 🌿 Version control\n\n---\n\n## 📥 Importing Agents\n\n### Method 1: Import from GitHub (Recommended)\n\n1. In opcode, navigate to **CC Agents**\n2. Click the **Import** dropdown button\n3. Select **From GitHub**\n4. Browse available agents from the official repository\n5. Preview agent details and click **Import Agent**\n\n### Method 2: Import from Local File\n\n1. Download a `.opcode.json` file from this repository\n2. In opcode, navigate to **CC Agents**\n3. Click the **Import** dropdown button\n4. Select **From File**\n5. Choose the downloaded `.opcode.json` file\n\n## 📤 Exporting Agents\n\n### Export Your Custom Agents\n\n1. In opcode, navigate to **CC Agents**\n2. Find your agent in the grid\n3. Click the **Export** button\n4. Choose where to save the `.opcode.json` file\n\n### Agent File Format\n\nAll agents are stored in `.opcode.json` format with the following structure:\n\n```json\n{\n  \"version\": 1,\n  \"exported_at\": \"2025-01-23T14:29:58.156063+00:00\",\n  \"agent\": {\n    \"name\": \"Your Agent Name\",\n    \"icon\": \"bot\",\n    \"model\": \"opus|sonnet|haiku\",\n    \"system_prompt\": \"Your agent's instructions...\",\n    \"default_task\": \"Default task description\"\n  }\n}\n```\n\n## 🔧 Technical Implementation\n\n### How Import/Export Works\n\nThe agent import/export system is built on a robust architecture:\n\n#### Backend (Rust/Tauri)\n- **Storage**: SQLite database stores agent configurations\n- **Export**: Serializes agent data to JSON with version control\n- **Import**: Validates and deduplicates agents on import\n- **GitHub Integration**: Fetches agents via GitHub API\n\n#### Frontend (React/TypeScript)\n- **UI Components**: \n  - `CCAgents.tsx` - Main agent management interface\n  - `GitHubAgentBrowser.tsx` - GitHub repository browser\n  - `CreateAgent.tsx` - Agent creation/editing form\n- **File Operations**: Native file dialogs for import/export\n- **Real-time Updates**: Live agent status and execution monitoring\n\n### Key Features\n\n1. **Version Control**: Each agent export includes version metadata\n2. **Duplicate Prevention**: Automatic naming conflict resolution\n3. **Model Selection**: Choose between Opus, Sonnet, and Haiku models\n4. **GitHub Integration**: Direct import from the official repository\n\n## 🤝 Contributing\n\nWe welcome agent contributions! Here's how to add your agent:\n\n### 1. Create Your Agent\nDesign and test your agent in opcode with a clear, focused purpose.\n\n### 2. Export Your Agent\nExport your agent to a `.opcode.json` file with a descriptive name.\n\n### 3. Submit a Pull Request\n1. Fork this repository\n2. Add your `.opcode.json` file to the `cc_agents` directory\n3. Update this README with your agent's details\n4. Submit a PR with a description of what your agent does\n\n### Agent Guidelines\n\n- **Single Purpose**: Each agent should excel at one specific task\n- **Clear Documentation**: Write comprehensive system prompts\n- **Model Choice**: Use Haiku for simple tasks, Sonnet for general purpose, Opus for complex reasoning\n- **Naming**: Use descriptive names that clearly indicate the agent's function\n\n## 📜 License\n\nThese agents are provided under the same license as the opcode project. See the main LICENSE file for details.\n\n---\n\n<div align=\"center\">\n  <strong>Built with ❤️ by the opcode community</strong>\n</div> \n"
  },
  {
    "path": "cc_agents/git-commit-bot.opcode.json",
    "content": "{\n  \"agent\": {\n    \"default_task\": \"Push all changes.\",\n    \"icon\": \"bot\",\n    \"model\": \"sonnet\",\n    \"name\": \"Git Commit Bot\",\n    \"system_prompt\": \"<task>\\nYou are a Git Commit Push bot. Your task is to analyze changes in a git repository, write a detailed commit message following the Conventional Commits specification, and push the changes to git.\\n</task>\\n\\n# Instructions\\n\\n<instructions>\\nFirst, check if there are commits in the remote repository that have not been synced locally:\\n1. Run `git fetch` to update remote tracking branches\\n2. Check if the local branch is behind the remote using `git status` or `git log`\\n3. If there are unsynced commits from the remote:\\n   - Perform a `git pull` to merge remote changes\\n   - If merge conflicts occur:\\n     a. Carefully analyze the conflicting changes\\n     b. Resolve conflicts by keeping the appropriate changes from both versions\\n     c. Mark conflicts as resolved using `git add`\\n     d. Complete the merge\\n4. Only proceed with the following steps after ensuring the local repository is up-to-date\\n\\nAnalyze the changes shown in the git diff and status outputs. Pay attention to:\\n1. Which files were modified, added, or deleted\\n2. The nature of the changes (e.g., bug fixes, new features, refactoring)\\n3. The scope of the changes (which part of the project was affected)\\n\\nBased on your analysis, write a commit message following the Conventional Commits specification:\\n1. Use one of the following types: feat, fix, docs, style, refactor, perf, test, or chore\\n2. Include a scope in parentheses if applicable\\n3. Write a concise description in the present tense\\n4. If necessary, add a longer description after a blank line\\n5. Include any breaking changes or issues closed\\n\\nThen finally push the changes to git.\\n</instructions>\\n\\n# Notes\\n\\n<notes>\\n- Replace [branch_name] with the appropriate branch name based on the information in the git log. If you cannot determine the branch name, use \\\"main\\\" as the default.\\n- Remember to think carefully about the changes and their impact on the project when crafting your commit message. Your goal is to provide a clear and informative record of the changes made to the repository.\\n- When resolving merge conflicts, prioritize maintaining functionality and avoiding breaking changes. If unsure about a conflict resolution, prefer a conservative approach that preserves existing behavior.\\n</notes>\"\n  },\n  \"exported_at\": \"2025-06-23T14:29:58.156063+00:00\",\n  \"version\": 1\n}\n"
  },
  {
    "path": "cc_agents/security-scanner.opcode.json",
    "content": "{\n  \"agent\": {\n    \"default_task\": \"Review the codebase for security issues.\",\n    \"icon\": \"shield\",\n    \"model\": \"opus\",\n    \"name\": \"Security Scanner\",\n    \"system_prompt\": \"# AI SAST Agent - System Prompt\\n\\n<role>\\nYou are an advanced AI-powered Static Application Security Testing (SAST) agent specialized in performing deep, comprehensive security audits of codebases. You identify vulnerabilities with high precision, analyze attack vectors, and produce professional security reports following industry standards. You operate by orchestrating specialized sub-agents for each phase of the security assessment.\\n</role>\\n\\n<primary_objectives>\\n1. Perform thorough static analysis to identify security vulnerabilities\\n2. Minimize false positives through contextual analysis and validation\\n3. Provide actionable remediation guidance with code examples\\n4. Generate professional security reports suitable for development and security teams\\n5. Prioritize findings based on exploitability and business impact\\n</primary_objectives>\\n\\n<methodology>\\nApply a systematic approach combining:\\n- **OWASP Top 10** vulnerability patterns\\n- **CWE (Common Weakness Enumeration)** classification\\n- **STRIDE** threat modeling\\n- **Data Flow Analysis** for taint tracking\\n- **Control Flow Analysis** for logic vulnerabilities\\n</methodology>\\n\\n<workflow>\\n\\n## Phase 1: Codebase Intelligence Gathering\\n<task_spawn>\\nSpawn a **Codebase Intelligence Analyzer** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nPerform deep codebase analysis to extract:\\n\\n<analysis_targets>\\n- Language(s), frameworks, and libraries with versions\\n- Architecture patterns (MVC, microservices, serverless, etc.)\\n- Authentication and authorization mechanisms\\n- Data storage systems and ORM usage\\n- External integrations and API endpoints\\n- Input validation and sanitization practices\\n- Cryptographic implementations\\n- Session management approach\\n- File and resource handling\\n- Third-party dependencies and known CVEs\\n</analysis_targets>\\n```\\n</task_spawn>\\n\\n## Phase 2: Threat Modeling\\n<task_spawn>\\nSpawn a **Threat Modeling Specialist** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nCreate a comprehensive threat model based on the codebase intelligence:\\n\\n<threat_model_components>\\n1. Asset Identification:\\n   - Sensitive data (PII, credentials, financial)\\n   - Critical business logic\\n   - Infrastructure components\\n   \\n2. Trust Boundaries:\\n   - User-to-application boundaries\\n   - Service-to-service boundaries\\n   - Network segmentation points\\n   \\n3. Entry Points:\\n   - API endpoints\\n   - User interfaces\\n   - File upload mechanisms\\n   - Background job processors\\n   - WebSocket connections\\n   \\n4. STRIDE Analysis per component:\\n   - Spoofing threats\\n   - Tampering threats\\n   - Repudiation threats\\n   - Information disclosure threats\\n   - Denial of service threats\\n   - Elevation of privilege threats\\n</threat_model_components>\\n```\\n</task_spawn>\\n\\n## Phase 3: Vulnerability Scanning\\n<task_spawn>\\nFor each identified entry point and component, spawn a **Vulnerability Scanner** sub-agent using the `Task` tool:\\n\\n```\\nScan for vulnerabilities in component: [COMPONENT_NAME]\\n\\n<scanning_checklist>\\nINJECTION VULNERABILITIES:\\n- SQL Injection (including blind, time-based, union-based)\\n- NoSQL Injection\\n- LDAP Injection\\n- OS Command Injection\\n- Code Injection (eval, dynamic execution)\\n- XML/XXE Injection\\n- Template Injection\\n- Header Injection\\n\\nAUTHENTICATION & SESSION:\\n- Broken authentication flows\\n- Weak password policies\\n- Session fixation\\n- Insufficient session expiration\\n- Predictable tokens\\n- Missing MFA enforcement\\n\\nACCESS CONTROL:\\n- Horizontal privilege escalation\\n- Vertical privilege escalation\\n- IDOR (Insecure Direct Object References)\\n- Missing function-level access control\\n- Path traversal\\n- Forced browsing\\n\\nDATA EXPOSURE:\\n- Sensitive data in logs\\n- Unencrypted sensitive data\\n- Information leakage in errors\\n- Directory listing\\n- Source code disclosure\\n- API information disclosure\\n\\nCRYPTOGRAPHIC ISSUES:\\n- Weak algorithms\\n- Hardcoded keys/secrets\\n- Insufficient key length\\n- Improper IV usage\\n- Insecure random number generation\\n\\nBUSINESS LOGIC:\\n- Race conditions\\n- Time-of-check time-of-use (TOCTOU)\\n- Workflow bypass\\n- Price manipulation\\n- Insufficient rate limiting\\n\\nCONFIGURATION:\\n- Security misconfiguration\\n- Default credentials\\n- Unnecessary services\\n- Verbose error messages\\n- Missing security headers\\n</scanning_checklist>\\n\\n<analysis_requirements>\\nFor each potential vulnerability:\\n1. Trace complete data flow from source to sink\\n2. Identify all transformations applied\\n3. Check for existing mitigations\\n4. Verify exploitability conditions\\n5. Map to CWE identifier\\n</analysis_requirements>\\n\\nReturn findings in structured format with full context.\\n```\\n</task_spawn>\\n\\n## Phase 4: Exploit Development & Validation\\n<task_spawn>\\nSpawn an **Exploit Developer** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nFor each identified vulnerability, develop proof-of-concept exploits:\\n\\n<exploit_requirements>\\n1. Create minimal, working PoC code\\n2. Document exact preconditions\\n3. Show full attack chain\\n4. Demonstrate impact clearly\\n5. Avoid destructive payloads\\n6. Include both manual and automated versions\\n</exploit_requirements>\\n\\n<poc_template>\\nFor each vulnerability provide:\\n- Setup requirements\\n- Step-by-step exploitation\\n- Expected vs actual behavior\\n- Screenshot/output evidence\\n- Automation script (curl/python/etc)\\n</poc_template>\\n\\nValidate each finding to ensure:\\n- Reproducibility\\n- Real-world exploitability\\n- No false positives\\n```\\n</task_spawn>\\n\\n## Phase 5: Remediation Design\\n<task_spawn>\\nSpawn a **Security Architect** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nDesign comprehensive remediation strategies:\\n\\n<remediation_components>\\n1. Immediate Fixes:\\n   - Code patches with examples\\n   - Configuration changes\\n   - Quick mitigations\\n\\n2. Long-term Solutions:\\n   - Architectural improvements\\n   - Security control implementations\\n   - Process enhancements\\n\\n3. Defense in Depth:\\n   - Primary fix\\n   - Compensating controls\\n   - Detection mechanisms\\n   - Incident response procedures\\n</remediation_components>\\n\\nInclude:\\n- Specific code examples in the target language\\n- Library recommendations with versions\\n- Testing strategies for fixes\\n- Regression prevention measures\\n```\\n</task_spawn>\\n\\n## Phase 6: Report Generation\\n<task_spawn>\\nSpawn a **Security Report Writer** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nGenerate a professional security assessment report:\\n\\n<report_sections>\\n1. Executive Summary\\n   - Key findings overview\\n   - Risk summary\\n   - Business impact analysis\\n   - Prioritized recommendations\\n\\n2. Technical Summary\\n   - Vulnerability statistics\\n   - Severity distribution\\n   - Attack vector analysis\\n   - Affected components\\n\\n3. Detailed Findings\\n   [Use HackerOne format for each]\\n\\n4. Remediation Roadmap\\n   - Quick wins (< 1 day)\\n   - Short-term (1-7 days)\\n   - Long-term (> 7 days)\\n\\n5. Appendices\\n   - Methodology\\n   - Tools used\\n   - References\\n</report_sections>\\n```\\n</task_spawn>\\n\\n</workflow>\\n\\n<vulnerability_report_format>\\n## [CWE-XXX] Vulnerability Title\\n\\n### Summary\\n**Severity**: Critical | High | Medium | Low | Informational\\n**CVSS Score**: X.X (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)\\n**CWE**: CWE-XXX\\n**OWASP**: A0X:2021 – Category Name\\n\\n### Description\\n[Concise explanation of the vulnerability and its potential impact]\\n\\n### Technical Details\\n<details>\\n<summary>Affected Component</summary>\\n\\n```\\nFile: /path/to/vulnerable/file.ext\\nFunction: vulnerableFunction()\\nLines: 42-58\\n```\\n</details>\\n\\n<details>\\n<summary>Data Flow Analysis</summary>\\n\\n```\\n1. User input received at: controller.getUserInput() [line 42]\\n   ↓ (no sanitization)\\n2. Passed to: service.processData(input) [line 45]\\n   ↓ (string concatenation)\\n3. Used in: database.query(sql + input) [line 58]\\n   ↓ (direct execution)\\n4. SINK: SQL query execution with untrusted data\\n```\\n</details>\\n\\n### Proof of Concept\\n\\n```bash\\n# Manual exploitation\\ncurl -X POST https://target.com/api/users \\\\\\n  -H \\\"Content-Type: application/json\\\" \\\\\\n  -d '{\\\"name\\\": \\\"admin\\\\\\\"; DROP TABLE users; --\\\"}'\\n\\n# Automated PoC\\npython3 exploit_sqli.py --target https://target.com --payload \\\"' OR '1'='1\\\"\\n```\\n\\n**Expected Result**: Error or filtered input\\n**Actual Result**: SQL query executed, data exposed\\n\\n### Impact\\n- **Confidentiality**: High - Full database access possible\\n- **Integrity**: High - Data manipulation possible\\n- **Availability**: Medium - DoS via resource exhaustion\\n\\n### Remediation\\n\\n#### Immediate Fix\\n```[language]\\n// Vulnerable code\\nconst query = `SELECT * FROM users WHERE id = ${userId}`;\\n\\n// Secure code\\nconst query = 'SELECT * FROM users WHERE id = ?';\\ndb.query(query, [userId]);\\n```\\n\\n#### Long-term Solution\\n1. Implement parameterized queries throughout\\n2. Add input validation layer\\n3. Deploy WAF rules for SQL injection patterns\\n4. Enable database query logging and monitoring\\n\\n### References\\n- [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)\\n- [OWASP SQL Injection Prevention](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)\\n\\n---\\n</vulnerability_report_format>\\n\\n<severity_classification>\\n**Critical**: \\n- Remote code execution\\n- Authentication bypass\\n- Full data breach potential\\n- Complete system compromise\\n\\n**High**:\\n- SQL/NoSQL injection\\n- Privilege escalation\\n- Sensitive data exposure\\n- Critical business logic flaws\\n\\n**Medium**:\\n- XSS (stored/reflected)\\n- CSRF on sensitive actions\\n- Session management issues\\n- Information disclosure\\n\\n**Low**:\\n- Missing security headers\\n- Weak configurations\\n- Information leakage\\n- Minor logic flaws\\n\\n**Informational**:\\n- Best practice violations\\n- Defense-in-depth opportunities\\n- Future-proofing recommendations\\n</severity_classification>\\n\\n<quality_assurance>\\nBefore finalizing any finding:\\n1. ✓ Verified exploitability (not just theoretical)\\n2. ✓ Confirmed source-to-sink flow\\n3. ✓ Tested proposed fix\\n4. ✓ No false positives\\n5. ✓ Business context considered\\n6. ✓ CWE/OWASP mapping accurate\\n</quality_assurance>\\n\\n<communication_guidelines>\\n- Use clear, non-technical language in summaries\\n- Provide technical depth in detailed sections\\n- Include visual diagrams where helpful\\n- Reference industry standards\\n- Maintain professional, constructive tone\\n- Focus on solutions, not just problems\\n</communication_guidelines>\\n\\n<continuous_improvement>\\nAfter each phase:\\n- Log any false positives encountered\\n- Document new vulnerability patterns discovered\\n- Update scanning rules based on findings\\n- Refine severity ratings based on context\\n- Enhance PoC templates for efficiency\\n</continuous_improvement>\"\n  },\n  \"exported_at\": \"2025-06-23T14:29:55.510402+00:00\",\n  \"version\": 1\n}\n"
  },
  {
    "path": "cc_agents/unit-tests-bot.opcode.json",
    "content": "{\n  \"agent\": {\n    \"default_task\": \"Generate unit tests for this codebase.\",\n    \"icon\": \"code\",\n    \"model\": \"opus\",\n    \"name\": \"Unit Tests Bot\",\n    \"system_prompt\": \"# Unit Tests Generation Agent\\n\\n<role>\\nYou are an autonomous Unit Test Generation Agent specialized in analyzing codebases, writing comprehensive unit tests, verifying test coverage, and documenting the testing process. You work by spawning specialized sub-agents for each phase of the testing workflow.\\n</role>\\n\\n<primary_objectives>\\n1. Analyze the existing codebase structure and coding patterns\\n2. Generate comprehensive unit tests that match the codebase style\\n3. Execute and verify all generated tests\\n4. Create detailed documentation of the testing process and coverage\\n5. Ensure 100% critical path coverage and >80% overall code coverage\\n</primary_objectives>\\n\\n<workflow>\\n\\n## Phase 1: Codebase Analysis\\n<task_spawn>\\nSpawn a **Codebase Analyzer** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nAnalyze the codebase structure and extract:\\n- Programming language(s) and frameworks\\n- Existing test framework and patterns\\n- Code style conventions (naming, formatting, structure)\\n- Directory structure and test file locations\\n- Dependencies and testing utilities\\n- Coverage requirements and existing coverage reports\\n```\\n</task_spawn>\\n\\n## Phase 2: Test Planning\\n<task_spawn>\\nSpawn a **Test Planner** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nBased on the codebase analysis, create a comprehensive test plan:\\n- Identify all testable modules/classes/functions\\n- Categorize by priority (critical, high, medium, low)\\n- Define test scenarios for each component\\n- Specify edge cases and error conditions\\n- Plan integration test requirements\\n- Estimate coverage targets per module\\n```\\n</task_spawn>\\n\\n## Phase 3: Test Generation\\n<task_spawn>\\nFor each module identified in the test plan, spawn a **Test Writer** sub-agent using the `Task` tool:\\n\\n```\\nGenerate unit tests for module: [MODULE_NAME]\\nRequirements:\\n- Follow existing test patterns and conventions\\n- Use the same testing framework as the codebase\\n- Include positive, negative, and edge case scenarios\\n- Add descriptive test names and comments\\n- Mock external dependencies appropriately\\n- Ensure tests are isolated and repeatable\\nReturn the complete test file(s) with proper imports and setup.\\n```\\n</task_spawn>\\n\\n## Phase 4: Test Verification\\n<task_spawn>\\nSpawn a **Test Verifier** sub-agent using the `Task` tool with the following instruction:\\n```\\nExecute and verify all generated tests:\\n- Run the test suite and capture results\\n- Identify any failing tests\\n- Check for flaky or non-deterministic tests\\n- Measure code coverage metrics\\n- Validate test isolation and independence\\n- Ensure no test pollution or side effects\\nReturn a verification report with any necessary fixes.\\n```\\n</task_spawn>\\n\\n## Phase 5: Coverage Optimization\\n<task_spawn>\\nIf coverage targets are not met, spawn a **Coverage Optimizer** sub-agent using the `Task` tool:\\n\\n```\\nAnalyze coverage gaps and generate additional tests:\\n- Identify uncovered code paths\\n- Generate tests for missed branches\\n- Add tests for error handling paths\\n- Cover edge cases in complex logic\\n- Ensure mutation testing resistance\\nReturn additional tests to meet coverage targets.\\n```\\n</task_spawn>\\n\\n## Phase 6: Documentation Generation\\n<task_spawn>\\nSpawn a **Documentation Writer** sub-agent using the `Task` tool with the following instruction:\\n\\n```\\nCreate comprehensive testing documentation:\\n- Overview of test suite structure\\n- Test coverage summary and metrics\\n- Guide for running and maintaining tests\\n- Description of key test scenarios\\n- Known limitations and future improvements\\n- CI/CD integration instructions\\nReturn documentation in Markdown format.\\n```\\n</task_spawn>\\n\\n</workflow>\\n\\n<style_consistency_rules>\\n- **Naming Conventions**: Match the existing codebase patterns (camelCase, snake_case, PascalCase)\\n- **Test Structure**: Follow the Arrange-Act-Assert or Given-When-Then pattern consistently\\n- **File Organization**: Place tests in the same structure as source files\\n- **Import Style**: Use the same import conventions as the main codebase\\n- **Assertion Style**: Use the project's preferred assertion library and patterns\\n- **Comment Style**: Match the documentation style (JSDoc, docstrings, etc.)\\n</style_consistency_rules>\\n\\n<test_quality_criteria>\\n- Each test should have a single, clear purpose\\n- Test names must describe what is being tested and expected outcome\\n- Tests must be independent and can run in any order\\n- Use appropriate mocking for external dependencies\\n- Include both happy path and error scenarios\\n- Ensure tests fail meaningfully when code is broken\\n- Avoid testing implementation details, focus on behavior\\n</test_quality_criteria>\\n\\n<error_handling>\\nIf any phase encounters errors:\\n1. Log the error with context\\n2. Attempt automatic resolution\\n3. If resolution fails, document the issue\\n4. Continue with remaining modules\\n5. Report unresolvable issues in final documentation\\n</error_handling>\\n\\n<verification_steps>\\n1. **Syntax Verification**: Ensure all tests compile/parse correctly\\n2. **Execution Verification**: Run each test in isolation and as a suite\\n3. **Coverage Verification**: Confirm coverage meets targets\\n4. **Performance Verification**: Ensure tests complete in reasonable time\\n5. **Determinism Verification**: Run tests multiple times to check consistency\\n</verification_steps>\\n\\n<best_practices>\\n- **DRY Principle**: Extract common test utilities and helpers\\n- **Clear Assertions**: Use descriptive matchers and error messages\\n- **Test Data**: Use factories or builders for complex test data\\n- **Async Testing**: Properly handle promises and async operations\\n- **Resource Cleanup**: Always clean up after tests (files, connections, etc.)\\n- **Meaningful Variables**: Use descriptive names for test data and results\\n</best_practices>\\n\\n<communication_protocol>\\n- Report progress after each major phase\\n- Log detailed information for debugging\\n- Summarize results at each stage\\n- Provide actionable feedback for failures\\n- Include time estimates for long-running operations\\n</communication_protocol>\\n\\n<final_checklist>\\nBefore completing the task, verify:\\n- [ ] All source files have corresponding test files\\n- [ ] Coverage targets are met (>80% overall, 100% critical)\\n- [ ] All tests pass consistently\\n- [ ] No hardcoded values or environment dependencies\\n- [ ] Tests follow codebase conventions\\n- [ ] Documentation is complete and accurate\\n- [ ] CI/CD integration is configured\\n</final_checklist>\"\n  },\n  \"exported_at\": \"2025-06-23T14:29:51.009370+00:00\",\n  \"version\": 1\n}\n"
  },
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html lang=\"en\" class=\"dark\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"color-scheme\" content=\"dark\" />\n    <title>opcode - Claude Code Session Browser</title>\n  </head>\n\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "justfile",
    "content": "# Opcode - NixOS Build & Development Commands\n\n# Show available commands\ndefault:\n    @just --list\n\n# Enter the Nix development environment\nshell:\n    nix-shell\n\n# Install frontend dependencies\ninstall:\n    npm install\n\n# Build the React frontend\nbuild-frontend:\n    npm run build\n\n# Build the Tauri backend (debug)\nbuild-backend:\n    cd src-tauri && cargo build\n\n# Build the Tauri backend (release)\nbuild-backend-release:\n    cd src-tauri && cargo build --release\n\n# Build everything (frontend + backend)\nbuild: install build-frontend build-backend\n\n# Run the application in development mode\nrun: build-frontend\n    cd src-tauri && cargo run\n\n# Run the application (release mode)\nrun-release: build-frontend build-backend-release\n    cd src-tauri && cargo run --release\n\n# Clean all build artifacts\nclean:\n    rm -rf node_modules dist\n    cd src-tauri && cargo clean\n\n# Development server (requires frontend build first)\ndev: build-frontend\n    cd src-tauri && cargo run\n\n# Run tests\ntest:\n    cd src-tauri && cargo test\n\n# Format Rust code\nfmt:\n    cd src-tauri && cargo fmt\n\n# Check Rust code\ncheck:\n    cd src-tauri && cargo check\n\n# Quick development cycle: build frontend and run\nquick: build-frontend\n    cd src-tauri && cargo run\n\n# Full rebuild from scratch\nrebuild: clean build run\n\n# Run web server mode for phone access\nweb: build-frontend\n    cd src-tauri && cargo run --bin opcode-web\n\n# Run web server on custom port\nweb-port PORT: build-frontend\n    cd src-tauri && cargo run --bin opcode-web -- --port {{PORT}}\n\n# Get local IP for phone access\nip:\n    @echo \"🌐 Your PC's IP addresses:\"\n    @ip route get 1.1.1.1 | grep -oP 'src \\K\\S+' || echo \"Could not detect IP\"\n    @echo \"\"\n    @echo \"📱 Use this IP on your phone: http://YOUR_IP:8080\"\n\n# Show build information\ninfo:\n    @echo \"🚀 Opcode - Claude Code GUI Application\"\n    @echo \"Built for NixOS without Docker\"\n    @echo \"\"\n    @echo \"📦 Frontend: React + TypeScript + Vite\"\n    @echo \"🦀 Backend: Rust + Tauri\"\n    @echo \"🏗️  Build System: Nix + Just\"\n    @echo \"\"\n    @echo \"💡 Common commands:\"\n    @echo \"  just run      - Build and run (desktop)\"\n    @echo \"  just web      - Run web server for phone access\"\n    @echo \"  just quick    - Quick build and run\"\n    @echo \"  just rebuild  - Full clean rebuild\"\n    @echo \"  just shell    - Enter Nix environment\"\n    @echo \"  just ip       - Show IP for phone access\""
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"opcode\",\n  \"private\": true,\n  \"version\": \"0.2.1\",\n  \"license\": \"AGPL-3.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"prebuild\": \"\",\n    \"build:executables\": \"bun run scripts/fetch-and-build.js --version=1.0.41\",\n    \"build:executables:current\": \"bun run scripts/fetch-and-build.js current --version=1.0.41\",\n    \"build:executables:linux\": \"bun run scripts/fetch-and-build.js linux --version=1.0.41\",\n    \"build:executables:macos\": \"bun run scripts/fetch-and-build.js macos --version=1.0.41\",\n    \"build:executables:windows\": \"bun run scripts/fetch-and-build.js windows --version=1.0.41\",\n    \"preview\": \"vite preview\",\n    \"tauri\": \"tauri\",\n    \"build:dmg\": \"tauri build --bundles dmg\",\n    \"check\": \"tsc --noEmit && cd src-tauri && cargo check\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"^3.9.1\",\n    \"@radix-ui/react-dialog\": \"^1.1.4\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.15\",\n    \"@radix-ui/react-label\": \"^2.1.1\",\n    \"@radix-ui/react-popover\": \"^1.1.4\",\n    \"@radix-ui/react-radio-group\": \"^1.3.7\",\n    \"@radix-ui/react-select\": \"^2.1.3\",\n    \"@radix-ui/react-switch\": \"^1.1.3\",\n    \"@radix-ui/react-tabs\": \"^1.1.3\",\n    \"@radix-ui/react-toast\": \"^1.2.3\",\n    \"@radix-ui/react-tooltip\": \"^1.1.5\",\n    \"@tailwindcss/cli\": \"^4.1.8\",\n    \"@tailwindcss/vite\": \"^4.1.8\",\n    \"@tanstack/react-virtual\": \"^3.13.10\",\n    \"@tauri-apps/api\": \"^2.1.1\",\n    \"@tauri-apps/plugin-dialog\": \"^2.0.2\",\n    \"@tauri-apps/plugin-global-shortcut\": \"^2.0.0\",\n    \"@tauri-apps/plugin-opener\": \"^2\",\n    \"@tauri-apps/plugin-shell\": \"^2.0.1\",\n    \"@types/diff\": \"^8.0.0\",\n    \"@types/react-syntax-highlighter\": \"^15.5.13\",\n    \"@uiw/react-md-editor\": \"^4.0.7\",\n    \"ansi-to-html\": \"^0.7.2\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"date-fns\": \"^3.6.0\",\n    \"diff\": \"^8.0.2\",\n    \"framer-motion\": \"^12.0.0-alpha.1\",\n    \"html2canvas\": \"^1.4.1\",\n    \"lucide-react\": \"^0.468.0\",\n    \"posthog-js\": \"^1.258.3\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-hook-form\": \"^7.54.2\",\n    \"react-markdown\": \"^9.0.3\",\n    \"react-syntax-highlighter\": \"^15.6.1\",\n    \"recharts\": \"^2.14.1\",\n    \"remark-gfm\": \"^4.0.0\",\n    \"tailwind-merge\": \"^2.6.0\",\n    \"tailwindcss\": \"^4.1.8\",\n    \"zod\": \"^3.24.1\",\n    \"zustand\": \"^5.0.6\"\n  },\n  \"devDependencies\": {\n    \"@tauri-apps/cli\": \"^2.7.1\",\n    \"@types/node\": \"^22.15.30\",\n    \"@types/react\": \"^18.3.1\",\n    \"@types/react-dom\": \"^18.3.1\",\n    \"@types/sharp\": \"^0.32.0\",\n    \"@vitejs/plugin-react\": \"^4.3.4\",\n    \"sharp\": \"^0.34.2\",\n    \"typescript\": \"~5.6.2\",\n    \"vite\": \"^6.0.3\"\n  },\n  \"trustedDependencies\": [\n    \"@parcel/watcher\",\n    \"@tailwindcss/oxide\"\n  ],\n  \"optionalDependencies\": {\n    \"@esbuild/linux-x64\": \"^0.25.6\",\n    \"@rollup/rollup-linux-x64-gnu\": \"^4.45.1\"\n  }\n}\n"
  },
  {
    "path": "scripts/bump-version.sh",
    "content": "#!/bin/bash\n\n# Script to bump version across all files\n# Usage: ./scripts/bump-version.sh 1.0.0\n\nset -e\n\nif [ -z \"$1\" ]; then\n    echo \"Usage: $0 <version>\"\n    echo \"Example: $0 1.0.0\"\n    exit 1\nfi\n\nVERSION=$1\n\necho \"Bumping version to $VERSION...\"\n\n# Update package.json\nsed -i.bak \"s/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"$VERSION\\\"/\" package.json && rm package.json.bak\n\n# Update Cargo.toml\nsed -i.bak \"s/^version = \\\".*\\\"/version = \\\"$VERSION\\\"/\" src-tauri/Cargo.toml && rm src-tauri/Cargo.toml.bak\n\n# Update tauri.conf.json\nsed -i.bak \"s/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"$VERSION\\\"/\" src-tauri/tauri.conf.json && rm src-tauri/tauri.conf.json.bak\n\n# Update Info.plist\nsed -i.bak \"s/<string>.*<\\/string><!-- VERSION -->/<string>$VERSION<\\/string><!-- VERSION -->/\" src-tauri/Info.plist && rm src-tauri/Info.plist.bak\n\necho \"✅ Version bumped to $VERSION in all files\"\necho \"\"\necho \"Next steps:\"\necho \"1. Review the changes: git diff\"\necho \"2. Commit: git commit -am \\\"chore: bump version to v$VERSION\\\"\"\necho \"3. Tag: git tag -a v$VERSION -m \\\"Release v$VERSION\\\"\"\necho \"4. Push: git push && git push --tags\"\n"
  },
  {
    "path": "shell.nix",
    "content": "{ pkgs ? import <nixpkgs> {} }:\n\npkgs.mkShell {\n  buildInputs = with pkgs; [\n    # Core development tools\n    just\n    git\n\n    # Node.js/Bun toolchain\n    bun\n    nodejs\n\n    # Rust toolchain\n    rustc\n    cargo\n    rustfmt\n    clippy\n    \n    # System dependencies for Tauri development\n    pkg-config\n    webkitgtk_4_1\n    gtk3\n    cairo\n    gdk-pixbuf\n    glib\n    dbus\n    openssl\n    librsvg\n    libsoup_3\n    libayatana-appindicator\n    \n    # Development utilities\n    curl\n    wget\n    jq\n  ];\n  \n  # Environment variables for development\n  RUST_SRC_PATH = \"${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}\";\n}\n"
  },
  {
    "path": "src/App.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Bot, FolderCode } from \"lucide-react\";\nimport { api, type Project, type Session, type ClaudeMdFile } from \"@/lib/api\";\nimport { initializeWebMode } from \"@/lib/apiAdapter\";\nimport { OutputCacheProvider } from \"@/lib/outputCache\";\nimport { TabProvider } from \"@/contexts/TabContext\";\nimport { ThemeProvider } from \"@/contexts/ThemeContext\";\nimport { Card } from \"@/components/ui/card\";\nimport { ProjectList } from \"@/components/ProjectList\";\nimport { FilePicker } from \"@/components/FilePicker\";\nimport { SessionList } from \"@/components/SessionList\";\nimport { CustomTitlebar } from \"@/components/CustomTitlebar\";\nimport { MarkdownEditor } from \"@/components/MarkdownEditor\";\nimport { ClaudeFileEditor } from \"@/components/ClaudeFileEditor\";\nimport { Settings } from \"@/components/Settings\";\nimport { CCAgents } from \"@/components/CCAgents\";\nimport { UsageDashboard } from \"@/components/UsageDashboard\";\nimport { MCPManager } from \"@/components/MCPManager\";\nimport { NFOCredits } from \"@/components/NFOCredits\";\nimport { ClaudeBinaryDialog } from \"@/components/ClaudeBinaryDialog\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { ProjectSettings } from '@/components/ProjectSettings';\nimport { TabManager } from \"@/components/TabManager\";\nimport { TabContent } from \"@/components/TabContent\";\nimport { useTabState } from \"@/hooks/useTabState\";\nimport { useAppLifecycle, useTrackEvent } from \"@/hooks\";\nimport { StartupIntro } from \"@/components/StartupIntro\";\n\ntype View = \n  | \"welcome\" \n  | \"projects\" \n  | \"editor\" \n  | \"claude-file-editor\" \n  | \"settings\"\n  | \"cc-agents\"\n  | \"create-agent\"\n  | \"github-agents\"\n  | \"agent-execution\"\n  | \"agent-run-view\"\n  | \"mcp\"\n  | \"usage-dashboard\"\n  | \"project-settings\"\n  | \"tabs\"; // New view for tab-based interface\n\n/**\n * AppContent component - Contains the main app logic, wrapped by providers\n */\nfunction AppContent() {\n  const [view, setView] = useState<View>(\"tabs\");\n  const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab, createAgentsTab } = useTabState();\n  const [projects, setProjects] = useState<Project[]>([]);\n  const [selectedProject, setSelectedProject] = useState<Project | null>(null);\n  const [sessions, setSessions] = useState<Session[]>([]);\n  const [editingClaudeFile, setEditingClaudeFile] = useState<ClaudeMdFile | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [_error, setError] = useState<string | null>(null);\n  const [showNFO, setShowNFO] = useState(false);\n  const [showClaudeBinaryDialog, setShowClaudeBinaryDialog] = useState(false);\n  const [showProjectPicker, setShowProjectPicker] = useState(false);\n  const [homeDirectory, setHomeDirectory] = useState<string>('/');\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" | \"info\" } | null>(null);\n  const [projectForSettings, setProjectForSettings] = useState<Project | null>(null);\n  const [previousView] = useState<View>(\"welcome\");\n  \n  // Initialize analytics lifecycle tracking\n  useAppLifecycle();\n  const trackEvent = useTrackEvent();\n  \n  // Track user journey milestones\n  const [hasTrackedFirstChat] = useState(false);\n  // const [hasTrackedFirstAgent] = useState(false);\n  \n  // Track when user reaches different journey stages\n  useEffect(() => {\n    if (view === \"projects\" && projects.length > 0 && !hasTrackedFirstChat) {\n      // User has projects - they're past onboarding\n      trackEvent.journeyMilestone({\n        journey_stage: 'onboarding',\n        milestone_reached: 'projects_created',\n        time_to_milestone_ms: Date.now() - performance.timing.navigationStart\n      });\n    }\n  }, [view, projects.length, hasTrackedFirstChat, trackEvent]);\n\n  // Initialize web mode compatibility on mount\n  useEffect(() => {\n    initializeWebMode();\n  }, []);\n\n  // Load projects on mount when in projects view\n  useEffect(() => {\n    if (view === \"projects\") {\n      loadProjects();\n    } else if (view === \"welcome\") {\n      // Reset loading state for welcome view\n      setLoading(false);\n    }\n  }, [view]);\n\n  // Keyboard shortcuts for tab navigation\n  useEffect(() => {\n    if (view !== \"tabs\") return;\n    \n    const handleKeyDown = (e: KeyboardEvent) => {\n      const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;\n      const modKey = isMac ? e.metaKey : e.ctrlKey;\n      \n      if (modKey) {\n        switch (e.key) {\n          case 't':\n            e.preventDefault();\n            window.dispatchEvent(new CustomEvent('create-chat-tab'));\n            break;\n          case 'w':\n            e.preventDefault();\n            window.dispatchEvent(new CustomEvent('close-current-tab'));\n            break;\n          case 'Tab':\n            e.preventDefault();\n            if (e.shiftKey) {\n              window.dispatchEvent(new CustomEvent('switch-to-previous-tab'));\n            } else {\n              window.dispatchEvent(new CustomEvent('switch-to-next-tab'));\n            }\n            break;\n          default:\n            // Handle number keys 1-9\n            if (e.key >= '1' && e.key <= '9') {\n              e.preventDefault();\n              const index = parseInt(e.key) - 1;\n              window.dispatchEvent(new CustomEvent('switch-to-tab-by-index', { detail: { index } }));\n            }\n            break;\n        }\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [view]);\n\n  // Listen for Claude not found events\n  useEffect(() => {\n    const handleClaudeNotFound = () => {\n      setShowClaudeBinaryDialog(true);\n    };\n\n    window.addEventListener('claude-not-found', handleClaudeNotFound as EventListener);\n    return () => {\n      window.removeEventListener('claude-not-found', handleClaudeNotFound as EventListener);\n    };\n  }, []);\n\n  /**\n   * Loads all projects from the ~/.claude/projects directory\n   */\n  const loadProjects = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const projectList = await api.listProjects();\n      setProjects(projectList);\n    } catch (err) {\n      console.error(\"Failed to load projects:\", err);\n      setError(\"Failed to load projects. Please ensure ~/.claude directory exists.\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Handles project selection and loads its sessions\n   */\n  const handleProjectClick = async (project: Project) => {\n    try {\n      setLoading(true);\n      setError(null);\n      const sessionList = await api.getProjectSessions(project.id);\n      setSessions(sessionList);\n      setSelectedProject(project);\n    } catch (err) {\n      console.error(\"Failed to load sessions:\", err);\n      setError(\"Failed to load sessions for this project.\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Opens the project directory picker\n   */\n  const handleOpenProject = async () => {\n    // Get home directory before showing picker\n    const homeDir = await api.getHomeDirectory();\n    setHomeDirectory(homeDir);\n    setShowProjectPicker(true);\n  };\n\n  /**\n   * Opens a new Claude Code session in the interactive UI\n   */\n  // New session creation is handled by the tab system via titlebar actions\n\n  /**\n   * Handles editing a CLAUDE.md file from a project\n   */\n  const handleEditClaudeFile = (file: ClaudeMdFile) => {\n    setEditingClaudeFile(file);\n    handleViewChange(\"claude-file-editor\");\n  };\n\n  /**\n   * Returns from CLAUDE.md file editor to projects view\n   */\n  const handleBackFromClaudeFileEditor = () => {\n    setEditingClaudeFile(null);\n    handleViewChange(\"projects\");\n  };\n\n  /**\n   * Handles view changes with navigation protection\n   */\n  const handleViewChange = (newView: View) => {\n    // No need for navigation protection with tabs since sessions stay open\n    setView(newView);\n  };\n\n  /**\n   * Handles navigating to hooks configuration\n   */\n  // Project settings navigation handled via `projectForSettings` state when needed\n\n\n  const renderContent = () => {\n    switch (view) {\n      case \"welcome\":\n        return (\n          <div className=\"flex items-center justify-center p-4\" style={{ height: \"100%\" }}>\n            <div className=\"w-full max-w-4xl\">\n              {/* Welcome Header */}\n              <motion.div\n                initial={{ opacity: 0, y: 8 }}\n                animate={{ opacity: 1, y: 0 }}\n                transition={{ duration: 0.15 }}\n                className=\"mb-12 text-center\"\n              >\n                <h1 className=\"text-4xl font-bold tracking-tight\">\n                  <span className=\"rotating-symbol\"></span>\n                  Welcome to opcode\n                </h1>\n              </motion.div>\n\n              {/* Navigation Cards */}\n              <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 max-w-2xl mx-auto\">\n                {/* CC Agents Card */}\n                <motion.div\n                  initial={{ opacity: 0, y: 8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  transition={{ duration: 0.15, delay: 0.05 }}\n                >\n                  <Card \n                    className=\"h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover trailing-border\"\n                    onClick={() => handleViewChange(\"cc-agents\")}\n                  >\n                    <div className=\"h-full flex flex-col items-center justify-center p-8\">\n                      <Bot className=\"h-16 w-16 mb-4 text-primary\" />\n                      <h2 className=\"text-xl font-semibold\">CC Agents</h2>\n                    </div>\n                  </Card>\n                </motion.div>\n\n                {/* Projects Card */}\n                <motion.div\n                  initial={{ opacity: 0, y: 8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  transition={{ duration: 0.15, delay: 0.1 }}\n                >\n                  <Card \n                    className=\"h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover trailing-border\"\n                    onClick={() => handleViewChange(\"projects\")}\n                  >\n                    <div className=\"h-full flex flex-col items-center justify-center p-8\">\n                      <FolderCode className=\"h-16 w-16 mb-4 text-primary\" />\n                      <h2 className=\"text-xl font-semibold\">Projects</h2>\n                    </div>\n                  </Card>\n                </motion.div>\n\n              </div>\n            </div>\n          </div>\n        );\n\n      case \"cc-agents\":\n        return (\n          <CCAgents \n            onBack={() => handleViewChange(\"welcome\")} \n          />\n        );\n\n      case \"editor\":\n        return (\n          <div className=\"flex-1 overflow-hidden\">\n            <MarkdownEditor onBack={() => handleViewChange(\"welcome\")} />\n          </div>\n        );\n      \n      case \"settings\":\n        return <Settings onBack={() => handleViewChange(\"welcome\")} />;\n      \n      case \"projects\":\n        if (selectedProject) {\n          return (\n            <SessionList\n              sessions={sessions}\n              projectPath={selectedProject.path}\n              onEditClaudeFile={handleEditClaudeFile}\n            />\n          );\n        }\n        return (\n          <ProjectList\n            projects={projects}\n            onProjectClick={handleProjectClick}\n            onOpenProject={handleOpenProject}\n            loading={loading}\n          />\n        );\n      \n      case \"claude-file-editor\":\n        return editingClaudeFile ? (\n          <ClaudeFileEditor\n            file={editingClaudeFile}\n            onBack={handleBackFromClaudeFileEditor}\n          />\n        ) : null;\n      \n      case \"tabs\":\n        return (\n          <div className=\"h-full flex flex-col\">\n            <TabManager className=\"flex-shrink-0\" />\n            <div className=\"flex-1 overflow-hidden\">\n              <TabContent />\n            </div>\n          </div>\n        );\n      \n      case \"usage-dashboard\":\n        return (\n          <UsageDashboard onBack={() => handleViewChange(\"welcome\")} />\n        );\n      \n      case \"mcp\":\n        return (\n          <MCPManager onBack={() => handleViewChange(\"welcome\")} />\n        );\n      \n      case \"project-settings\":\n        if (projectForSettings) {\n          return (\n            <ProjectSettings\n              project={projectForSettings}\n              onBack={() => {\n                setProjectForSettings(null);\n                handleViewChange(previousView || \"projects\");\n              }}\n            />\n          );\n        }\n        break;\n      \n      default:\n        return null;\n    }\n  };\n\n  return (\n    <div className=\"h-screen flex flex-col\">\n      {/* Custom Titlebar */}\n      <CustomTitlebar\n        onAgentsClick={() => createAgentsTab()}\n        onUsageClick={() => createUsageTab()}\n        onClaudeClick={() => createClaudeMdTab()}\n        onMCPClick={() => createMCPTab()}\n        onSettingsClick={() => createSettingsTab()}\n        onInfoClick={() => setShowNFO(true)}\n      />\n      \n      {/* Topbar - Commented out since navigation moved to titlebar */}\n      {/* <Topbar\n        onClaudeClick={() => createClaudeMdTab()}\n        onSettingsClick={() => createSettingsTab()}\n        onUsageClick={() => createUsageTab()}\n        onMCPClick={() => createMCPTab()}\n        onInfoClick={() => setShowNFO(true)}\n        onAgentsClick={() => setShowAgentsModal(true)}\n      /> */}\n      \n      \n      \n      {/* Main Content */}\n      <div className=\"flex-1 overflow-hidden\">\n        {renderContent()}\n      </div>\n      \n      {/* NFO Credits Modal */}\n      {showNFO && <NFOCredits onClose={() => setShowNFO(false)} />}\n      \n      \n      {/* Claude Binary Dialog */}\n      <ClaudeBinaryDialog\n        open={showClaudeBinaryDialog}\n        onOpenChange={setShowClaudeBinaryDialog}\n        onSuccess={() => {\n          setToast({ message: \"Claude binary path saved successfully\", type: \"success\" });\n          // Trigger a refresh of the Claude version check\n          window.location.reload();\n        }}\n        onError={(message) => setToast({ message, type: \"error\" })}\n      />\n\n      {/* File picker modal for selecting project directory */}\n      {showProjectPicker && (\n        <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm\">\n          <div className=\"w-full max-w-2xl h-[600px] bg-background border rounded-lg shadow-lg\">\n            <FilePicker\n              basePath={homeDirectory}\n              onSelect={async (entry) => {\n                if (entry.is_directory) {\n                  // Create or open a project for this directory\n                  try {\n                    const project = await api.createProject(entry.path);\n                    setShowProjectPicker(false);\n                    await loadProjects();\n                    await handleProjectClick(project);\n                  } catch (err) {\n                    console.error('Failed to create project:', err);\n                    setError('Failed to create project for the selected directory.');\n                  }\n                }\n              }}\n              onClose={() => setShowProjectPicker(false)}\n            />\n          </div>\n        </div>\n      )}\n      \n      {/* Toast Container */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n\n      {/* File picker modal for selecting project directory */}\n      {showProjectPicker && (\n        <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm\">\n          <div className=\"w-full max-w-2xl h-[600px] bg-background border rounded-lg shadow-lg\">\n            <FilePicker\n              basePath={homeDirectory}\n              onSelect={async (entry) => {\n                if (entry.is_directory) {\n                  // Create or open a project for this directory\n                  try {\n                    const project = await api.createProject(entry.path);\n                    setShowProjectPicker(false);\n                    await loadProjects();\n                    // Load sessions for the selected project\n                    await handleProjectClick(project);\n                  } catch (err) {\n                    console.error('Failed to create project:', err);\n                    setError('Failed to create project for the selected directory.');\n                  }\n                }\n              }}\n              onClose={() => setShowProjectPicker(false)}\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\n/**\n * Main App component - Wraps the app with providers\n */\nfunction App() {\n  const [showIntro, setShowIntro] = useState(() => {\n    // Read cached preference synchronously to avoid any initial flash\n    try {\n      const cached = typeof window !== 'undefined'\n        ? window.localStorage.getItem('app_setting:startup_intro_enabled')\n        : null;\n      if (cached === 'true') return true;\n      if (cached === 'false') return false;\n    } catch (_ignore) {}\n    return true; // default if no cache\n  });\n\n  useEffect(() => {\n    let timer: number | undefined;\n    (async () => {\n      try {\n        const pref = await api.getSetting('startup_intro_enabled');\n        const enabled = pref === null ? true : pref === 'true';\n        if (enabled) {\n          // keep intro visible and hide after duration\n          timer = window.setTimeout(() => setShowIntro(false), 2000);\n        } else {\n          // user disabled intro: hide immediately to avoid any overlay delay\n          setShowIntro(false);\n        }\n      } catch (err) {\n        // On failure, show intro once to keep UX consistent\n        timer = window.setTimeout(() => setShowIntro(false), 2000);\n      }\n    })();\n    return () => {\n      if (timer) window.clearTimeout(timer);\n    };\n  }, []);\n\n  return (\n    <ThemeProvider>\n      <OutputCacheProvider>\n        <TabProvider>\n          <AppContent />\n          <StartupIntro visible={showIntro} />\n        </TabProvider>\n      </OutputCacheProvider>\n    </ThemeProvider>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "src/assets/shimmer.css",
    "content": "/**\n * Shimmer animation styles\n * Provides a sword-like shimmer effect for elements\n */\n\n@keyframes shimmer {\n  0% {\n    transform: translateX(-100%);\n    opacity: 0;\n  }\n  20% {\n    opacity: 1;\n  }\n  40% {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n  50% {\n    transform: translateX(-100%);\n    opacity: 0;\n  }\n  70% {\n    opacity: 1;\n  }\n  90% {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n  100% {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n}\n\n@keyframes shimmer-text {\n  0% {\n    background-position: -200% center;\n  }\n  45% {\n    background-position: 200% center;\n  }\n  50% {\n    background-position: -200% center;\n  }\n  95% {\n    background-position: 200% center;\n  }\n  96%, 100% {\n    background-position: 200% center;\n    -webkit-text-fill-color: currentColor;\n    background: none;\n  }\n}\n\n/* Overlay variant: keeps the overlay text transparent and simply fades it out */\n@keyframes shimmer-overlay {\n  0% {\n    background-position: -150% center;\n    opacity: 1;\n  }\n  100% {\n    background-position: 150% center;\n    opacity: 0;\n  }\n}\n\n@keyframes symbol-rotate {\n  0% {\n    content: '◐';\n    opacity: 1;\n    transform: translateY(0) scale(1);\n  }\n  25% {\n    content: '◓';\n    opacity: 1;\n    transform: translateY(0) scale(1);\n  }\n  50% {\n    content: '◑';\n    opacity: 1;\n    transform: translateY(0) scale(1);\n  }\n  75% {\n    content: '◒';\n    opacity: 1;\n    transform: translateY(0) scale(1);\n  }\n  100% {\n    content: '◐';\n    opacity: 1;\n    transform: translateY(0) scale(1);\n  }\n}\n\n.shimmer-once {\n  position: relative;\n  display: inline-block;\n  background: linear-gradient(\n    105deg,\n    currentColor 0%,\n    currentColor 40%,\n    #d97757 50%,\n    currentColor 60%,\n    currentColor 100%\n  );\n  background-size: 200% auto;\n  background-position: -200% center;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  background-clip: text;\n  animation: shimmer-text 1s ease-out forwards;\n}\n\n/* Ensures text remains visible after shimmer completes on engines\n   where -webkit-text-fill-color set in keyframes may not persist */\n.shimmer-fallback-visible {\n  -webkit-text-fill-color: currentColor !important;\n  background: none !important;\n}\n\n/* Layered brand text: base solid text plus shimmering overlay to avoid flicker */\n.brand-text { position: relative; display: inline-block; }\n.brand-text-solid { position: relative; color: currentColor; }\n.brand-text-shimmer {\n  position: absolute;\n  inset: 0;\n  color: transparent;\n  -webkit-text-fill-color: transparent;\n  background: linear-gradient(\n    90deg,\n    transparent 0%,\n    transparent 47%,\n    rgba(217, 119, 87, 0.35) 50%,\n    transparent 53%,\n    transparent 100%\n  );\n  background-size: 300% auto;\n  background-position: -150% center;\n  -webkit-background-clip: text;\n  background-clip: text;\n  pointer-events: none;\n  will-change: background-position, opacity;\n  animation: shimmer-overlay 1.1s cubic-bezier(0.4, 0, 0.2, 1) forwards;\n}\n\n.rotating-symbol {\n  display: inline-block;\n  color: #8B5CF6;\n  font-size: 1.5rem; /* Make it bigger! */\n  margin-right: 0.5rem;\n  font-weight: bold;\n  vertical-align: middle;\n  position: relative;\n  line-height: 1;\n  top: -2px;\n}\n\n.rotating-symbol::before {\n  content: '◐';\n  display: inline-block;\n  animation: symbol-rotate 2s linear infinite;\n  font-size: inherit;\n  line-height: inherit;\n  vertical-align: baseline;\n}\n\n/* Allow pausing the rotating symbol via an extra class */\n.rotating-symbol.paused::before {\n  animation: none !important;\n}\n\n.shimmer-hover {\n  position: relative;\n  overflow: hidden;\n}\n\n.shimmer-hover::before {\n  content: '';\n  position: absolute;\n  top: -50%;\n  left: 0;\n  width: 100%;\n  height: 200%;\n  background: linear-gradient(\n    105deg,\n    transparent 0%,\n    transparent 40%,\n    rgba(217, 119, 87, 0.4) 50%,\n    transparent 60%,\n    transparent 100%\n  );\n  transform: translateX(-100%) rotate(-10deg);\n  opacity: 0;\n  pointer-events: none;\n  z-index: 1;\n}\n\n.shimmer-hover > * {\n  position: relative;\n  z-index: 2;\n}\n\n.shimmer-hover:hover::before {\n  animation: shimmer 1s ease-out;\n} \n"
  },
  {
    "path": "src/components/AgentExecution.tsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { \n  ArrowLeft, \n  Play, \n  StopCircle, \n  Terminal,\n  AlertCircle,\n  Loader2,\n  Copy,\n  ChevronDown,\n  Maximize2,\n  X,\n  Settings2\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Popover } from \"@/components/ui/popover\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { api, type Agent } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { listen, type UnlistenFn } from \"@tauri-apps/api/event\";\nimport { StreamMessage } from \"./StreamMessage\";\nimport { ExecutionControlBar } from \"./ExecutionControlBar\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { HooksEditor } from \"./HooksEditor\";\nimport { useTrackEvent, useComponentMetrics, useFeatureAdoptionTracking } from \"@/hooks\";\nimport { useTabState } from \"@/hooks/useTabState\";\n\ninterface AgentExecutionProps {\n  /**\n   * The agent to execute\n   */\n  agent: Agent;\n  /**\n   * Optional initial project path\n   */\n  projectPath?: string;\n  /**\n   * Optional tab ID for updating tab status\n   */\n  tabId?: string;\n  /**\n   * Callback to go back to the agents list\n   */\n  onBack: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\nexport interface ClaudeStreamMessage {\n  type: \"system\" | \"assistant\" | \"user\" | \"result\";\n  subtype?: string;\n  message?: {\n    content?: any[];\n    usage?: {\n      input_tokens: number;\n      output_tokens: number;\n    };\n  };\n  usage?: {\n    input_tokens: number;\n    output_tokens: number;\n  };\n  [key: string]: any;\n}\n\n/**\n * AgentExecution component for running CC agents\n * \n * @example\n * <AgentExecution agent={agent} onBack={() => setView('list')} />\n */\nexport const AgentExecution: React.FC<AgentExecutionProps> = ({\n  agent,\n  projectPath: initialProjectPath,\n  tabId,\n  onBack,\n  className,\n}) => {\n  const [projectPath] = useState(initialProjectPath || \"\");\n  const [task, setTask] = useState(agent.default_task || \"\");\n  const [model, setModel] = useState(agent.model || \"sonnet\");\n  const [isRunning, setIsRunning] = useState(false);\n  \n  // Get tab state functions\n  const { updateTabStatus } = useTabState();\n  const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);\n  const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);\n  const [error, setError] = useState<string | null>(null);\n  const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);\n  \n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n  useComponentMetrics('AgentExecution');\n  const agentFeatureTracking = useFeatureAdoptionTracking(`agent_${agent.name || 'custom'}`);\n  \n  // Hooks configuration state\n  const [isHooksDialogOpen, setIsHooksDialogOpen] = useState(false);\n\n  // IME composition state\n  const isIMEComposingRef = useRef(false);\n  const [activeHooksTab, setActiveHooksTab] = useState(\"project\");\n\n  // Execution stats\n  const [executionStartTime, setExecutionStartTime] = useState<number | null>(null);\n  const [totalTokens, setTotalTokens] = useState(0);\n  const [elapsedTime, setElapsedTime] = useState(0);\n  const [hasUserScrolled, setHasUserScrolled] = useState(false);\n  const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);\n  \n  const messagesEndRef = useRef<HTMLDivElement>(null);\n  const messagesContainerRef = useRef<HTMLDivElement>(null);\n  const scrollContainerRef = useRef<HTMLDivElement>(null);\n  const fullscreenScrollRef = useRef<HTMLDivElement>(null);\n  const fullscreenMessagesEndRef = useRef<HTMLDivElement>(null);\n  const unlistenRefs = useRef<UnlistenFn[]>([]);\n  const elapsedTimeIntervalRef = useRef<NodeJS.Timeout | null>(null);\n  const [runId, setRunId] = useState<number | null>(null);\n\n  // Filter out messages that shouldn't be displayed\n  const displayableMessages = React.useMemo(() => {\n    return messages.filter((message, index) => {\n      // Skip meta messages that don't have meaningful content\n      if (message.isMeta && !message.leafUuid && !message.summary) {\n        return false;\n      }\n\n      // Skip empty user messages\n      if (message.type === \"user\" && message.message) {\n        if (message.isMeta) return false;\n        \n        const msg = message.message;\n        if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) {\n          return false;\n        }\n        \n        // Check if user message has visible content by checking its parts\n        if (Array.isArray(msg.content)) {\n          let hasVisibleContent = false;\n          for (const content of msg.content) {\n            if (content.type === \"text\") {\n              hasVisibleContent = true;\n              break;\n            } else if (content.type === \"tool_result\") {\n              // Check if this tool result will be skipped by a widget\n              let willBeSkipped = false;\n              if (content.tool_use_id) {\n                // Look for the matching tool_use in previous assistant messages\n                for (let i = index - 1; i >= 0; i--) {\n                  const prevMsg = messages[i];\n                  if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {\n                    const toolUse = prevMsg.message.content.find((c: any) => \n                      c.type === 'tool_use' && c.id === content.tool_use_id\n                    );\n                    if (toolUse) {\n                      const toolName = toolUse.name?.toLowerCase();\n                      const toolsWithWidgets = [\n                        'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read', \n                        'glob', 'bash', 'write', 'grep'\n                      ];\n                      if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {\n                        willBeSkipped = true;\n                      }\n                      break;\n                    }\n                  }\n                }\n              }\n              \n              if (!willBeSkipped) {\n                hasVisibleContent = true;\n                break;\n              }\n            }\n          }\n          \n          if (!hasVisibleContent) {\n            return false;\n          }\n        }\n      }\n\n      return true;\n    });\n  }, [messages]);\n\n  // Virtualizers for efficient, smooth scrolling of potentially very long outputs\n  const rowVirtualizer = useVirtualizer({\n    count: displayableMessages.length,\n    getScrollElement: () => scrollContainerRef.current,\n    estimateSize: () => 150, // fallback estimate; dynamically measured afterwards\n    overscan: 5,\n  });\n\n  const fullscreenRowVirtualizer = useVirtualizer({\n    count: displayableMessages.length,\n    getScrollElement: () => fullscreenScrollRef.current,\n    estimateSize: () => 150,\n    overscan: 5,\n  });\n\n  useEffect(() => {\n    // Clean up listeners on unmount\n    return () => {\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      if (elapsedTimeIntervalRef.current) {\n        clearInterval(elapsedTimeIntervalRef.current);\n      }\n    };\n  }, []);\n\n  // Check if user is at the very bottom of the scrollable container\n  const isAtBottom = () => {\n    const container = isFullscreenModalOpen ? fullscreenScrollRef.current : scrollContainerRef.current;\n    if (container) {\n      const { scrollTop, scrollHeight, clientHeight } = container;\n      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;\n      return distanceFromBottom < 1;\n    }\n    return true;\n  };\n\n  useEffect(() => {\n    if (displayableMessages.length === 0) return;\n\n    // Auto-scroll only if the user has not manually scrolled OR they are still at the bottom\n    const shouldAutoScroll = !hasUserScrolled || isAtBottom();\n\n    if (shouldAutoScroll) {\n      if (isFullscreenModalOpen) {\n        fullscreenRowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: \"end\", behavior: \"smooth\" });\n      } else {\n        rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: \"end\", behavior: \"smooth\" });\n      }\n    }\n  }, [displayableMessages.length, hasUserScrolled, isFullscreenModalOpen, rowVirtualizer, fullscreenRowVirtualizer]);\n\n  // Update elapsed time while running\n  useEffect(() => {\n    if (isRunning && executionStartTime) {\n      elapsedTimeIntervalRef.current = setInterval(() => {\n        setElapsedTime(Math.floor((Date.now() - executionStartTime) / 1000));\n      }, 100);\n    } else {\n      if (elapsedTimeIntervalRef.current) {\n        clearInterval(elapsedTimeIntervalRef.current);\n      }\n    }\n    \n    return () => {\n      if (elapsedTimeIntervalRef.current) {\n        clearInterval(elapsedTimeIntervalRef.current);\n      }\n    };\n  }, [isRunning, executionStartTime]);\n\n  // Calculate total tokens from messages\n  useEffect(() => {\n    const tokens = messages.reduce((total, msg) => {\n      if (msg.message?.usage) {\n        return total + msg.message.usage.input_tokens + msg.message.usage.output_tokens;\n      }\n      if (msg.usage) {\n        return total + msg.usage.input_tokens + msg.usage.output_tokens;\n      }\n      return total;\n    }, 0);\n    setTotalTokens(tokens);\n  }, [messages]);\n\n\n  // Project path selection is handled upstream when opening an execution tab\n\n  const handleOpenHooksDialog = async () => {\n    setIsHooksDialogOpen(true);\n  };\n\n  const handleExecute = async () => {\n    try {\n      setIsRunning(true);\n      // Update tab status to running\n      console.log('Setting tab status to running for tab:', tabId);\n      if (tabId) {\n        updateTabStatus(tabId, 'running');\n      }\n      setExecutionStartTime(Date.now());\n      setMessages([]);\n      setRawJsonlOutput([]);\n      setRunId(null);\n      \n      // Clear any existing listeners\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      unlistenRefs.current = [];\n      \n      // Execute the agent and get the run ID\n      const executionRunId = await api.executeAgent(agent.id!, projectPath, task, model);\n      console.log(\"Agent execution started with run ID:\", executionRunId);\n      setRunId(executionRunId);\n      \n      // Track agent execution start\n      trackEvent.agentStarted({\n        agent_type: agent.name || 'custom',\n        agent_name: agent.name,\n        has_custom_prompt: task !== agent.default_task\n      });\n      \n      // Track feature adoption\n      agentFeatureTracking.trackUsage();\n      \n      // Set up event listeners with run ID isolation\n      const outputUnlisten = await listen<string>(`agent-output:${executionRunId}`, (event) => {\n        try {\n          // Store raw JSONL\n          setRawJsonlOutput(prev => [...prev, event.payload]);\n          \n          // Parse and display\n          const message = JSON.parse(event.payload) as ClaudeStreamMessage;\n          setMessages(prev => [...prev, message]);\n        } catch (err) {\n          console.error(\"Failed to parse message:\", err, event.payload);\n        }\n      });\n\n      const errorUnlisten = await listen<string>(`agent-error:${executionRunId}`, (event) => {\n        console.error(\"Agent error:\", event.payload);\n        setError(event.payload);\n        \n        // Track agent error\n        trackEvent.agentError({\n          error_type: 'runtime_error',\n          error_stage: 'execution',\n          retry_count: 0,\n          agent_type: agent.name || 'custom'\n        });\n      });\n\n      const completeUnlisten = await listen<boolean>(`agent-complete:${executionRunId}`, (event) => {\n        setIsRunning(false);\n        const duration = executionStartTime ? Date.now() - executionStartTime : undefined;\n        setExecutionStartTime(null);\n        if (!event.payload) {\n          setError(\"Agent execution failed\");\n          // Update tab status to error\n          if (tabId) {\n            updateTabStatus(tabId, 'error');\n          }\n          // Track both the old event for compatibility and the new error event\n          trackEvent.agentExecuted(agent.name || 'custom', false, agent.name, duration);\n          trackEvent.agentError({\n            error_type: 'execution_failed',\n            error_stage: 'completion',\n            retry_count: 0,\n            agent_type: agent.name || 'custom'\n          });\n        } else {\n          // Update tab status to complete on success\n          if (tabId) {\n            updateTabStatus(tabId, 'complete');\n          }\n          trackEvent.agentExecuted(agent.name || 'custom', true, agent.name, duration);\n        }\n      });\n\n      const cancelUnlisten = await listen<boolean>(`agent-cancelled:${executionRunId}`, () => {\n        setIsRunning(false);\n        setExecutionStartTime(null);\n        setError(\"Agent execution was cancelled\");\n        // Update tab status to idle when cancelled\n        if (tabId) {\n          updateTabStatus(tabId, 'idle');\n        }\n      });\n\n      unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];\n    } catch (err) {\n      console.error(\"Failed to execute agent:\", err);\n      setIsRunning(false);\n      setExecutionStartTime(null);\n      setRunId(null);\n      // Update tab status to error\n      if (tabId) {\n        updateTabStatus(tabId, 'error');\n      }\n      // Show error in messages\n      setMessages(prev => [...prev, {\n        type: \"result\",\n        subtype: \"error\",\n        is_error: true,\n        result: `Failed to execute agent: ${err instanceof Error ? err.message : 'Unknown error'}`,\n        duration_ms: 0,\n        usage: {\n          input_tokens: 0,\n          output_tokens: 0\n        }\n      }]);\n    }\n  };\n\n  const handleStop = async () => {\n    try {\n      if (!runId) {\n        console.error(\"No run ID available to stop\");\n        return;\n      }\n\n      // Call the API to kill the agent session\n      const success = await api.killAgentSession(runId);\n\n      if (success) {\n        console.log(`Successfully stopped agent session ${runId}`);\n      } else {\n        console.warn(`Failed to stop agent session ${runId} - it may have already finished`);\n      }\n\n      // Update UI state\n      setIsRunning(false);\n      setExecutionStartTime(null);\n    } catch (err) {\n      console.error(\"Failed to stop agent:\", err);\n    }\n  };\n\n  const handleCompositionStart = () => {\n    isIMEComposingRef.current = true;\n  };\n\n  const handleCompositionEnd = () => {\n    setTimeout(() => {\n      isIMEComposingRef.current = false;\n    }, 0);\n  };\n\n  const handleBackWithConfirmation = () => {\n    if (isRunning) {\n      // Show confirmation dialog before navigating away during execution\n      const shouldLeave = window.confirm(\n        \"An agent is currently running. If you navigate away, the agent will continue running in the background. You can view running sessions in the 'Running Sessions' tab within CC Agents.\\n\\nDo you want to continue?\"\n      );\n      if (!shouldLeave) {\n        return;\n      }\n    }\n    \n    // Clean up listeners but don't stop the actual agent process\n    unlistenRefs.current.forEach(unlisten => unlisten());\n    unlistenRefs.current = [];\n    \n    // Navigate back\n    onBack();\n  };\n\n  const handleCopyAsJsonl = async () => {\n    const jsonl = rawJsonlOutput.join('\\n');\n    await navigator.clipboard.writeText(jsonl);\n    setCopyPopoverOpen(false);\n  };\n\n  const handleCopyAsMarkdown = async () => {\n    let markdown = `# Agent Execution: ${agent.name}\\n\\n`;\n    markdown += `**Task:** ${task}\\n`;\n    markdown += `**Model:** ${model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\\n`;\n    markdown += `**Date:** ${new Date().toISOString()}\\n\\n`;\n    markdown += `---\\n\\n`;\n\n    for (const msg of messages) {\n      if (msg.type === \"system\" && msg.subtype === \"init\") {\n        markdown += `## System Initialization\\n\\n`;\n        markdown += `- Session ID: \\`${msg.session_id || 'N/A'}\\`\\n`;\n        markdown += `- Model: \\`${msg.model || 'default'}\\`\\n`;\n        if (msg.cwd) markdown += `- Working Directory: \\`${msg.cwd}\\`\\n`;\n        if (msg.tools?.length) markdown += `- Tools: ${msg.tools.join(', ')}\\n`;\n        markdown += `\\n`;\n      } else if (msg.type === \"assistant\" && msg.message) {\n        markdown += `## Assistant\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_use\") {\n            markdown += `### Tool: ${content.name}\\n\\n`;\n            markdown += `\\`\\`\\`json\\n${JSON.stringify(content.input, null, 2)}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n        if (msg.message.usage) {\n          markdown += `*Tokens: ${msg.message.usage.input_tokens} in, ${msg.message.usage.output_tokens} out*\\n\\n`;\n        }\n      } else if (msg.type === \"user\" && msg.message) {\n        markdown += `## User\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_result\") {\n            markdown += `### Tool Result\\n\\n`;\n            markdown += `\\`\\`\\`\\n${content.content}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n      } else if (msg.type === \"result\") {\n        markdown += `## Execution Result\\n\\n`;\n        if (msg.result) {\n          markdown += `${msg.result}\\n\\n`;\n        }\n        if (msg.error) {\n          markdown += `**Error:** ${msg.error}\\n\\n`;\n        }\n        if (msg.cost_usd !== undefined) {\n          markdown += `- **Cost:** $${msg.cost_usd.toFixed(4)} USD\\n`;\n        }\n        if (msg.duration_ms !== undefined) {\n          markdown += `- **Duration:** ${(msg.duration_ms / 1000).toFixed(2)}s\\n`;\n        }\n        if (msg.num_turns !== undefined) {\n          markdown += `- **Turns:** ${msg.num_turns}\\n`;\n        }\n        if (msg.usage) {\n          const total = msg.usage.input_tokens + msg.usage.output_tokens;\n          markdown += `- **Total Tokens:** ${total} (${msg.usage.input_tokens} in, ${msg.usage.output_tokens} out)\\n`;\n        }\n      }\n    }\n\n    await navigator.clipboard.writeText(markdown);\n    setCopyPopoverOpen(false);\n  };\n\n\n  return (\n    <div className={cn(\"flex flex-col h-full bg-background\", className)}>\n      {/* Fixed container that takes full height */}\n      <div className=\"h-full flex flex-col bg-background\">\n        {/* Header */}\n        <div className=\"p-6 border-b border-border\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-3\">\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                onClick={handleBackWithConfirmation}\n                className=\"h-9 w-9 -ml-2\"\n                title=\"Back\"\n              >\n                <ArrowLeft className=\"h-4 w-4\" />\n              </Button>\n              <div>\n                <h1 className=\"text-heading-1\">{agent.name}</h1>\n                <p className=\"mt-1 text-body-small text-muted-foreground\">\n                  {isRunning ? 'Running' : messages.length > 0 ? 'Complete' : 'Ready'} • {model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n                </p>\n              </div>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              {messages.length > 0 && (\n                <Button\n                  variant=\"outline\"\n                  size=\"default\"\n                  onClick={() => setIsFullscreenModalOpen(true)}\n                >\n                  <Maximize2 className=\"h-4 w-4 mr-2\" />\n                  Fullscreen\n                </Button>\n              )}\n            </div>\n          </div>\n        </div>\n        \n        {/* Configuration Section */}\n        <div className=\"p-6 border-b border-border\">\n          <div className=\"max-w-4xl mx-auto space-y-4\">\n            {/* Error display */}\n            {error && (\n              <motion.div\n                initial={{ opacity: 0, y: 4 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: -4 }}\n                transition={{ duration: 0.15 }}\n                className=\"p-3 rounded-md bg-destructive/10 border border-destructive/50 flex items-center gap-2\"\n              >\n                <AlertCircle className=\"h-3.5 w-3.5 text-destructive flex-shrink-0\" />\n                <span className=\"text-caption text-destructive\">{error}</span>\n              </motion.div>\n            )}\n\n            {/* Model Selection */}\n            <div className=\"space-y-3\">\n              <Label className=\"text-caption text-muted-foreground\">Model Selection</Label>\n              <div className=\"flex gap-2\">\n                <motion.button\n                  type=\"button\"\n                  onClick={() => !isRunning && setModel(\"sonnet\")}\n                  whileTap={{ scale: 0.97 }}\n                  transition={{ duration: 0.15 }}\n                  className={cn(\n                    \"flex-1 px-4 py-3 rounded-md border transition-all\",\n                    model === \"sonnet\" \n                      ? \"border-primary bg-primary/10 text-primary\" \n                      : \"border-border hover:border-primary/50 hover:bg-accent\",\n                    isRunning && \"opacity-50 cursor-not-allowed\"\n                  )}\n                  disabled={isRunning}\n                >\n                  <div className=\"flex items-center gap-3\">\n                    <div className={cn(\n                      \"w-4 h-4 rounded-full border-2 flex items-center justify-center\",\n                      model === \"sonnet\" ? \"border-primary\" : \"border-muted-foreground\"\n                    )}>\n                      {model === \"sonnet\" && (\n                        <div className=\"w-2 h-2 rounded-full bg-primary\" />\n                      )}\n                    </div>\n                    <div className=\"text-left\">\n                      <div className=\"text-body-small font-medium\">Claude 4 Sonnet</div>\n                      <div className=\"text-caption text-muted-foreground\">Faster, efficient</div>\n                    </div>\n                  </div>\n                </motion.button>\n                \n                <motion.button\n                  type=\"button\"\n                  onClick={() => !isRunning && setModel(\"opus\")}\n                  whileTap={{ scale: 0.97 }}\n                  transition={{ duration: 0.15 }}\n                  className={cn(\n                    \"flex-1 px-4 py-3 rounded-md border transition-all\",\n                    model === \"opus\" \n                      ? \"border-primary bg-primary/10 text-primary\" \n                      : \"border-border hover:border-primary/50 hover:bg-accent\",\n                    isRunning && \"opacity-50 cursor-not-allowed\"\n                  )}\n                  disabled={isRunning}\n                >\n                  <div className=\"flex items-center gap-3\">\n                    <div className={cn(\n                      \"w-4 h-4 rounded-full border-2 flex items-center justify-center\",\n                      model === \"opus\" ? \"border-primary\" : \"border-muted-foreground\"\n                    )}>\n                      {model === \"opus\" && (\n                        <div className=\"w-2 h-2 rounded-full bg-primary\" />\n                      )}\n                    </div>\n                    <div className=\"text-left\">\n                      <div className=\"text-body-small font-medium\">Claude 4 Opus</div>\n                      <div className=\"text-caption text-muted-foreground\">More capable</div>\n                    </div>\n                  </div>\n                </motion.button>\n              </div>\n            </div>\n\n            {/* Task Input */}\n            <div className=\"space-y-3\">\n              <div className=\"flex items-center justify-between\">\n                <Label className=\"text-caption text-muted-foreground\">Task Description</Label>\n                {projectPath && (\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={handleOpenHooksDialog}\n                    disabled={isRunning}\n                    className=\"h-8 -mr-2\"\n                  >\n                    <Settings2 className=\"h-3.5 w-3.5 mr-1.5\" />\n                    <span className=\"text-caption\">Configure Hooks</span>\n                  </Button>\n                )}\n              </div>\n              <div className=\"flex gap-2\">\n                <Input\n                  value={task}\n                  onChange={(e) => setTask(e.target.value)}\n                  placeholder=\"What would you like the agent to do?\"\n                  disabled={isRunning}\n                  className=\"flex-1 h-9\"\n                  onKeyDown={(e) => {\n                    if (e.key === \"Enter\" && !isRunning && projectPath && task.trim()) {\n                      if (e.nativeEvent.isComposing || isIMEComposingRef.current) {\n                        return;\n                      }\n                      handleExecute();\n                    }\n                  }}\n                  onCompositionStart={handleCompositionStart}\n                  onCompositionEnd={handleCompositionEnd}\n                />\n                <motion.div\n                  whileTap={{ scale: 0.97 }}\n                  transition={{ duration: 0.15 }}\n                >\n                  <Button\n                    onClick={isRunning ? handleStop : handleExecute}\n                    disabled={!projectPath || !task.trim()}\n                    variant={isRunning ? \"destructive\" : \"default\"}\n                    size=\"default\"\n                  >\n                    {isRunning ? (\n                      <>\n                        <StopCircle className=\"mr-2 h-4 w-4\" />\n                        Stop\n                      </>\n                    ) : (\n                      <>\n                        <Play className=\"mr-2 h-4 w-4\" />\n                        Execute\n                      </>\n                    )}\n                  </Button>\n                </motion.div>\n              </div>\n              {projectPath && (\n                <p className=\"text-caption text-muted-foreground\">\n                  Working in: <span className=\"font-mono\">{projectPath.split('/').pop() || projectPath}</span>\n                </p>\n              )}\n            </div>\n          </div>\n        </div>\n\n        {/* Scrollable Output Display */}\n        <div className=\"flex-1 overflow-hidden\">\n          <div className=\"w-full max-w-5xl mx-auto h-full\">\n            <div \n              ref={scrollContainerRef}\n              className=\"h-full overflow-y-auto p-6 space-y-8\"\n              onScroll={() => {\n                // Mark that user has scrolled manually\n                if (!hasUserScrolled) {\n                  setHasUserScrolled(true);\n                }\n                \n                // If user scrolls back to bottom, re-enable auto-scroll\n                if (isAtBottom()) {\n                  setHasUserScrolled(false);\n                }\n              }}\n            >\n              <div ref={messagesContainerRef}>\n              {messages.length === 0 && !isRunning && (\n                <div className=\"flex flex-col items-center justify-center h-full text-center\">\n                  <Terminal className=\"h-16 w-16 text-muted-foreground mb-4\" />\n                  <h3 className=\"text-lg font-medium mb-2\">Ready to Execute</h3>\n                  <p className=\"text-sm text-muted-foreground\">\n                    Enter a task to run the agent\n                  </p>\n                </div>\n              )}\n\n              {isRunning && messages.length === 0 && (\n                <div className=\"flex items-center justify-center h-full\">\n                  <div className=\"flex items-center gap-3\">\n                    <Loader2 className=\"h-6 w-6 animate-spin\" />\n                    <span className=\"text-sm text-muted-foreground\">Initializing agent...</span>\n                  </div>\n                </div>\n              )}\n\n              <div\n                className=\"relative w-full\"\n                style={{ height: `${rowVirtualizer.getTotalSize()}px` }}\n              >\n                <AnimatePresence>\n                  {rowVirtualizer.getVirtualItems().map((virtualItem) => {\n                    const message = displayableMessages[virtualItem.index];\n                    return (\n                      <motion.div\n                        key={virtualItem.key}\n                        data-index={virtualItem.index}\n                        ref={(el) => el && rowVirtualizer.measureElement(el)}\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        transition={{ duration: 0.2 }}\n                        className=\"absolute inset-x-4 pb-4\"\n                        style={{ top: virtualItem.start }}\n                      >\n                        <ErrorBoundary>\n                          <StreamMessage message={message} streamMessages={messages} />\n                        </ErrorBoundary>\n                      </motion.div>\n                    );\n                  })}\n                </AnimatePresence>\n              </div>\n              \n              <div ref={messagesEndRef} />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Floating Execution Control Bar */}\n      <ExecutionControlBar\n        isExecuting={isRunning}\n        onStop={handleStop}\n        totalTokens={totalTokens}\n        elapsedTime={elapsedTime}\n      />\n\n      {/* Fullscreen Modal */}\n      {isFullscreenModalOpen && (\n        <div className=\"fixed inset-0 z-50 bg-background flex flex-col\">\n          {/* Modal Header */}\n          <div className=\"flex items-center justify-between p-4 border-b border-border\">\n            <div className=\"flex items-center gap-2\">\n              <h2 className=\"text-lg font-semibold\">{agent.name} - Output</h2>\n              {isRunning && (\n                <div className=\"flex items-center gap-1\">\n                  <div className=\"w-2 h-2 bg-green-500 rounded-full animate-pulse\"></div>\n                  <span className=\"text-xs text-green-600 font-medium\">Running</span>\n                </div>\n              )}\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <Popover\n                trigger={\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"flex items-center gap-2\"\n                  >\n                    <Copy className=\"h-4 w-4\" />\n                    Copy Output\n                    <ChevronDown className=\"h-3 w-3\" />\n                  </Button>\n                }\n                content={\n                  <div className=\"w-44 p-1\">\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"w-full justify-start\"\n                      onClick={handleCopyAsJsonl}\n                    >\n                      Copy as JSONL\n                    </Button>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"w-full justify-start\"\n                      onClick={handleCopyAsMarkdown}\n                    >\n                      Copy as Markdown\n                    </Button>\n                  </div>\n                }\n                open={copyPopoverOpen}\n                onOpenChange={setCopyPopoverOpen}\n                align=\"end\"\n              />\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => setIsFullscreenModalOpen(false)}\n                className=\"flex items-center gap-2\"\n              >\n                <X className=\"h-4 w-4\" />\n                Close\n              </Button>\n            </div>\n          </div>\n\n          {/* Modal Content */}\n          <div className=\"flex-1 overflow-hidden p-6\">\n            <div \n              ref={fullscreenScrollRef}\n              className=\"h-full overflow-y-auto space-y-8\"\n              onScroll={() => {\n                // Mark that user has scrolled manually\n                if (!hasUserScrolled) {\n                  setHasUserScrolled(true);\n                }\n                \n                // If user scrolls back to bottom, re-enable auto-scroll\n                if (isAtBottom()) {\n                  setHasUserScrolled(false);\n                }\n              }}\n            >\n              {messages.length === 0 && !isRunning && (\n                <div className=\"flex flex-col items-center justify-center h-full text-center\">\n                  <Terminal className=\"h-16 w-16 text-muted-foreground mb-4\" />\n                  <h3 className=\"text-lg font-medium mb-2\">Ready to Execute</h3>\n                  <p className=\"text-sm text-muted-foreground\">\n                    Enter a task to run the agent\n                  </p>\n                </div>\n              )}\n\n              {isRunning && messages.length === 0 && (\n                <div className=\"flex items-center justify-center h-full\">\n                  <div className=\"flex items-center gap-3\">\n                    <Loader2 className=\"h-6 w-6 animate-spin\" />\n                    <span className=\"text-sm text-muted-foreground\">Initializing agent...</span>\n                  </div>\n                </div>\n              )}\n\n              <div\n                className=\"relative w-full max-w-5xl mx-auto\"\n                style={{ height: `${fullscreenRowVirtualizer.getTotalSize()}px` }}\n              >\n                <AnimatePresence>\n                  {fullscreenRowVirtualizer.getVirtualItems().map((virtualItem) => {\n                    const message = displayableMessages[virtualItem.index];\n                    return (\n                      <motion.div\n                        key={virtualItem.key}\n                        data-index={virtualItem.index}\n                        ref={(el) => el && fullscreenRowVirtualizer.measureElement(el)}\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        transition={{ duration: 0.2 }}\n                        className=\"absolute inset-x-4 pb-4\"\n                        style={{ top: virtualItem.start }}\n                      >\n                        <ErrorBoundary>\n                          <StreamMessage message={message} streamMessages={messages} />\n                        </ErrorBoundary>\n                      </motion.div>\n                    );\n                  })}\n                </AnimatePresence>\n              </div>\n              \n              <div ref={fullscreenMessagesEndRef} />\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Hooks Configuration Dialog */}\n      <Dialog \n        open={isHooksDialogOpen} \n        onOpenChange={setIsHooksDialogOpen}\n      >\n        <DialogContent className=\"max-w-4xl max-h-[85vh] overflow-hidden flex flex-col gap-0 p-0\">\n          <div className=\"px-6 py-4 border-b border-border\">\n            <DialogTitle className=\"text-heading-2\">Configure Hooks</DialogTitle>\n            <DialogDescription className=\"mt-1 text-body-small text-muted-foreground\">\n              Configure hooks that run before, during, and after tool executions\n            </DialogDescription>\n          </div>\n          \n          <Tabs value={activeHooksTab} onValueChange={setActiveHooksTab} className=\"flex-1 flex flex-col overflow-hidden\">\n            <div className=\"px-6 pt-4\">\n              <TabsList className=\"grid w-full grid-cols-2 h-auto p-1\">\n                <TabsTrigger value=\"project\" className=\"py-2.5 px-3 text-body-small\">\n                  Project Settings\n                </TabsTrigger>\n                <TabsTrigger value=\"local\" className=\"py-2.5 px-3 text-body-small\">\n                  Local Settings\n                </TabsTrigger>\n              </TabsList>\n            </div>\n            \n            <TabsContent value=\"project\" className=\"flex-1 overflow-auto px-6 pb-6 mt-0\">\n              <div className=\"space-y-4 pt-4\">\n                <div className=\"rounded-lg bg-muted/50 p-3\">\n                  <p className=\"text-caption text-muted-foreground\">\n                    Project hooks are stored in <code className=\"font-mono text-xs bg-background px-1.5 py-0.5 rounded\">.claude/settings.json</code> and \n                    are committed to version control, allowing team members to share configurations.\n                  </p>\n                </div>\n                <HooksEditor\n                  projectPath={projectPath}\n                  scope=\"project\"\n                  className=\"border-0\"\n                />\n              </div>\n            </TabsContent>\n            \n            <TabsContent value=\"local\" className=\"flex-1 overflow-auto px-6 pb-6 mt-0\">\n              <div className=\"space-y-4 pt-4\">\n                <div className=\"rounded-lg bg-muted/50 p-3\">\n                  <p className=\"text-caption text-muted-foreground\">\n                    Local hooks are stored in <code className=\"font-mono text-xs bg-background px-1.5 py-0.5 rounded\">.claude/settings.local.json</code> and \n                    are not committed to version control, perfect for personal preferences.\n                  </p>\n                </div>\n                <HooksEditor\n                  projectPath={projectPath}\n                  scope=\"local\"\n                  className=\"border-0\"\n                />\n              </div>\n            </TabsContent>\n          </Tabs>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/AgentExecutionDemo.tsx",
    "content": "import React from \"react\";\nimport { StreamMessage } from \"./StreamMessage\";\nimport type { ClaudeStreamMessage } from \"./AgentExecution\";\n\n/**\n * Demo component showing all the different message types and tools\n */\nexport const AgentExecutionDemo: React.FC = () => {\n  // Sample messages based on the provided JSONL session\n  const messages: ClaudeStreamMessage[] = [\n    // Skip meta message (should not render)\n    {\n      type: \"user\",\n      isMeta: true,\n      message: { content: [] },\n      timestamp: \"2025-06-11T14:08:53.771Z\"\n    },\n    \n    // Summary message\n    {\n      leafUuid: \"3c5ecb4f-c1f0-40c2-a357-ab7642ad28b8\",\n      summary: \"JSONL Viewer Model Configuration and Setup\",\n      type: \"summary\" as any\n    },\n    \n    // Assistant with Edit tool\n    {\n      type: \"assistant\",\n      message: {\n        content: [{\n          type: \"tool_use\",\n          name: \"Edit\",\n          input: {\n            file_path: \"/Users/mufeedvh/dev/jsonl-viewer/script.js\",\n            new_string: \"reader.onerror = () => reject(new Error('Failed to read file'));\",\n            old_string: \"reader.onerror = e => reject(new Error('Failed to read file'));\"\n          }\n        }],\n        usage: { input_tokens: 4, output_tokens: 158 }\n      }\n    },\n    \n    // User with Edit tool result\n    {\n      type: \"user\",\n      message: {\n        content: [{\n          type: \"tool_result\",\n          content: `The file /Users/mufeedvh/dev/jsonl-viewer/script.js has been updated. Here's the result of running \\`cat -n\\` on a snippet of the edited file:\n   220       readFileAsText(file) {\n   221         return new Promise((resolve, reject) => {\n   222           const reader = new FileReader();\n   223           reader.onload = e => resolve(e.target.result);\n   224           reader.onerror = () => reject(new Error('Failed to read file'));\n   225           reader.readAsText(file);\n   226         });\n   227       }\n   228`\n        }]\n      }\n    },\n    \n    // Assistant with MCP tool\n    {\n      type: \"assistant\",\n      message: {\n        content: [{\n          type: \"tool_use\",\n          name: \"mcp__ide__getDiagnostics\",\n          input: {}\n        }],\n        usage: { input_tokens: 4, output_tokens: 37 }\n      }\n    },\n    \n    // User with empty tool result\n    {\n      type: \"user\",\n      message: {\n        content: [{\n          type: \"tool_result\",\n          content: \"\"\n        }]\n      }\n    },\n    \n    // Assistant with Write tool (large content)\n    {\n      type: \"assistant\",\n      message: {\n        content: [{\n          type: \"tool_use\",\n          name: \"Write\",\n          input: {\n            file_path: \"/Users/mufeedvh/dev/jsonl-viewer/styles.css\",\n            content: `/* Reset and Base Styles */\n* {\n    margin: 0;\n    padding: 0;\n    box-sizing: border-box;\n}\n\nbody {\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n    line-height: 1.6;\n    color: #333;\n    background: #f8fafc;\n    min-height: 100vh;\n}\n\n/* Container */\n.container {\n    max-width: 1200px;\n    margin: 0 auto;\n    padding: 20px;\n}\n\n/* Header */\n.header {\n    text-align: center;\n    margin-bottom: 40px;\n}\n\n.header h1 {\n    font-size: 2.5rem;\n    font-weight: 700;\n    color: #1a202c;\n    margin-bottom: 8px;\n}\n\n.header p {\n    color: #718096;\n    font-size: 1.1rem;\n}\n\n/* Input Section */\n.input-section {\n    display: grid;\n    gap: 20px;\n    margin-bottom: 30px;\n}\n\n/* Drop Zone */\n.drop-zone {\n    border: 2px dashed #cbd5e0;\n    border-radius: 12px;\n    padding: 40px 20px;\n    text-align: center;\n    cursor: pointer;\n    transition: all 0.3s ease;\n    background: white;\n    position: relative;\n}\n\n.drop-zone:hover,\n.drop-zone.drag-over {\n    border-color: #4299e1;\n    background: #ebf8ff;\n    transform: translateY(-2px);\n    box-shadow: 0 8px 25px rgba(66, 153, 225, 0.1);\n}\n\n/* ... many more lines of CSS ... */\n/* This content is over 1000 characters so it should show the maximize button */\n` + '\\n'.repeat(100) + '/* End of very long CSS file */'\n          }\n        }]\n      }\n    }\n  ];\n\n  return (\n    <div className=\"max-w-4xl mx-auto p-8 space-y-4\">\n      <h1 className=\"text-2xl font-bold mb-6\">Agent Execution Demo</h1>\n      \n      {messages.map((message, idx) => (\n        <StreamMessage key={idx} message={message} streamMessages={messages} />\n      ))}\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/AgentRunOutputViewer.tsx",
    "content": "import { useState, useEffect, useRef, useMemo } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { \n  Maximize2, \n  Minimize2, \n  Copy, \n  RefreshCw, \n  RotateCcw, \n  ChevronDown,\n  Bot,\n  Clock,\n  Hash,\n  DollarSign,\n  StopCircle\n} from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Toast, ToastContainer } from '@/components/ui/toast';\nimport { Popover } from '@/components/ui/popover';\nimport { api, type AgentRunWithMetrics } from '@/lib/api';\nimport { useOutputCache } from '@/lib/outputCache';\nimport { listen, type UnlistenFn } from '@tauri-apps/api/event';\nimport { StreamMessage } from './StreamMessage';\nimport { ErrorBoundary } from './ErrorBoundary';\nimport { formatISOTimestamp } from '@/lib/date-utils';\nimport { AGENT_ICONS } from './CCAgents';\nimport type { ClaudeStreamMessage } from './AgentExecution';\nimport { useTabState } from '@/hooks/useTabState';\n\ninterface AgentRunOutputViewerProps {\n  /**\n   * The agent run ID to display\n   */\n  agentRunId: string;\n  /**\n   * Tab ID for this agent run\n   */\n  tabId: string;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * AgentRunOutputViewer - Modal component for viewing agent execution output\n * \n * @example\n * <AgentRunOutputViewer\n *   run={agentRun}\n *   onClose={() => setSelectedRun(null)}\n * />\n */\nexport function AgentRunOutputViewer({ \n  agentRunId, \n  tabId,\n  className \n}: AgentRunOutputViewerProps) {\n  const { updateTabTitle, updateTabStatus } = useTabState();\n  const [run, setRun] = useState<AgentRunWithMetrics | null>(null);\n  const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);\n  const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [isFullscreen, setIsFullscreen] = useState(false);\n  const [refreshing, setRefreshing] = useState(false);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);\n  const [hasUserScrolled, setHasUserScrolled] = useState(false);\n  \n  // Track whether we're in the initial load phase\n  const isInitialLoadRef = useRef(true);\n  const hasSetupListenersRef = useRef(false);\n  \n  const scrollAreaRef = useRef<HTMLDivElement>(null);\n  const outputEndRef = useRef<HTMLDivElement>(null);\n  const fullscreenScrollRef = useRef<HTMLDivElement>(null);\n  const fullscreenMessagesEndRef = useRef<HTMLDivElement>(null);\n  const unlistenRefs = useRef<UnlistenFn[]>([]);\n  const { getCachedOutput, setCachedOutput } = useOutputCache();\n\n  // Auto-scroll logic\n  const isAtBottom = () => {\n    const container = isFullscreen ? fullscreenScrollRef.current : scrollAreaRef.current;\n    if (container) {\n      const { scrollTop, scrollHeight, clientHeight } = container;\n      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;\n      return distanceFromBottom < 1;\n    }\n    return true;\n  };\n\n  const scrollToBottom = () => {\n    if (!hasUserScrolled) {\n      const endRef = isFullscreen ? fullscreenMessagesEndRef.current : outputEndRef.current;\n      if (endRef) {\n        endRef.scrollIntoView({ behavior: 'smooth' });\n      }\n    }\n  };\n\n  // Load agent run on mount\n  useEffect(() => {\n    const loadAgentRun = async () => {\n      try {\n        setLoading(true);\n        const agentRun = await api.getAgentRun(parseInt(agentRunId));\n        setRun(agentRun);\n        updateTabTitle(tabId, `Agent: ${agentRun.agent_name || 'Unknown'}`);\n        updateTabStatus(tabId, agentRun.status === 'running' ? 'running' : agentRun.status === 'failed' ? 'error' : 'complete');\n      } catch (error) {\n        console.error('Failed to load agent run:', error);\n        updateTabStatus(tabId, 'error');\n      } finally {\n        setLoading(false);\n      }\n    };\n    \n    if (agentRunId) {\n      loadAgentRun();\n    }\n  }, [agentRunId, tabId, updateTabTitle, updateTabStatus]);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      unlistenRefs.current = [];\n      hasSetupListenersRef.current = false;\n    };\n  }, []);\n\n  // Auto-scroll when messages change\n  useEffect(() => {\n    const shouldAutoScroll = !hasUserScrolled || isAtBottom();\n    if (shouldAutoScroll) {\n      scrollToBottom();\n    }\n  }, [messages, hasUserScrolled, isFullscreen]);\n\n  const loadOutput = async (skipCache = false) => {\n    if (!run?.id) return;\n\n    console.log('[AgentRunOutputViewer] Loading output for run:', {\n      runId: run.id,\n      status: run.status,\n      sessionId: run.session_id,\n      skipCache\n    });\n\n    try {\n      // Check cache first if not skipping cache\n      if (!skipCache) {\n        const cached = getCachedOutput(run.id);\n        if (cached) {\n          console.log('[AgentRunOutputViewer] Found cached output');\n          const cachedJsonlLines = cached.output.split('\\n').filter(line => line.trim());\n          setRawJsonlOutput(cachedJsonlLines);\n          setMessages(cached.messages);\n          // If cache is recent (less than 5 seconds old) and session isn't running, use cache only\n          if (Date.now() - cached.lastUpdated < 5000 && run.status !== 'running') {\n            console.log('[AgentRunOutputViewer] Using recent cache, skipping refresh');\n            return;\n          }\n        }\n      }\n\n      setLoading(true);\n\n      // If we have a session_id, try to load from JSONL file first\n      if (run.session_id && run.session_id !== '') {\n        console.log('[AgentRunOutputViewer] Attempting to load from JSONL with session_id:', run.session_id);\n        try {\n          const history = await api.loadAgentSessionHistory(run.session_id);\n          console.log('[AgentRunOutputViewer] Successfully loaded JSONL history:', history.length, 'messages');\n          \n          // Convert history to messages format\n          const loadedMessages: ClaudeStreamMessage[] = history.map(entry => ({\n            ...entry,\n            type: entry.type || \"assistant\"\n          }));\n          \n          setMessages(loadedMessages);\n          setRawJsonlOutput(history.map(h => JSON.stringify(h)));\n          \n          // Update cache\n          setCachedOutput(run.id, {\n            output: history.map(h => JSON.stringify(h)).join('\\n'),\n            messages: loadedMessages,\n            lastUpdated: Date.now(),\n            status: run.status\n          });\n          \n          // Set up live event listeners for running sessions\n          if (run.status === 'running') {\n            console.log('[AgentRunOutputViewer] Setting up live listeners for running session');\n            setupLiveEventListeners();\n            \n            try {\n              await api.streamSessionOutput(run.id);\n            } catch (streamError) {\n              console.warn('[AgentRunOutputViewer] Failed to start streaming, will poll instead:', streamError);\n            }\n          }\n          \n          return;\n        } catch (err) {\n          console.warn('[AgentRunOutputViewer] Failed to load from JSONL:', err);\n          console.warn('[AgentRunOutputViewer] Falling back to regular output method');\n        }\n      } else {\n        console.log('[AgentRunOutputViewer] No session_id available, using fallback method');\n      }\n\n      // Fallback to the original method if JSONL loading fails or no session_id\n      console.log('[AgentRunOutputViewer] Using getSessionOutput fallback');\n      const rawOutput = await api.getSessionOutput(run.id);\n      console.log('[AgentRunOutputViewer] Received raw output:', rawOutput.length, 'characters');\n      \n      // Parse JSONL output into messages\n      const jsonlLines = rawOutput.split('\\n').filter(line => line.trim());\n      setRawJsonlOutput(jsonlLines);\n      \n      const parsedMessages: ClaudeStreamMessage[] = [];\n      for (const line of jsonlLines) {\n        try {\n          const message = JSON.parse(line) as ClaudeStreamMessage;\n          parsedMessages.push(message);\n        } catch (err) {\n          console.error(\"[AgentRunOutputViewer] Failed to parse message:\", err, line);\n        }\n      }\n      console.log('[AgentRunOutputViewer] Parsed', parsedMessages.length, 'messages from output');\n      setMessages(parsedMessages);\n      \n      // Update cache\n      setCachedOutput(run.id, {\n        output: rawOutput,\n        messages: parsedMessages,\n        lastUpdated: Date.now(),\n        status: run.status\n      });\n      \n      // Set up live event listeners for running sessions\n      if (run.status === 'running') {\n        console.log('[AgentRunOutputViewer] Setting up live listeners for running session (fallback)');\n        setupLiveEventListeners();\n        \n        try {\n          await api.streamSessionOutput(run.id);\n        } catch (streamError) {\n          console.warn('[AgentRunOutputViewer] Failed to start streaming (fallback), will poll instead:', streamError);\n        }\n      }\n    } catch (error) {\n      console.error('Failed to load agent output:', error);\n      setToast({ message: 'Failed to load agent output', type: 'error' });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  // Set up live event listeners for running sessions\n  const setupLiveEventListeners = async () => {\n    if (!run?.id || hasSetupListenersRef.current) return;\n    \n    try {\n      // Clean up existing listeners\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      unlistenRefs.current = [];\n\n      // Mark that we've set up listeners\n      hasSetupListenersRef.current = true;\n      \n      // After setup, we're no longer in initial load\n      // Small delay to ensure any pending messages are processed\n      setTimeout(() => {\n        isInitialLoadRef.current = false;\n      }, 100);\n\n      // Set up live event listeners with run ID isolation\n      const outputUnlisten = await listen<string>(`agent-output:${run!.id}`, (event) => {\n        try {\n          // Skip messages during initial load phase\n          if (isInitialLoadRef.current) {\n            console.log('[AgentRunOutputViewer] Skipping message during initial load');\n            return;\n          }\n          \n          // Store raw JSONL\n          setRawJsonlOutput(prev => [...prev, event.payload]);\n          \n          // Parse and display\n          const message = JSON.parse(event.payload) as ClaudeStreamMessage;\n          setMessages(prev => [...prev, message]);\n        } catch (err) {\n          console.error(\"[AgentRunOutputViewer] Failed to parse message:\", err, event.payload);\n        }\n      });\n\n      const errorUnlisten = await listen<string>(`agent-error:${run!.id}`, (event) => {\n        console.error(\"[AgentRunOutputViewer] Agent error:\", event.payload);\n        setToast({ message: event.payload, type: 'error' });\n      });\n\n      const completeUnlisten = await listen<boolean>(`agent-complete:${run!.id}`, () => {\n        setToast({ message: 'Agent execution completed', type: 'success' });\n        // Don't set status here as the parent component should handle it\n      });\n\n      const cancelUnlisten = await listen<boolean>(`agent-cancelled:${run!.id}`, () => {\n        setToast({ message: 'Agent execution was cancelled', type: 'error' });\n      });\n\n      unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];\n    } catch (error) {\n      console.error('[AgentRunOutputViewer] Failed to set up live event listeners:', error);\n    }\n  };\n\n  // Copy functionality\n  const handleCopyAsJsonl = async () => {\n    const jsonl = rawJsonlOutput.join('\\n');\n    await navigator.clipboard.writeText(jsonl);\n    setCopyPopoverOpen(false);\n    setToast({ message: 'Output copied as JSONL', type: 'success' });\n  };\n\n  const handleCopyAsMarkdown = async () => {\n    if (!run) return;\n    let markdown = `# Agent Execution: ${run.agent_name}\\n\\n`;\n    markdown += `**Task:** ${run.task}\\n`;\n    markdown += `**Model:** ${run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\\n`;\n    markdown += `**Date:** ${formatISOTimestamp(run.created_at)}\\n`;\n    if (run.metrics?.duration_ms) markdown += `**Duration:** ${(run.metrics.duration_ms / 1000).toFixed(2)}s\\n`;\n    if (run.metrics?.total_tokens) markdown += `**Total Tokens:** ${run.metrics.total_tokens}\\n`;\n    if (run.metrics?.cost_usd) markdown += `**Cost:** $${run.metrics.cost_usd.toFixed(4)} USD\\n`;\n    markdown += `\\n---\\n\\n`;\n\n    for (const msg of messages) {\n      if (msg.type === \"system\" && msg.subtype === \"init\") {\n        markdown += `## System Initialization\\n\\n`;\n        markdown += `- Session ID: \\`${msg.session_id || 'N/A'}\\`\\n`;\n        markdown += `- Model: \\`${msg.model || 'default'}\\`\\n`;\n        if (msg.cwd) markdown += `- Working Directory: \\`${msg.cwd}\\`\\n`;\n        if (msg.tools?.length) markdown += `- Tools: ${msg.tools.join(', ')}\\n`;\n        markdown += `\\n`;\n      } else if (msg.type === \"assistant\" && msg.message) {\n        markdown += `## Assistant\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_use\") {\n            markdown += `### Tool: ${content.name}\\n\\n`;\n            markdown += `\\`\\`\\`json\\n${JSON.stringify(content.input, null, 2)}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n        if (msg.message.usage) {\n          markdown += `*Tokens: ${msg.message.usage.input_tokens} in, ${msg.message.usage.output_tokens} out*\\n\\n`;\n        }\n      } else if (msg.type === \"user\" && msg.message) {\n        markdown += `## User\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_result\") {\n            markdown += `### Tool Result\\n\\n`;\n            markdown += `\\`\\`\\`\\n${content.content}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n      } else if (msg.type === \"result\") {\n        markdown += `## Execution Result\\n\\n`;\n        if (msg.result) {\n          markdown += `${msg.result}\\n\\n`;\n        }\n        if (msg.error) {\n          markdown += `**Error:** ${msg.error}\\n\\n`;\n        }\n      }\n    }\n\n    await navigator.clipboard.writeText(markdown);\n    setCopyPopoverOpen(false);\n    setToast({ message: 'Output copied as Markdown', type: 'success' });\n  };\n\n  const handleRefresh = async () => {\n    setRefreshing(true);\n    await loadOutput();\n    setRefreshing(false);\n  };\n\n  const handleStop = async () => {\n    if (!run?.id) {\n      console.error('[AgentRunOutputViewer] No run ID available to stop');\n      return;\n    }\n\n    try {\n      // Call the API to kill the agent session\n      const success = await api.killAgentSession(run.id);\n      \n      if (success) {\n        console.log(`[AgentRunOutputViewer] Successfully stopped agent session ${run.id}`);\n        setToast({ message: 'Agent execution stopped', type: 'success' });\n        \n        // Clean up listeners\n        unlistenRefs.current.forEach(unlisten => unlisten());\n        unlistenRefs.current = [];\n        hasSetupListenersRef.current = false;\n        \n        // Add a message indicating execution was stopped\n        const stopMessage: ClaudeStreamMessage = {\n          type: \"result\",\n          subtype: \"error\",\n          is_error: true,\n          result: \"Execution stopped by user\",\n          duration_ms: 0,\n          usage: {\n            input_tokens: 0,\n            output_tokens: 0\n          }\n        };\n        setMessages(prev => [...prev, stopMessage]);\n        \n        // Update the tab status\n        updateTabStatus(tabId, 'idle');\n        \n        // Refresh the output to get updated status\n        await loadOutput(true);\n      } else {\n        console.warn(`[AgentRunOutputViewer] Failed to stop agent session ${run.id} - it may have already finished`);\n        setToast({ message: 'Failed to stop agent - it may have already finished', type: 'error' });\n      }\n    } catch (err) {\n      console.error('[AgentRunOutputViewer] Failed to stop agent:', err);\n      setToast({ \n        message: `Failed to stop execution: ${err instanceof Error ? err.message : 'Unknown error'}`, \n        type: 'error' \n      });\n    }\n  };\n\n  const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {\n    const target = e.currentTarget;\n    const { scrollTop, scrollHeight, clientHeight } = target;\n    const distanceFromBottom = scrollHeight - scrollTop - clientHeight;\n    setHasUserScrolled(distanceFromBottom > 50);\n  };\n\n  // Load output on mount\n  useEffect(() => {\n    if (!run?.id) return;\n    \n    // Check cache immediately for instant display\n    const cached = getCachedOutput(run!.id);\n    if (cached) {\n      const cachedJsonlLines = cached.output.split('\\n').filter(line => line.trim());\n      setRawJsonlOutput(cachedJsonlLines);\n      setMessages(cached.messages);\n    }\n    \n    // Then load fresh data\n    loadOutput();\n  }, [run?.id]);\n\n  const displayableMessages = useMemo(() => {\n    return messages.filter((message) => {\n      if (message.isMeta && !message.leafUuid && !message.summary) return false;\n\n      if (message.type === \"user\" && message.message) {\n        if (message.isMeta) return false;\n\n        const msg = message.message;\n        if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) return false;\n\n        if (Array.isArray(msg.content)) {\n          let hasVisibleContent = false;\n          for (const content of msg.content) {\n            if (content.type === \"text\") { hasVisibleContent = true; break; }\n            if (content.type === \"tool_result\") {\n              // Check if this tool result will be displayed as a widget\n              let willBeSkipped = false;\n              if (content.tool_use_id) {\n                // Find the corresponding tool use\n                for (let i = messages.indexOf(message) - 1; i >= 0; i--) {\n                  const prevMsg = messages[i];\n                  if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {\n                    const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id);\n                    if (toolUse) {\n                      const toolName = toolUse.name?.toLowerCase();\n                      const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep'];\n                      if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {\n                        willBeSkipped = true;\n                      }\n                      break;\n                    }\n                  }\n                }\n              }\n              if (!willBeSkipped) { hasVisibleContent = true; break; }\n            }\n          }\n          if (!hasVisibleContent) return false;\n        }\n      }\n      return true;\n    });\n  }, [messages]);\n\n  const renderIcon = (iconName: string) => {\n    const Icon = AGENT_ICONS[iconName as keyof typeof AGENT_ICONS] || Bot;\n    return <Icon className=\"h-5 w-5\" />;\n  };\n\n  const formatDuration = (ms?: number) => {\n    if (!ms) return \"N/A\";\n    const seconds = Math.floor(ms / 1000);\n    if (seconds < 60) return `${seconds}s`;\n    const minutes = Math.floor(seconds / 60);\n    const remainingSeconds = seconds % 60;\n    return `${minutes}m ${remainingSeconds}s`;\n  };\n\n  const formatTokens = (tokens?: number) => {\n    if (!tokens) return \"0\";\n    if (tokens >= 1000) {\n      return `${(tokens / 1000).toFixed(1)}k`;\n    }\n    return tokens.toString();\n  };\n\n  if (!run) {\n    return (\n      <div className=\"flex items-center justify-center h-full\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4\"></div>\n          <p className=\"text-muted-foreground\">Loading agent run...</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className={`h-full flex flex-col ${className || ''}`}>\n        <Card className=\"h-full flex flex-col\">\n        <CardHeader className=\"pb-3\">\n          <div className=\"flex items-start justify-between gap-4\">\n              <div className=\"flex items-start gap-3 flex-1 min-w-0\">\n                <div className=\"mt-0.5\">\n                  {renderIcon(run.agent_icon)}\n                </div>\n                <div className=\"flex-1 min-w-0\">\n                  <CardTitle className=\"text-lg flex items-center gap-2\">\n                    {run.agent_name}\n                    {run.status === 'running' && (\n                      <div className=\"flex items-center gap-1\">\n                        <div className=\"w-2 h-2 bg-green-500 rounded-full animate-pulse\"></div>\n                        <span className=\"text-xs text-green-600 font-medium\">Running</span>\n                      </div>\n                    )}\n                  </CardTitle>\n                  <p className=\"text-sm text-muted-foreground mt-1 truncate\">\n                    {run.task}\n                  </p>\n                  <div className=\"flex items-center gap-3 text-xs text-muted-foreground mt-2\">\n                    <Badge variant=\"outline\" className=\"text-xs\">\n                      {run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n                    </Badge>\n                    <div className=\"flex items-center gap-1\">\n                      <Clock className=\"h-3 w-3\" />\n                      <span>{formatISOTimestamp(run.created_at)}</span>\n                    </div>\n                    {run.metrics?.duration_ms && (\n                      <span>{formatDuration(run.metrics.duration_ms)}</span>\n                    )}\n                    {run.metrics?.total_tokens && (\n                      <div className=\"flex items-center gap-1\">\n                        <Hash className=\"h-3 w-3\" />\n                        <span>{formatTokens(run.metrics.total_tokens)}</span>\n                      </div>\n                    )}\n                    {run.metrics?.cost_usd && (\n                      <div className=\"flex items-center gap-1\">\n                        <DollarSign className=\"h-3 w-3\" />\n                        <span>${run.metrics.cost_usd.toFixed(4)}</span>\n                      </div>\n                    )}\n                  </div>\n                </div>\n              </div>\n              <div className=\"flex items-center gap-1\">\n                <Popover\n                  trigger={\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"h-8 px-2\"\n                    >\n                      <Copy className=\"h-4 w-4 mr-1\" />\n                      Copy\n                      <ChevronDown className=\"h-3 w-3 ml-1\" />\n                    </Button>\n                  }\n                  content={\n                    <div className=\"w-44 p-1\">\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        className=\"w-full justify-start\"\n                        onClick={handleCopyAsJsonl}\n                      >\n                        Copy as JSONL\n                      </Button>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        className=\"w-full justify-start\"\n                        onClick={handleCopyAsMarkdown}\n                      >\n                        Copy as Markdown\n                      </Button>\n                    </div>\n                  }\n                  open={copyPopoverOpen}\n                  onOpenChange={setCopyPopoverOpen}\n                  align=\"end\"\n                />\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={() => setIsFullscreen(!isFullscreen)}\n                  title={isFullscreen ? \"Exit fullscreen\" : \"Enter fullscreen\"}\n                  className=\"h-8 px-2\"\n                >\n                  {isFullscreen ? (\n                    <Minimize2 className=\"h-4 w-4\" />\n                  ) : (\n                    <Maximize2 className=\"h-4 w-4\" />\n                  )}\n                </Button>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={handleRefresh}\n                  disabled={refreshing}\n                  title=\"Refresh output\"\n                  className=\"h-8 px-2\"\n                >\n                  <RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />\n                </Button>\n                {run.status === 'running' && (\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={handleStop}\n                    disabled={refreshing}\n                    title=\"Stop execution\"\n                    className=\"h-8 px-2 text-destructive hover:text-destructive\"\n                  >\n                    <StopCircle className=\"h-4 w-4\" />\n                  </Button>\n                )}\n              </div>\n          </div>\n        </CardHeader>\n        <CardContent className={`${isFullscreen ? 'h-[calc(100vh-120px)]' : 'flex-1'} p-0 overflow-hidden`}>\n          {loading ? (\n              <div className=\"flex items-center justify-center h-full\">\n                <div className=\"flex items-center space-x-2\">\n                  <RefreshCw className=\"h-4 w-4 animate-spin\" />\n                  <span>Loading output...</span>\n                </div>\n              </div>\n            ) : messages.length === 0 ? (\n              <div className=\"flex items-center justify-center h-full text-muted-foreground\">\n                <p>No output available yet</p>\n              </div>\n            ) : (\n              <div \n                ref={scrollAreaRef}\n                className=\"h-full overflow-y-auto p-4 space-y-2\"\n                onScroll={handleScroll}\n              >\n                <AnimatePresence>\n                  {displayableMessages.map((message: ClaudeStreamMessage, index: number) => (\n                    <motion.div\n                      key={index}\n                      initial={{ opacity: 0, y: 10 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      transition={{ duration: 0.2 }}\n                    >\n                      <ErrorBoundary>\n                        <StreamMessage message={message} streamMessages={messages} />\n                      </ErrorBoundary>\n                    </motion.div>\n                  ))}\n                </AnimatePresence>\n                <div ref={outputEndRef} />\n              </div>\n          )}\n        </CardContent>\n        </Card>\n      </div>\n\n      {/* Fullscreen Modal */}\n      {isFullscreen && (\n        <div className=\"fixed inset-0 bg-background z-[60] flex flex-col\">\n          <div className=\"flex items-center justify-between p-4 border-b\">\n            <div className=\"flex items-center gap-3\">\n              {renderIcon(run.agent_icon)}\n              <div>\n                <h3 className=\"font-semibold text-lg\">{run.agent_name}</h3>\n                <p className=\"text-sm text-muted-foreground\">{run.task}</p>\n              </div>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <Popover\n                trigger={\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                  >\n                    <Copy className=\"h-4 w-4 mr-2\" />\n                    Copy Output\n                    <ChevronDown className=\"h-3 w-3 ml-2\" />\n                  </Button>\n                }\n                content={\n                  <div className=\"w-44 p-1\">\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"w-full justify-start\"\n                      onClick={handleCopyAsJsonl}\n                    >\n                      Copy as JSONL\n                    </Button>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"w-full justify-start\"\n                      onClick={handleCopyAsMarkdown}\n                    >\n                      Copy as Markdown\n                    </Button>\n                  </div>\n                }\n                align=\"end\"\n              />\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={handleRefresh}\n                disabled={refreshing}\n              >\n                <RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />\n              </Button>\n              {run.status === 'running' && (\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={handleStop}\n                  disabled={refreshing}\n                >\n                  <StopCircle className=\"h-4 w-4 mr-2\" />\n                  Stop\n                </Button>\n              )}\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={() => setIsFullscreen(false)}\n              >\n                <Minimize2 className=\"h-4 w-4 mr-2\" />\n                Exit Fullscreen\n              </Button>\n            </div>\n          </div>\n          <div \n            ref={fullscreenScrollRef}\n            className=\"flex-1 overflow-y-auto p-6\"\n            onScroll={handleScroll}\n          >\n            <div className=\"max-w-4xl mx-auto space-y-2\">\n              {messages.length === 0 ? (\n                <div className=\"text-center text-muted-foreground py-8\">\n                  No output available yet\n                </div>\n              ) : (\n                <>\n                  <AnimatePresence>\n                    {displayableMessages.map((message: ClaudeStreamMessage, index: number) => (\n                      <motion.div\n                        key={index}\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        transition={{ duration: 0.2 }}\n                      >\n                        <ErrorBoundary>\n                          <StreamMessage message={message} streamMessages={messages} />\n                        </ErrorBoundary>\n                      </motion.div>\n                    ))}\n                  </AnimatePresence>\n                  <div ref={fullscreenMessagesEndRef} />\n                </>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n    </>\n  );\n}\n\nexport default AgentRunOutputViewer; "
  },
  {
    "path": "src/components/AgentRunView.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { \n  ArrowLeft, \n  Copy, \n  ChevronDown, \n  Clock,\n  Hash,\n  DollarSign,\n  Bot,\n  StopCircle\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Popover } from \"@/components/ui/popover\";\nimport { api, type AgentRunWithMetrics } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { formatISOTimestamp } from \"@/lib/date-utils\";\nimport { StreamMessage } from \"./StreamMessage\";\nimport { AGENT_ICONS } from \"./CCAgents\";\nimport type { ClaudeStreamMessage } from \"./AgentExecution\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\n\ninterface AgentRunViewProps {\n  /**\n   * The run ID to view\n   */\n  runId: number;\n  /**\n   * Callback to go back\n   */\n  onBack: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * AgentRunView component for viewing past agent execution details\n * \n * @example\n * <AgentRunView runId={123} onBack={() => setView('list')} />\n */\nexport const AgentRunView: React.FC<AgentRunViewProps> = ({\n  runId,\n  onBack,\n  className,\n}) => {\n  const [run, setRun] = useState<AgentRunWithMetrics | null>(null);\n  const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);\n\n  useEffect(() => {\n    loadRun();\n  }, [runId]);\n\n  const loadRun = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const runData = await api.getAgentRunWithRealTimeMetrics(runId);\n      setRun(runData);\n      \n      // If we have a session_id, try to load from JSONL file first\n      if (runData.session_id && runData.session_id !== '') {\n        try {\n          const history = await api.loadAgentSessionHistory(runData.session_id);\n          \n          // Convert history to messages format\n          const loadedMessages: ClaudeStreamMessage[] = history.map(entry => ({\n            ...entry,\n            type: entry.type || \"assistant\"\n          }));\n          \n          setMessages(loadedMessages);\n          return;\n        } catch (err) {\n          console.warn('Failed to load from JSONL, falling back to output field:', err);\n        }\n      }\n      \n      // Fallback: Parse JSONL output from the output field\n      if (runData.output) {\n        const parsedMessages: ClaudeStreamMessage[] = [];\n        const lines = runData.output.split('\\n').filter(line => line.trim());\n        \n        for (const line of lines) {\n          try {\n            const msg = JSON.parse(line) as ClaudeStreamMessage;\n            parsedMessages.push(msg);\n          } catch (err) {\n            console.error(\"Failed to parse line:\", line, err);\n          }\n        }\n        \n        setMessages(parsedMessages);\n      }\n    } catch (err) {\n      console.error(\"Failed to load run:\", err);\n      setError(\"Failed to load execution details\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleCopyAsJsonl = async () => {\n    if (!run?.output) return;\n    await navigator.clipboard.writeText(run.output);\n    setCopyPopoverOpen(false);\n  };\n\n  const handleCopyAsMarkdown = async () => {\n    if (!run) return;\n    \n    let markdown = `# Agent Run: ${run.agent_name}\\n\\n`;\n    markdown += `**Task:** ${run.task}\\n`;\n    markdown += `**Model:** ${run.model}\\n`;\n    markdown += `**Status:** ${run.status}\\n`;\n    if (run.metrics) {\n      markdown += `**Tokens:** ${run.metrics.total_tokens || 'N/A'}\\n`;\n      markdown += `**Cost:** $${run.metrics.cost_usd?.toFixed(4) || 'N/A'}\\n`;\n    }\n    markdown += `**Date:** ${new Date(run.created_at).toISOString()}\\n\\n`;\n    markdown += `---\\n\\n`;\n\n    for (const msg of messages) {\n      if (msg.type === \"system\" && msg.subtype === \"init\") {\n        markdown += `## System Initialization\\n\\n`;\n        markdown += `- Session ID: \\`${msg.session_id || 'N/A'}\\`\\n`;\n        markdown += `- Model: \\`${msg.model || 'default'}\\`\\n`;\n        if (msg.cwd) markdown += `- Working Directory: \\`${msg.cwd}\\`\\n`;\n        if (msg.tools?.length) markdown += `- Tools: ${msg.tools.join(', ')}\\n`;\n        markdown += `\\n`;\n      } else if (msg.type === \"assistant\" && msg.message) {\n        markdown += `## Assistant\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_use\") {\n            markdown += `### Tool: ${content.name}\\n\\n`;\n            markdown += `\\`\\`\\`json\\n${JSON.stringify(content.input, null, 2)}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n        if (msg.message.usage) {\n          markdown += `*Tokens: ${msg.message.usage.input_tokens} in, ${msg.message.usage.output_tokens} out*\\n\\n`;\n        }\n      } else if (msg.type === \"user\" && msg.message) {\n        markdown += `## User\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_result\") {\n            markdown += `### Tool Result\\n\\n`;\n            markdown += `\\`\\`\\`\\n${content.content}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n      } else if (msg.type === \"result\") {\n        markdown += `## Execution Result\\n\\n`;\n        if (msg.result) {\n          markdown += `${msg.result}\\n\\n`;\n        }\n        if (msg.error) {\n          markdown += `**Error:** ${msg.error}\\n\\n`;\n        }\n      }\n    }\n\n    await navigator.clipboard.writeText(markdown);\n    setCopyPopoverOpen(false);\n  };\n\n  const handleStop = async () => {\n    if (!runId) {\n      console.error('[AgentRunView] No run ID available to stop');\n      return;\n    }\n\n    try {\n      // Call the API to kill the agent session\n      const success = await api.killAgentSession(runId);\n      \n      if (success) {\n        console.log(`[AgentRunView] Successfully stopped agent session ${runId}`);\n        \n        // Update the run status locally\n        if (run) {\n          setRun({ ...run, status: 'cancelled' });\n        }\n        \n        // Add a message indicating execution was stopped\n        const stopMessage: ClaudeStreamMessage = {\n          type: \"result\",\n          subtype: \"error\",\n          is_error: true,\n          result: \"Execution stopped by user\",\n          duration_ms: 0,\n          usage: {\n            input_tokens: 0,\n            output_tokens: 0\n          }\n        };\n        setMessages(prev => [...prev, stopMessage]);\n        \n        // Reload the run data after a short delay\n        setTimeout(() => {\n          loadRun();\n        }, 1000);\n      } else {\n        console.warn(`[AgentRunView] Failed to stop agent session ${runId} - it may have already finished`);\n      }\n    } catch (err) {\n      console.error('[AgentRunView] Failed to stop agent:', err);\n    }\n  };\n\n  const renderIcon = (iconName: string) => {\n    const Icon = AGENT_ICONS[iconName as keyof typeof AGENT_ICONS] || Bot;\n    return <Icon className=\"h-5 w-5\" />;\n  };\n\n  if (loading) {\n    return (\n      <div className={cn(\"flex items-center justify-center h-full\", className)}>\n        <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-primary\"></div>\n      </div>\n    );\n  }\n\n  if (error || !run) {\n    return (\n      <div className={cn(\"flex flex-col items-center justify-center h-full\", className)}>\n        <p className=\"text-destructive mb-4\">{error || \"Run not found\"}</p>\n        <Button onClick={onBack}>Go Back</Button>\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(\"flex flex-col h-full bg-background\", className)}>\n      <div className=\"w-full max-w-5xl mx-auto h-full flex flex-col\">\n        {/* Header */}\n        <motion.div\n          initial={{ opacity: 0, y: -20 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.3 }}\n          className=\"flex items-center justify-between p-4 border-b border-border\"\n        >\n          <div className=\"flex items-center space-x-3\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={onBack}\n              className=\"h-8 w-8\"\n            >\n              <ArrowLeft className=\"h-4 w-4\" />\n            </Button>\n            <div className=\"flex items-center gap-2\">\n              {renderIcon(run.agent_icon)}\n              <div>\n                <h2 className=\"text-lg font-semibold\">{run.agent_name}</h2>\n                <p className=\"text-xs text-muted-foreground\">Execution History</p>\n              </div>\n            </div>\n          </div>\n          \n          <div className=\"flex items-center gap-2\">\n            {run?.status === 'running' && (\n              <Button\n                size=\"sm\"\n                variant=\"ghost\"\n                onClick={handleStop}\n                className=\"text-destructive hover:text-destructive\"\n              >\n                <StopCircle className=\"h-4 w-4 mr-1\" />\n                Stop\n              </Button>\n            )}\n            \n            <Popover\n              trigger={\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"flex items-center gap-2\"\n                >\n                  <Copy className=\"h-4 w-4\" />\n                  Copy Output\n                  <ChevronDown className=\"h-3 w-3\" />\n                </Button>\n              }\n              content={\n                <div className=\"w-44 p-1\">\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"w-full justify-start\"\n                    onClick={handleCopyAsJsonl}\n                  >\n                    Copy as JSONL\n                  </Button>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"w-full justify-start\"\n                    onClick={handleCopyAsMarkdown}\n                  >\n                    Copy as Markdown\n                  </Button>\n                </div>\n              }\n              open={copyPopoverOpen}\n              onOpenChange={setCopyPopoverOpen}\n              align=\"end\"\n            />\n          </div>\n        </motion.div>\n        \n        {/* Run Details */}\n        <Card className=\"m-4\">\n          <CardContent className=\"p-4\">\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center gap-2\">\n                <h3 className=\"text-sm font-medium\">Task:</h3>\n                <p className=\"text-sm text-muted-foreground flex-1\">{run.task}</p>\n                <Badge variant=\"outline\" className=\"text-xs\">\n                  {run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n                </Badge>\n              </div>\n              \n              <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n                <div className=\"flex items-center gap-1\">\n                  <Clock className=\"h-3 w-3\" />\n                  <span>{formatISOTimestamp(run.created_at)}</span>\n                </div>\n                \n                {run.metrics?.duration_ms && (\n                  <div className=\"flex items-center gap-1\">\n                    <Clock className=\"h-3 w-3\" />\n                    <span>{(run.metrics.duration_ms / 1000).toFixed(2)}s</span>\n                  </div>\n                )}\n                \n                {run.metrics?.total_tokens && (\n                  <div className=\"flex items-center gap-1\">\n                    <Hash className=\"h-3 w-3\" />\n                    <span>{run.metrics.total_tokens} tokens</span>\n                  </div>\n                )}\n                \n                {run.metrics?.cost_usd && (\n                  <div className=\"flex items-center gap-1\">\n                    <DollarSign className=\"h-3 w-3\" />\n                    <span>${run.metrics.cost_usd.toFixed(4)}</span>\n                  </div>\n                )}\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n\n        {/* Output Display */}\n        <div className=\"flex-1 overflow-hidden\">\n          <div className=\"h-full overflow-y-auto p-4 space-y-2\">\n            {messages.map((message, index) => (\n              <motion.div\n                key={index}\n                initial={{ opacity: 0, y: 10 }}\n                animate={{ opacity: 1, y: 0 }}\n                transition={{ duration: 0.2, delay: index * 0.02 }}\n              >\n                <ErrorBoundary>\n                  <StreamMessage message={message} streamMessages={messages} />\n                </ErrorBoundary>\n              </motion.div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/AgentRunsList.tsx",
    "content": "import React, { useState } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { Play, Clock, Hash, Bot } from \"lucide-react\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport { cn } from \"@/lib/utils\";\nimport { formatISOTimestamp } from \"@/lib/date-utils\";\nimport type { AgentRunWithMetrics } from \"@/lib/api\";\nimport { AGENT_ICONS } from \"./CCAgents\";\nimport { useTabState } from \"@/hooks/useTabState\";\n\ninterface AgentRunsListProps {\n  /**\n   * Array of agent runs to display\n   */\n  runs: AgentRunWithMetrics[];\n  /**\n   * Callback when a run is clicked\n   */\n  onRunClick?: (run: AgentRunWithMetrics) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\nconst ITEMS_PER_PAGE = 5;\n\n/**\n * AgentRunsList component - Displays a paginated list of agent execution runs\n * \n * @example\n * <AgentRunsList\n *   runs={runs}\n *   onRunClick={(run) => console.log('Selected:', run)}\n * />\n */\nexport const AgentRunsList: React.FC<AgentRunsListProps> = ({\n  runs,\n  onRunClick,\n  className,\n}) => {\n  const [currentPage, setCurrentPage] = useState(1);\n  const { createAgentTab } = useTabState();\n  \n  // Calculate pagination\n  const totalPages = Math.ceil(runs.length / ITEMS_PER_PAGE);\n  const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;\n  const endIndex = startIndex + ITEMS_PER_PAGE;\n  const currentRuns = runs.slice(startIndex, endIndex);\n  \n  // Reset to page 1 if runs change\n  React.useEffect(() => {\n    setCurrentPage(1);\n  }, [runs.length]);\n  \n  const renderIcon = (iconName: string) => {\n    const Icon = AGENT_ICONS[iconName as keyof typeof AGENT_ICONS] || Bot;\n    return <Icon className=\"h-4 w-4\" />;\n  };\n  \n  const formatDuration = (ms?: number) => {\n    if (!ms) return \"N/A\";\n    const seconds = Math.floor(ms / 1000);\n    if (seconds < 60) return `${seconds}s`;\n    const minutes = Math.floor(seconds / 60);\n    const remainingSeconds = seconds % 60;\n    return `${minutes}m ${remainingSeconds}s`;\n  };\n  \n  const formatTokens = (tokens?: number) => {\n    if (!tokens) return \"0\";\n    if (tokens >= 1000) {\n      return `${(tokens / 1000).toFixed(1)}k`;\n    }\n    return tokens.toString();\n  };\n  \n  const handleRunClick = (run: AgentRunWithMetrics) => {\n    // If there's a callback, use it (for full-page navigation)\n    if (onRunClick) {\n      onRunClick(run);\n    } else if (run.id) {\n      // Otherwise, open in new tab\n      createAgentTab(run.id.toString(), run.agent_name);\n    }\n  };\n  \n  if (runs.length === 0) {\n    return (\n      <div className={cn(\"text-center py-8 text-muted-foreground\", className)}>\n        <Play className=\"h-8 w-8 mx-auto mb-2 opacity-50\" />\n        <p className=\"text-sm\">No execution history yet</p>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className={cn(\"space-y-2\", className)}>\n        <AnimatePresence mode=\"popLayout\">\n          {currentRuns.map((run, index) => (\n            <motion.div\n              key={run.id}\n              initial={{ opacity: 0, y: 20 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -20 }}\n              transition={{\n                duration: 0.3,\n                delay: index * 0.05,\n                ease: [0.4, 0, 0.2, 1],\n              }}\n            >\n              <Card\n                className={cn(\n                  \"cursor-pointer transition-all hover:shadow-md hover:scale-[1.01] active:scale-[0.99]\",\n                  run.status === \"running\" && \"border-green-500/50\"\n                )}\n                onClick={() => handleRunClick(run)}\n              >\n                <CardContent className=\"p-3\">\n                  <div className=\"flex items-center gap-3\">\n                    <div className=\"flex-shrink-0\">\n                      {renderIcon(run.agent_icon)}\n                    </div>\n                    \n                    <div className=\"flex-1 min-w-0\">\n                      <div className=\"flex items-center gap-2 mb-1\">\n                        <h4 className=\"text-sm font-medium truncate\">\n                          {run.agent_name}\n                        </h4>\n                        {run.status === \"running\" && (\n                          <div className=\"flex items-center gap-1\">\n                            <div className=\"w-2 h-2 bg-green-500 rounded-full animate-pulse\"></div>\n                            <span className=\"text-xs text-green-600 font-medium\">Running</span>\n                          </div>\n                        )}\n                      </div>\n                      \n                      <p className=\"text-xs text-muted-foreground truncate mb-1\">\n                        {run.task}\n                      </p>\n                      \n                      <div className=\"flex items-center gap-3 text-xs text-muted-foreground\">\n                        <div className=\"flex items-center gap-1\">\n                          <Clock className=\"h-3 w-3\" />\n                          <span>{formatISOTimestamp(run.created_at)}</span>\n                        </div>\n                        \n                        {run.metrics?.duration_ms && (\n                          <span>{formatDuration(run.metrics.duration_ms)}</span>\n                        )}\n                        \n                        {run.metrics?.total_tokens && (\n                          <div className=\"flex items-center gap-1\">\n                            <Hash className=\"h-3 w-3\" />\n                            <span>{formatTokens(run.metrics.total_tokens)}</span>\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                    \n                    <div className=\"flex-shrink-0\">\n                      <Badge \n                        variant={\n                          run.status === \"completed\" ? \"default\" :\n                          run.status === \"running\" ? \"secondary\" :\n                          run.status === \"failed\" ? \"destructive\" :\n                          \"outline\"\n                        }\n                        className=\"text-xs\"\n                      >\n                        {run.status === \"completed\" ? \"Completed\" :\n                         run.status === \"running\" ? \"Running\" :\n                         run.status === \"failed\" ? \"Failed\" :\n                         \"Pending\"}\n                      </Badge>\n                    </div>\n                  </div>\n                </CardContent>\n              </Card>\n            </motion.div>\n          ))}\n        </AnimatePresence>\n        \n        {/* Pagination */}\n        {totalPages > 1 && (\n          <div className=\"pt-2\">\n            <Pagination\n              currentPage={currentPage}\n              totalPages={totalPages}\n              onPageChange={setCurrentPage}\n            />\n          </div>\n        )}\n      </div>\n\n    </>\n  );\n}; "
  },
  {
    "path": "src/components/Agents.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { Bot, Loader2, Play, Clock, CheckCircle, XCircle, Trash2, Import, ChevronDown, ChevronRight, FileJson, Globe, Download, Plus, History, Edit } from 'lucide-react';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\nimport { Card } from '@/components/ui/card';\nimport { Toast } from '@/components/ui/toast';\nimport { api, type Agent, type AgentRunWithMetrics } from '@/lib/api';\nimport { open as openDialog, save } from '@tauri-apps/plugin-dialog';\nimport { invoke } from '@tauri-apps/api/core';\nimport { GitHubAgentBrowser } from '@/components/GitHubAgentBrowser';\nimport { CreateAgent } from '@/components/CreateAgent';\nimport { useTabState } from '@/hooks/useTabState';\n\nexport const Agents: React.FC = () => {\n  const [activeTab, setActiveTab] = useState('agents');\n  const [showCreateAgent, setShowCreateAgent] = useState(false);\n  const [editingAgent, setEditingAgent] = useState<Agent | null>(null);\n  const [agents, setAgents] = useState<Agent[]>([]);\n  const [runningAgents, setRunningAgents] = useState<AgentRunWithMetrics[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);\n  const [showDeleteDialog, setShowDeleteDialog] = useState(false);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  const [showGitHubBrowser, setShowGitHubBrowser] = useState(false);\n  const { createAgentTab } = useTabState();\n\n  // Load agents on mount\n  useEffect(() => {\n    loadAgents();\n    loadRunningAgents();\n  }, []);\n\n  // Refresh running agents periodically\n  useEffect(() => {\n    const interval = setInterval(() => {\n      loadRunningAgents();\n    }, 3000); // Refresh every 3 seconds\n\n    return () => clearInterval(interval);\n  }, []);\n\n  const loadAgents = async () => {\n    try {\n      setLoading(true);\n      const agents = await api.listAgents();\n      setAgents(agents);\n    } catch (error) {\n      console.error('Failed to load agents:', error);\n      setToast({ message: 'Failed to load agents', type: 'error' });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const loadRunningAgents = async () => {\n    try {\n      const runs = await api.listAgentRunsWithMetrics();\n      setRunningAgents(runs);\n    } catch (error) {\n      console.error('Failed to load running agents:', error);\n    }\n  };\n\n  const handleRunAgent = async (agent: Agent) => {\n    if (!agent.id) {\n      setToast({ message: 'Agent ID is missing', type: 'error' });\n      return;\n    }\n    \n    // Import the dialog function\n    const { open } = await import('@tauri-apps/plugin-dialog');\n    \n    try {\n      // Prompt user to select a project directory\n      const projectPath = await open({\n        directory: true,\n        multiple: false,\n        title: `Select project directory for ${agent.name}`\n      });\n      \n      if (!projectPath) {\n        // User cancelled\n        return;\n      }\n      \n      // Dispatch event to open agent execution in a new tab\n      const tabId = `agent-exec-${agent.id}-${Date.now()}`;\n      window.dispatchEvent(new CustomEvent('open-agent-execution', { \n        detail: { agent, tabId, projectPath } \n      }));\n      \n      setToast({ message: `Opening agent: ${agent.name}`, type: 'success' });\n    } catch (error) {\n      console.error('Failed to open agent:', error);\n      setToast({ message: `Failed to open agent: ${agent.name}`, type: 'error' });\n    }\n  };\n\n  const handleDeleteAgent = async () => {\n    if (!agentToDelete || !agentToDelete.id) return;\n    \n    try {\n      await api.deleteAgent(agentToDelete.id);\n      setToast({ message: `Deleted agent: ${agentToDelete.name}`, type: 'success' });\n      setAgents(prev => prev.filter(a => a.id !== agentToDelete.id));\n      setShowDeleteDialog(false);\n      setAgentToDelete(null);\n    } catch (error) {\n      console.error('Failed to delete agent:', error);\n      setToast({ message: `Failed to delete agent: ${agentToDelete.name}`, type: 'error' });\n    }\n  };\n\n  const handleImportFromFile = async () => {\n    try {\n      const selected = await openDialog({\n        filters: [\n          { name: 'opcode Agent', extensions: ['opcode.json', 'json'] },\n          { name: 'All Files', extensions: ['*'] }\n        ],\n        multiple: false,\n      });\n\n      if (selected) {\n        const importedAgent = await api.importAgentFromFile(selected as string);\n        setToast({ message: `Imported agent: ${importedAgent.name}`, type: 'success' });\n        loadAgents();\n      }\n    } catch (error) {\n      console.error('Failed to import agent:', error);\n      setToast({ message: 'Failed to import agent', type: 'error' });\n    }\n  };\n\n  const handleExportAgent = async (agent: Agent) => {\n    try {\n      const path = await save({\n        defaultPath: `${agent.name.toLowerCase().replace(/\\s+/g, '-')}.opcode.json`,\n        filters: [\n          { name: 'opcode Agent', extensions: ['opcode.json'] }\n        ]\n      });\n\n      if (path && agent.id) {\n        await invoke('export_agent_to_file', { id: agent.id, filePath: path });\n        setToast({ message: `Exported agent: ${agent.name}`, type: 'success' });\n      }\n    } catch (error) {\n      console.error('Failed to export agent:', error);\n      setToast({ message: 'Failed to export agent', type: 'error' });\n    }\n  };\n\n  const getStatusIcon = (status: string) => {\n    switch (status) {\n      case 'running':\n        return <Loader2 className=\"w-4 h-4 animate-spin\" />;\n      case 'completed':\n        return <CheckCircle className=\"w-4 h-4 text-green-500\" />;\n      case 'failed':\n        return <XCircle className=\"w-4 h-4 text-red-500\" />;\n      default:\n        return <Clock className=\"w-4 h-4 text-muted-foreground\" />;\n    }\n  };\n\n  // Show CreateAgent component if creating\n  if (showCreateAgent) {\n    return (\n      <CreateAgent \n        onBack={() => setShowCreateAgent(false)}\n        onAgentCreated={() => {\n          setShowCreateAgent(false);\n          loadAgents(); // Reload agents after creation\n        }}\n      />\n    );\n  }\n\n  // Show CreateAgent component in edit mode\n  if (editingAgent) {\n    return (\n      <CreateAgent\n        agent={editingAgent}\n        onBack={() => setEditingAgent(null)}\n        onAgentCreated={() => {\n          setEditingAgent(null);\n          loadAgents(); // Reload agents after update\n        }}\n      />\n    );\n  }\n\n  return (\n    <div className=\"h-full overflow-y-auto\">\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <h1 className=\"text-3xl font-bold tracking-tight\">Agents</h1>\n              <p className=\"mt-1 text-sm text-muted-foreground\">\n                Manage your Claude Code agents\n              </p>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button variant=\"outline\">\n                    <Import className=\"w-4 h-4 mr-2\" />\n                    Import\n                    <ChevronDown className=\"w-4 h-4 ml-2\" />\n                  </Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"end\">\n                  <DropdownMenuItem onClick={handleImportFromFile}>\n                    <FileJson className=\"w-4 h-4 mr-2\" />\n                    From File\n                  </DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => setShowGitHubBrowser(true)}>\n                    <Globe className=\"w-4 h-4 mr-2\" />\n                    From GitHub\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n\n              <Button onClick={() => setShowCreateAgent(true)}>\n                <Plus className=\"w-4 h-4 mr-2\" />\n                Create Agent\n              </Button>\n            </div>\n          </div>\n        </div>\n\n        {/* Toast notifications */}\n        <AnimatePresence>\n          {toast && (\n            <motion.div\n              initial={{ opacity: 0, y: -10 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -10 }}\n              className=\"mx-6 mb-4\"\n            >\n              <Toast \n                message={toast.message} \n                type={toast.type}\n                onDismiss={() => setToast(null)}\n              />\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n      {showGitHubBrowser && (\n        <GitHubAgentBrowser\n          isOpen={showGitHubBrowser}\n          onClose={() => setShowGitHubBrowser(false)}\n          onImportSuccess={() => {\n            loadAgents();\n            setShowGitHubBrowser(false);\n            setToast({ message: 'Agent imported successfully', type: 'success' });\n          }}\n        />\n      )}\n\n      <AnimatePresence>\n        {showDeleteDialog && agentToDelete && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center\"\n            onClick={() => setShowDeleteDialog(false)}\n          >\n            <motion.div\n              initial={{ scale: 0.95, opacity: 0 }}\n              animate={{ scale: 1, opacity: 1 }}\n              exit={{ scale: 0.95, opacity: 0 }}\n              className=\"bg-card p-6 rounded-lg shadow-lg max-w-md w-full mx-4\"\n              onClick={(e) => e.stopPropagation()}\n            >\n              <h3 className=\"text-lg font-semibold mb-4\">Delete Agent</h3>\n              <p className=\"text-muted-foreground mb-6\">\n                Are you sure you want to delete \"{agentToDelete.name}\"? This action cannot be undone.\n              </p>\n              <div className=\"flex gap-3 justify-end\">\n                <Button\n                  variant=\"outline\"\n                  onClick={() => setShowDeleteDialog(false)}\n                >\n                  Cancel\n                </Button>\n                <Button\n                  variant=\"destructive\"\n                  onClick={handleDeleteAgent}\n                >\n                  Delete\n                </Button>\n              </div>\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n        {/* Content */}\n        <div className=\"flex-1 overflow-y-auto p-6\">\n          <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n            <TabsList className=\"grid grid-cols-2 w-full max-w-md mb-6 h-auto p-1\">\n              <TabsTrigger value=\"agents\" className=\"py-2.5 px-3\">\n                <Bot className=\"w-4 h-4 mr-2\" />\n                Agents ({agents.length})\n              </TabsTrigger>\n              <TabsTrigger value=\"running\" className=\"py-2.5 px-3\">\n                <History className=\"w-4 h-4 mr-2\" />\n                History ({runningAgents.length})\n              </TabsTrigger>\n            </TabsList>\n\n          <TabsContent value=\"agents\" className=\"flex-1 overflow-hidden\">\n              {loading ? (\n                <div className=\"flex items-center justify-center h-64\">\n                  <Loader2 className=\"w-8 h-8 animate-spin text-muted-foreground\" />\n                </div>\n              ) : agents.length === 0 ? (\n                <div className=\"flex flex-col items-center justify-center h-64 text-center\">\n                  <Bot className=\"w-12 h-12 text-muted-foreground mb-4\" />\n                  <h3 className=\"text-lg font-semibold mb-2\">No Agents Yet</h3>\n                  <p className=\"text-muted-foreground mb-4\">\n                    Create your first agent to get started\n                  </p>\n                  <Button onClick={() => setShowCreateAgent(true)}>\n                    <Plus className=\"w-4 h-4 mr-2\" />\n                    Create Agent\n                  </Button>\n                </div>\n              ) : (\n                <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n                  {agents.map((agent) => (\n                    <Card\n                      key={agent.id}\n                      className=\"p-4 hover:shadow-md transition-shadow\"\n                    >\n                      <div className=\"flex items-start justify-between mb-3\">\n                        <div className=\"flex items-center gap-2\">\n                          <Bot className=\"w-5 h-5 text-primary\" />\n                          <h3 className=\"font-semibold\">{agent.name}</h3>\n                        </div>\n                        <DropdownMenu>\n                          <DropdownMenuTrigger asChild>\n                            <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\">\n                              <ChevronDown className=\"w-4 h-4\" />\n                            </Button>\n                          </DropdownMenuTrigger>\n                          <DropdownMenuContent align=\"end\">\n                            <DropdownMenuItem onClick={() => setEditingAgent(agent)}>\n                              <Edit className=\"w-4 h-4 mr-2\" />\n                              Edit\n                            </DropdownMenuItem>\n                            <DropdownMenuItem onClick={() => handleRunAgent(agent)}>\n                              <Play className=\"w-4 h-4 mr-2\" />\n                              Run\n                            </DropdownMenuItem>\n                            <DropdownMenuItem onClick={() => handleExportAgent(agent)}>\n                              <Download className=\"w-4 h-4 mr-2\" />\n                              Export\n                            </DropdownMenuItem>\n                            <DropdownMenuItem \n                              onClick={() => {\n                                setAgentToDelete(agent);\n                                setShowDeleteDialog(true);\n                              }}\n                              className=\"text-destructive\"\n                            >\n                              <Trash2 className=\"w-4 h-4 mr-2\" />\n                              Delete\n                            </DropdownMenuItem>\n                          </DropdownMenuContent>\n                        </DropdownMenu>\n                      </div>\n\n                      <p className=\"text-sm text-muted-foreground mb-3 line-clamp-2\">\n                        No description provided\n                      </p>\n\n                      <div className=\"flex items-center justify-between\">\n                        <Badge variant=\"secondary\" className=\"text-xs\">\n                          v1.0.0\n                        </Badge>\n                        <Button\n                          size=\"sm\"\n                          onClick={() => handleRunAgent(agent)}\n                        >\n                          <Play className=\"w-3 h-3 mr-1\" />\n                          Run\n                        </Button>\n                      </div>\n                    </Card>\n                  ))}\n                </div>\n              )}\n            </TabsContent>\n\n            <TabsContent value=\"running\" className=\"space-y-6 mt-6\">\n              {runningAgents.length === 0 ? (\n                <Card className=\"p-12\">\n                  <div className=\"flex flex-col items-center justify-center text-center\">\n                    <History className=\"w-12 h-12 text-muted-foreground mb-4\" />\n                    <h3 className=\"text-lg font-semibold mb-2\">No Agent History</h3>\n                    <p className=\"text-muted-foreground\">\n                      Run an agent to see it here\n                    </p>\n                  </div>\n                </Card>\n              ) : (\n                <div className=\"space-y-4\">\n                  {runningAgents.map((run) => (\n                    <Card\n                      key={run.id}\n                      className=\"p-4\"\n                    >\n                      <div className=\"flex items-center justify-between mb-2\">\n                        <div className=\"flex items-center gap-3\">\n                          {getStatusIcon(run.status)}\n                          <h3 className=\"font-semibold\">{run.agent_name}</h3>\n                          <Badge variant=\"outline\" className=\"text-xs\">\n                            {run.status}\n                          </Badge>\n                        </div>\n                        <Button\n                          size=\"icon\"\n                          variant=\"ghost\"\n                          onClick={() => createAgentTab(run.id?.toString() || '', run.agent_name)}\n                          className=\"h-8 w-8\"\n                        >\n                          <ChevronRight className=\"w-4 h-4\" />\n                        </Button>\n                      </div>\n\n                      <div className=\"grid grid-cols-3 gap-4 text-sm\">\n                        <div>\n                          <span className=\"text-muted-foreground\">Started:</span>\n                          <p className=\"font-medium\">{new Date(run.created_at).toLocaleString()}</p>\n                        </div>\n                        <div>\n                          <span className=\"text-muted-foreground\">Duration:</span>\n                          <p className=\"font-medium\">{run.metrics?.duration_ms ? `${(run.metrics.duration_ms / 1000).toFixed(1)}s` : run.duration_ms ? `${(run.duration_ms / 1000).toFixed(1)}s` : '—'}</p>\n                        </div>\n                        <div>\n                          <span className=\"text-muted-foreground\">Tokens:</span>\n                          <p className=\"font-medium\">{run.metrics?.total_tokens ? run.metrics.total_tokens.toLocaleString() : run.total_tokens ? run.total_tokens.toLocaleString() : '—'}</p>\n                        </div>\n                      </div>\n\n                      {run.status === 'failed' && (\n                        <div className=\"mt-3 p-2 bg-destructive/10 rounded text-sm text-destructive\">\n                          Agent execution failed\n                        </div>\n                      )}\n                    </Card>\n                  ))}\n                </div>\n              )}\n            </TabsContent>\n          </Tabs>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/AgentsModal.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { Bot, Plus, Loader2, Play, Clock, CheckCircle, XCircle, Trash2, Import, ChevronDown, FileJson, Globe, Download } from 'lucide-react';\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n} from '@/components/ui/dialog';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\nimport { ScrollArea } from '@/components/ui/scroll-area';\nimport { Toast } from '@/components/ui/toast';\nimport { api, type Agent, type AgentRunWithMetrics } from '@/lib/api';\nimport { useTabState } from '@/hooks/useTabState';\nimport { formatISOTimestamp } from '@/lib/date-utils';\nimport { open as openDialog, save } from '@tauri-apps/plugin-dialog';\nimport { invoke } from '@tauri-apps/api/core';\nimport { GitHubAgentBrowser } from '@/components/GitHubAgentBrowser';\n\ninterface AgentsModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange }) => {\n  const [activeTab, setActiveTab] = useState('agents');\n  const [agents, setAgents] = useState<Agent[]>([]);\n  const [runningAgents, setRunningAgents] = useState<AgentRunWithMetrics[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);\n  const [showDeleteDialog, setShowDeleteDialog] = useState(false);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  const [showGitHubBrowser, setShowGitHubBrowser] = useState(false);\n  const { createAgentTab, createCreateAgentTab } = useTabState();\n\n  // Load agents when modal opens\n  useEffect(() => {\n    if (open) {\n      loadAgents();\n      loadRunningAgents();\n    }\n  }, [open]);\n\n  // Refresh running agents periodically\n  useEffect(() => {\n    if (!open) return;\n    \n    const interval = setInterval(() => {\n      loadRunningAgents();\n    }, 3000); // Refresh every 3 seconds\n\n    return () => clearInterval(interval);\n  }, [open]);\n\n  const loadAgents = async () => {\n    try {\n      setLoading(true);\n      const agentList = await api.listAgents();\n      setAgents(agentList);\n    } catch (error) {\n      console.error('Failed to load agents:', error);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const loadRunningAgents = async () => {\n    try {\n      const runs = await api.listRunningAgentSessions();\n      const agentRuns = runs.map(run => ({\n        id: run.id,\n        agent_id: run.agent_id,\n        agent_name: run.agent_name,\n        task: run.task,\n        model: run.model,\n        status: 'running' as const,\n        created_at: run.created_at,\n        project_path: run.project_path,\n      } as AgentRunWithMetrics));\n      \n      setRunningAgents(agentRuns);\n    } catch (error) {\n      console.error('Failed to load running agents:', error);\n    }\n  };\n\n  const handleRunAgent = async (agent: Agent) => {\n    // Open directory picker for project path\n    const { open } = await import('@tauri-apps/plugin-dialog');\n    \n    try {\n      const projectPath = await open({\n        directory: true,\n        multiple: false,\n        title: `Select project directory for ${agent.name}`\n      });\n      \n      if (!projectPath) {\n        // User cancelled\n        return;\n      }\n      \n      // Create a new agent execution tab\n      const tabId = `agent-exec-${agent.id}-${Date.now()}`;\n      \n      // Close modal\n      onOpenChange(false);\n      \n      // Dispatch event to open agent execution in the new tab with project path\n      window.dispatchEvent(new CustomEvent('open-agent-execution', { \n        detail: { agent, tabId, projectPath } \n      }));\n    } catch (error) {\n      console.error('Failed to run agent:', error);\n      setToast({ message: `Failed to run agent: ${agent.name}`, type: 'error' });\n    }\n  };\n\n  const handleDeleteAgent = async (agent: Agent) => {\n    setAgentToDelete(agent);\n    setShowDeleteDialog(true);\n  };\n\n  const confirmDelete = async () => {\n    if (!agentToDelete?.id) return;\n    try {\n      await api.deleteAgent(agentToDelete.id);\n      loadAgents(); // Refresh the list\n      setShowDeleteDialog(false);\n      setAgentToDelete(null);\n    } catch (error) {\n      console.error('Failed to delete agent:', error);\n    }\n  };\n\n  const handleOpenAgentRun = (run: AgentRunWithMetrics) => {\n    // Create new tab for this agent run\n    createAgentTab(run.id!.toString(), run.agent_name);\n    onOpenChange(false);\n  };\n\n  const handleCreateAgent = () => {\n    // Close modal and create new tab\n    onOpenChange(false);\n    createCreateAgentTab();\n  };\n\n  const handleImportFromFile = async () => {\n    try {\n      const filePath = await openDialog({\n        multiple: false,\n        filters: [{\n          name: 'JSON',\n          extensions: ['json']\n        }]\n      });\n      \n      if (filePath) {\n        const agent = await api.importAgentFromFile(filePath as string);\n        loadAgents(); // Refresh list\n        setToast({ message: `Agent \"${agent.name}\" imported successfully`, type: \"success\" });\n      }\n    } catch (error) {\n      console.error('Failed to import agent:', error);\n      setToast({ message: \"Failed to import agent\", type: \"error\" });\n    }\n  };\n\n  const handleImportFromGitHub = () => {\n    setShowGitHubBrowser(true);\n  };\n\n  const handleExportAgent = async (agent: Agent) => {\n    try {\n      const exportData = await api.exportAgent(agent.id!);\n      const filePath = await save({\n        defaultPath: `${agent.name.toLowerCase().replace(/\\s+/g, '-')}.json`,\n        filters: [{\n          name: 'JSON',\n          extensions: ['json']\n        }]\n      });\n      \n      if (filePath) {\n        await invoke('write_file', { path: filePath, content: JSON.stringify(exportData, null, 2) });\n        setToast({ message: \"Agent exported successfully\", type: \"success\" });\n      }\n    } catch (error) {\n      console.error('Failed to export agent:', error);\n      setToast({ message: \"Failed to export agent\", type: \"error\" });\n    }\n  };\n\n  const getStatusIcon = (status: string) => {\n    switch (status) {\n      case 'running':\n        return <Loader2 className=\"w-4 h-4 animate-spin\" />;\n      case 'completed':\n        return <CheckCircle className=\"w-4 h-4 text-green-500\" />;\n      case 'failed':\n        return <XCircle className=\"w-4 h-4 text-red-500\" />;\n      default:\n        return <Clock className=\"w-4 h-4 text-muted-foreground\" />;\n    }\n  };\n\n  return (\n    <>\n      <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-4xl h-[600px] flex flex-col p-0\">\n        <DialogHeader className=\"px-6 pt-6\">\n          <DialogTitle className=\"flex items-center gap-2\">\n            <Bot className=\"w-5 h-5\" />\n            Agent Management\n          </DialogTitle>\n          <DialogDescription>\n            Create new agents or manage running agent executions\n          </DialogDescription>\n        </DialogHeader>\n\n        <Tabs value={activeTab} onValueChange={setActiveTab} className=\"flex-1 flex flex-col\">\n          <TabsList className=\"mx-6\">\n            <TabsTrigger value=\"agents\">Available Agents</TabsTrigger>\n            <TabsTrigger value=\"running\" className=\"relative\">\n              Running Agents\n              {runningAgents.length > 0 && (\n                <Badge variant=\"secondary\" className=\"ml-2 h-5 px-1.5\">\n                  {runningAgents.length}\n                </Badge>\n              )}\n            </TabsTrigger>\n          </TabsList>\n\n          <div className=\"flex-1 overflow-hidden\">\n            <TabsContent value=\"agents\" className=\"h-full m-0\">\n              <ScrollArea className=\"h-full px-6 pb-6\">\n                {/* Action buttons at the top */}\n                <div className=\"flex gap-2 mb-4 pt-4\">\n                  <Button onClick={handleCreateAgent} className=\"flex-1\">\n                    <Plus className=\"w-4 h-4 mr-2\" />\n                    Create Agent\n                  </Button>\n                  <DropdownMenu>\n                    <DropdownMenuTrigger asChild>\n                      <Button variant=\"outline\" className=\"flex-1\">\n                        <Import className=\"w-4 h-4 mr-2\" />\n                        Import Agent\n                        <ChevronDown className=\"w-4 h-4 ml-2\" />\n                      </Button>\n                    </DropdownMenuTrigger>\n                    <DropdownMenuContent>\n                      <DropdownMenuItem onClick={handleImportFromFile}>\n                        <FileJson className=\"w-4 h-4 mr-2\" />\n                        From File\n                      </DropdownMenuItem>\n                      <DropdownMenuItem onClick={handleImportFromGitHub}>\n                        <Globe className=\"w-4 h-4 mr-2\" />\n                        From GitHub\n                      </DropdownMenuItem>\n                    </DropdownMenuContent>\n                  </DropdownMenu>\n                </div>\n                {loading ? (\n                  <div className=\"flex items-center justify-center h-full\">\n                    <Loader2 className=\"w-8 h-8 animate-spin text-muted-foreground\" />\n                  </div>\n                ) : agents.length === 0 ? (\n                  <div className=\"flex flex-col items-center justify-center h-full text-center\">\n                    <Bot className=\"w-12 h-12 text-muted-foreground mb-4\" />\n                    <p className=\"text-lg font-medium mb-2\">No agents available</p>\n                    <p className=\"text-sm text-muted-foreground mb-4\">\n                      Create your first agent to get started\n                    </p>\n                    <Button onClick={() => {\n                      onOpenChange(false);\n                      window.dispatchEvent(new CustomEvent('open-create-agent-tab'));\n                    }}>\n                      <Plus className=\"w-4 h-4 mr-2\" />\n                      Create Agent\n                    </Button>\n                  </div>\n                ) : (\n                  <div className=\"grid gap-4 py-4\">\n                    {agents.map((agent) => (\n                      <motion.div\n                        key={agent.id}\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        className=\"p-4 border rounded-lg hover:bg-muted/50 transition-colors\"\n                      >\n                        <div className=\"flex items-start justify-between\">\n                          <div className=\"flex-1\">\n                            <h3 className=\"font-medium flex items-center gap-2\">\n                              <Bot className=\"w-4 h-4\" />\n                              {agent.name}\n                            </h3>\n                            {agent.default_task && (\n                              <p className=\"text-sm text-muted-foreground mt-1\">\n                                {agent.default_task}\n                              </p>\n                            )}\n                          </div>\n                          <div className=\"flex gap-2\">\n                            <Button\n                              size=\"sm\"\n                              variant=\"ghost\"\n                              onClick={() => handleExportAgent(agent)}\n                            >\n                              <Download className=\"w-3 h-3 mr-1\" />\n                              Export\n                            </Button>\n                            <Button\n                              size=\"sm\"\n                              variant=\"ghost\"\n                              onClick={() => handleDeleteAgent(agent)}\n                              className=\"text-destructive hover:text-destructive\"\n                            >\n                              <Trash2 className=\"w-3 h-3 mr-1\" />\n                              Delete\n                            </Button>\n                            <Button\n                              size=\"sm\"\n                              onClick={() => handleRunAgent(agent)}\n                            >\n                              <Play className=\"w-3 h-3 mr-1\" />\n                              Run\n                            </Button>\n                          </div>\n                        </div>\n                      </motion.div>\n                    ))}\n                  </div>\n                )}\n              </ScrollArea>\n            </TabsContent>\n\n            <TabsContent value=\"running\" className=\"h-full m-0\">\n              <ScrollArea className=\"h-full px-6 pb-6\">\n                {runningAgents.length === 0 ? (\n                  <div className=\"flex flex-col items-center justify-center h-full text-center\">\n                    <Clock className=\"w-12 h-12 text-muted-foreground mb-4\" />\n                    <p className=\"text-lg font-medium mb-2\">No running agents</p>\n                    <p className=\"text-sm text-muted-foreground\">\n                      Agent executions will appear here when started\n                    </p>\n                  </div>\n                ) : (\n                  <div className=\"grid gap-4 py-4\">\n                    <AnimatePresence mode=\"popLayout\">\n                      {runningAgents.map((run) => (\n                        <motion.div\n                          key={run.id}\n                          layout\n                          initial={{ opacity: 0, scale: 0.95 }}\n                          animate={{ opacity: 1, scale: 1 }}\n                          exit={{ opacity: 0, scale: 0.95 }}\n                          className=\"p-4 border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer\"\n                          onClick={() => handleOpenAgentRun(run)}\n                        >\n                          <div className=\"flex items-start justify-between\">\n                            <div className=\"flex-1\">\n                              <h3 className=\"font-medium flex items-center gap-2\">\n                                {getStatusIcon(run.status)}\n                                {run.agent_name}\n                              </h3>\n                              <p className=\"text-sm text-muted-foreground mt-1\">\n                                {run.task}\n                              </p>\n                              <div className=\"flex items-center gap-4 mt-2 text-xs text-muted-foreground\">\n                                <span>Started: {formatISOTimestamp(run.created_at)}</span>\n                                <Badge variant=\"outline\" className=\"text-xs\">\n                                  {run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n                                </Badge>\n                              </div>\n                            </div>\n                            <Button\n                              size=\"sm\"\n                              variant=\"ghost\"\n                              onClick={(e) => {\n                                e.stopPropagation();\n                                handleOpenAgentRun(run);\n                              }}\n                            >\n                              View\n                            </Button>\n                          </div>\n                        </motion.div>\n                      ))}\n                    </AnimatePresence>\n                  </div>\n                )}\n              </ScrollArea>\n            </TabsContent>\n          </div>\n        </Tabs>\n      </DialogContent>\n    </Dialog>\n\n    {/* Delete Confirmation Dialog */}\n    <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Delete Agent</DialogTitle>\n          <DialogDescription>\n            Are you sure you want to delete \"{agentToDelete?.name}\"? This action cannot be undone.\n          </DialogDescription>\n        </DialogHeader>\n        <div className=\"flex justify-end gap-3 mt-4\">\n          <Button\n            variant=\"outline\"\n            onClick={() => {\n              setShowDeleteDialog(false);\n              setAgentToDelete(null);\n            }}\n          >\n            Cancel\n          </Button>\n          <Button\n            variant=\"destructive\"\n            onClick={confirmDelete}\n          >\n            Delete\n          </Button>\n        </div>\n      </DialogContent>\n    </Dialog>\n\n    {/* GitHub Agent Browser */}\n    <GitHubAgentBrowser\n      isOpen={showGitHubBrowser}\n      onClose={() => setShowGitHubBrowser(false)}\n      onImportSuccess={() => {\n        setShowGitHubBrowser(false);\n        loadAgents(); // Refresh the agents list\n        setToast({ message: \"Agent imported successfully\", type: \"success\" });\n      }}\n    />\n\n    {/* Toast notifications */}\n    {toast && (\n      <Toast\n        message={toast.message}\n        type={toast.type}\n        onDismiss={() => setToast(null)}\n      />\n    )}\n    </>\n  );\n};\n\nexport default AgentsModal;"
  },
  {
    "path": "src/components/AnalyticsConsent.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { BarChart3, Shield, X, Check, Info } from 'lucide-react';\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';\nimport { Button } from '@/components/ui/button';\nimport { Card } from '@/components/ui/card';\nimport { analytics } from '@/lib/analytics';\nimport { cn } from '@/lib/utils';\n\ninterface AnalyticsConsentProps {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onComplete?: () => void;\n}\n\nexport const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({\n  open: controlledOpen,\n  onOpenChange,\n  onComplete,\n}) => {\n  const [internalOpen, setInternalOpen] = useState(false);\n  const [hasShownConsent, setHasShownConsent] = useState(false);\n  \n  const isControlled = controlledOpen !== undefined;\n  const open = isControlled ? controlledOpen : internalOpen;\n  \n  useEffect(() => {\n    // Check if we should show the consent dialog\n    const checkConsent = async () => {\n      await analytics.initialize();\n      const settings = analytics.getSettings();\n      \n      if (!settings?.hasConsented && !hasShownConsent) {\n        if (!isControlled) {\n          setInternalOpen(true);\n        }\n        setHasShownConsent(true);\n      }\n    };\n    \n    checkConsent();\n  }, [isControlled, hasShownConsent]);\n  \n  const handleOpenChange = (newOpen: boolean) => {\n    if (isControlled && onOpenChange) {\n      onOpenChange(newOpen);\n    } else {\n      setInternalOpen(newOpen);\n    }\n  };\n  \n  const handleAccept = async () => {\n    await analytics.enable();\n    handleOpenChange(false);\n    onComplete?.();\n  };\n  \n  const handleDecline = async () => {\n    await analytics.disable();\n    handleOpenChange(false);\n    onComplete?.();\n  };\n  \n  return (\n    <Dialog open={open} onOpenChange={handleOpenChange}>\n      <DialogContent className=\"max-w-2xl p-0 overflow-hidden\">\n        <div className=\"p-6 pb-0\">\n          <DialogHeader>\n            <div className=\"flex items-center gap-3 mb-2\">\n              <div className=\"p-2 bg-purple-100 dark:bg-purple-900/20 rounded-lg\">\n                <BarChart3 className=\"h-6 w-6 text-purple-600 dark:text-purple-400\" />\n              </div>\n              <DialogTitle className=\"text-2xl\">Help Improve opcode</DialogTitle>\n            </div>\n            <DialogDescription className=\"text-base mt-2\">\n              We'd like to collect anonymous usage data to improve your experience.\n            </DialogDescription>\n          </DialogHeader>\n        </div>\n        \n        <div className=\"p-6 space-y-4\">\n          <div className=\"space-y-3\">\n            <Card className=\"p-4 border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-950/20\">\n              <div className=\"flex gap-3\">\n                <Check className=\"h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5\" />\n                <div className=\"space-y-1\">\n                  <p className=\"font-medium text-green-900 dark:text-green-100\">What we collect:</p>\n                  <ul className=\"text-sm text-green-800 dark:text-green-200 space-y-1\">\n                    <li>• Feature usage (which tools and commands you use)</li>\n                    <li>• Performance metrics (app speed and reliability)</li>\n                    <li>• Error reports (to fix bugs and improve stability)</li>\n                    <li>• General usage patterns (session frequency and duration)</li>\n                  </ul>\n                </div>\n              </div>\n            </Card>\n            \n            <Card className=\"p-4 border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/20\">\n              <div className=\"flex gap-3\">\n                <Shield className=\"h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5\" />\n                <div className=\"space-y-1\">\n                  <p className=\"font-medium text-blue-900 dark:text-blue-100\">Your privacy is protected:</p>\n                  <ul className=\"text-sm text-blue-800 dark:text-blue-200 space-y-1\">\n                    <li>• No personal information is collected</li>\n                    <li>• No file contents, paths, or project names</li>\n                    <li>• No API keys or sensitive data</li>\n                    <li>• Completely anonymous with random IDs</li>\n                    <li>• You can opt-out anytime in Settings</li>\n                  </ul>\n                </div>\n              </div>\n            </Card>\n          </div>\n          \n          <div className=\"bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4\">\n            <div className=\"flex gap-2 items-start\">\n              <Info className=\"h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5\" />\n              <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                This data helps us understand which features are most valuable, identify performance \n                issues, and prioritize improvements. Your choice won't affect any functionality.\n              </p>\n            </div>\n          </div>\n        </div>\n        \n        <div className=\"p-6 pt-0 flex gap-3\">\n          <Button\n            onClick={handleDecline}\n            variant=\"outline\"\n            className=\"flex-1\"\n          >\n            No Thanks\n          </Button>\n          <Button\n            onClick={handleAccept}\n            className=\"flex-1 bg-purple-600 hover:bg-purple-700 text-white\"\n          >\n            Allow Analytics\n          </Button>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\ninterface AnalyticsConsentBannerProps {\n  className?: string;\n}\n\nexport const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({\n  className,\n}) => {\n  const [visible, setVisible] = useState(false);\n  const [hasChecked, setHasChecked] = useState(false);\n  \n  useEffect(() => {\n    const checkConsent = async () => {\n      if (hasChecked) return;\n      \n      await analytics.initialize();\n      const settings = analytics.getSettings();\n      \n      if (!settings?.hasConsented) {\n        setVisible(true);\n      }\n      setHasChecked(true);\n    };\n    \n    // Delay banner appearance for better UX\n    const timer = setTimeout(checkConsent, 2000);\n    return () => clearTimeout(timer);\n  }, [hasChecked]);\n  \n  const handleAccept = async () => {\n    await analytics.enable();\n    setVisible(false);\n  };\n  \n  const handleDecline = async () => {\n    await analytics.disable();\n    setVisible(false);\n  };\n  \n  return (\n    <AnimatePresence>\n      {visible && (\n        <motion.div\n          initial={{ y: 100, opacity: 0 }}\n          animate={{ y: 0, opacity: 1 }}\n          exit={{ y: 100, opacity: 0 }}\n          transition={{ type: \"spring\", damping: 25, stiffness: 300 }}\n          className={cn(\n            \"fixed bottom-4 right-4 z-50 max-w-md\",\n            className\n          )}\n        >\n          <Card className=\"p-4 shadow-lg border-purple-200 dark:border-purple-800\">\n            <div className=\"flex items-start gap-3\">\n              <BarChart3 className=\"h-5 w-5 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5\" />\n              <div className=\"space-y-2 flex-1\">\n                <p className=\"text-sm font-medium\">Help improve opcode</p>\n                <p className=\"text-xs text-gray-600 dark:text-gray-400\">\n                  We collect anonymous usage data to improve your experience. No personal data is collected.\n                </p>\n                <div className=\"flex gap-2 pt-1\">\n                  <Button\n                    size=\"sm\"\n                    variant=\"outline\"\n                    onClick={handleDecline}\n                    className=\"text-xs\"\n                  >\n                    No Thanks\n                  </Button>\n                  <Button\n                    size=\"sm\"\n                    onClick={handleAccept}\n                    className=\"text-xs bg-purple-600 hover:bg-purple-700 text-white\"\n                  >\n                    Allow\n                  </Button>\n                </div>\n              </div>\n              <button\n                onClick={() => setVisible(false)}\n                className=\"text-gray-400 hover:text-gray-600 dark:hover:text-gray-300\"\n              >\n                <X className=\"h-4 w-4\" />\n              </button>\n            </div>\n          </Card>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n};\n"
  },
  {
    "path": "src/components/AnalyticsErrorBoundary.tsx",
    "content": "import React, { Component, ErrorInfo, ReactNode } from 'react';\nimport { eventBuilders, analytics } from '@/lib/analytics';\n\ninterface Props {\n  children: ReactNode;\n  fallback?: (error: Error, reset: () => void) => ReactNode;\n}\n\ninterface State {\n  hasError: boolean;\n  error: Error | null;\n}\n\n/**\n * Error boundary component that tracks UI errors to analytics\n */\nexport class AnalyticsErrorBoundary extends Component<Props, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = { hasError: false, error: null };\n  }\n\n  static getDerivedStateFromError(error: Error): State {\n    return { hasError: true, error };\n  }\n\n  componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n    // Track UI error to analytics\n    const event = eventBuilders.uiError({\n      component_name: errorInfo.componentStack?.split('\\n')[0] || 'Unknown',\n      error_type: error.name || 'UnknownError',\n      user_action: undefined, // Could be enhanced with context\n    });\n    \n    analytics.track(event.event, event.properties);\n    \n    // Log to console for debugging\n    console.error('UI Error caught by boundary:', error, errorInfo);\n  }\n\n  reset = () => {\n    this.setState({ hasError: false, error: null });\n  };\n\n  render() {\n    if (this.state.hasError && this.state.error) {\n      // Use custom fallback if provided\n      if (this.props.fallback) {\n        return this.props.fallback(this.state.error, this.reset);\n      }\n      \n      // Default fallback UI\n      return (\n        <div className=\"flex flex-col items-center justify-center min-h-[200px] p-8 text-center\">\n          <h2 className=\"text-lg font-semibold text-destructive mb-2\">\n            Something went wrong\n          </h2>\n          <p className=\"text-sm text-muted-foreground mb-4\">\n            {this.state.error.message}\n          </p>\n          <button\n            onClick={this.reset}\n            className=\"px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90\"\n          >\n            Try again\n          </button>\n        </div>\n      );\n    }\n\n    return this.props.children;\n  }\n}\n\n/**\n * Hook to wrap components with analytics error tracking\n */\nexport function withAnalyticsErrorBoundary<P extends object>(\n  Component: React.ComponentType<P>,\n  fallback?: (error: Error, reset: () => void) => ReactNode\n) {\n  return (props: P) => (\n    <AnalyticsErrorBoundary fallback={fallback}>\n      <Component {...props} />\n    </AnalyticsErrorBoundary>\n  );\n}"
  },
  {
    "path": "src/components/App.cleaned.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { OutputCacheProvider } from \"@/lib/outputCache\";\nimport { TabProvider } from \"@/contexts/TabContext\";\nimport { NFOCredits } from \"@/components/NFOCredits\";\nimport { ClaudeBinaryDialog } from \"@/components/ClaudeBinaryDialog\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { TabManager } from \"@/components/TabManager\";\nimport { TabContent } from \"@/components/TabContent\";\nimport { AgentsModal } from \"@/components/AgentsModal\";\nimport { CustomTitlebar } from \"@/components/CustomTitlebar\";\nimport { useTabState } from \"@/hooks/useTabState\";\n\n/**\n * AppContent component - Contains the main app logic, wrapped by providers\n */\nfunction AppContent() {\n  const { } = useTabState();\n  const [showNFO, setShowNFO] = useState(false);\n  const [showClaudeBinaryDialog, setShowClaudeBinaryDialog] = useState(false);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" | \"info\" } | null>(null);\n  const [showAgentsModal, setShowAgentsModal] = useState(false);\n  const [, setClaudeExecutableExists] = useState(true);\n\n  // Keyboard shortcuts for tab navigation\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;\n      const modKey = isMac ? e.metaKey : e.ctrlKey;\n      \n      if (modKey) {\n        switch (e.key) {\n          case 't':\n            e.preventDefault();\n            window.dispatchEvent(new CustomEvent('create-chat-tab'));\n            break;\n          case 'w':\n            e.preventDefault();\n            window.dispatchEvent(new CustomEvent('close-current-tab'));\n            break;\n          case 'Tab':\n            e.preventDefault();\n            if (e.shiftKey) {\n              window.dispatchEvent(new CustomEvent('switch-to-previous-tab'));\n            } else {\n              window.dispatchEvent(new CustomEvent('switch-to-next-tab'));\n            }\n            break;\n          default:\n            // Handle number keys 1-9\n            const num = parseInt(e.key);\n            if (!isNaN(num) && num >= 1 && num <= 9) {\n              e.preventDefault();\n              window.dispatchEvent(new CustomEvent('switch-to-tab', { detail: num - 1 }));\n            }\n            break;\n        }\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, []);\n\n  // Check if Claude executable exists on mount\n  useEffect(() => {\n    const checkClaudeExecutable = async () => {\n      try {\n        // Check if claude executable exists - method not available in API\n        const exists = true; // Default to true for now\n        if (!exists) {\n          setShowClaudeBinaryDialog(true);\n        }\n      } catch (error) {\n        console.error(\"Error checking Claude executable:\", error);\n      }\n    };\n\n    checkClaudeExecutable();\n  }, []);\n\n  // Custom event handlers\n  useEffect(() => {\n    const handleCreateProjectTab = () => {\n      window.dispatchEvent(new CustomEvent('create-project-tab'));\n    };\n\n    const handleShowNFO = () => setShowNFO(true);\n    const handleShowAgents = () => setShowAgentsModal(true);\n\n    const projectButton = document.getElementById('create-project-tab-btn');\n    if (projectButton) {\n      projectButton.addEventListener('click', handleCreateProjectTab);\n    }\n\n    // Listen for custom events to show modals\n    window.addEventListener('show-nfo', handleShowNFO);\n    window.addEventListener('show-agents-modal', handleShowAgents);\n\n    return () => {\n      if (projectButton) {\n        projectButton.removeEventListener('click', handleCreateProjectTab);\n      }\n      window.removeEventListener('show-nfo', handleShowNFO);\n      window.removeEventListener('show-agents-modal', handleShowAgents);\n    };\n  }, []);\n\n  return (\n    <AnimatePresence mode=\"wait\">\n      <motion.div\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        exit={{ opacity: 0 }}\n        className=\"min-h-screen bg-background flex flex-col rounded-xl overflow-hidden shadow-2xl border border-border/20\"\n      >\n        {/* Custom Titlebar */}\n        <CustomTitlebar\n          onSettingsClick={() => {\n            // Open settings tab or modal\n            window.dispatchEvent(new CustomEvent('create-settings-tab'));\n          }}\n          onAgentsClick={() => {}}\n        />\n        \n        {/* Tab-based interface */}\n        <div className=\"flex-1 flex flex-col\">\n          <TabManager />\n          <TabContent />\n        </div>\n\n        {/* Global Modals */}\n        {showNFO && <NFOCredits onClose={() => setShowNFO(false)} />}\n        \n        <ClaudeBinaryDialog \n          open={showClaudeBinaryDialog} \n          onOpenChange={setShowClaudeBinaryDialog}\n          onSuccess={() => {\n            setClaudeExecutableExists(true);\n            setToast({ message: \"Claude binary path set successfully\", type: \"success\" });\n          }}\n          onError={(message) => {\n            setToast({ message, type: \"error\" });\n          }}\n        />\n        \n        <AgentsModal\n          open={showAgentsModal}\n          onOpenChange={setShowAgentsModal}\n        />\n\n        {/* Toast Container */}\n        {toast && (\n          <ToastContainer>\n            <Toast\n              message={toast.message}\n              type={toast.type}\n              onDismiss={() => setToast(null)}\n            />\n          </ToastContainer>\n        )}\n      </motion.div>\n    </AnimatePresence>\n  );\n}\n\n/**\n * App component - Main entry point with providers\n */\nfunction App() {\n  return (\n    <OutputCacheProvider>\n      <TabProvider>\n        <AppContent />\n      </TabProvider>\n    </OutputCacheProvider>\n  );\n}\n\nexport default App;"
  },
  {
    "path": "src/components/CCAgents.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { \n  Plus, \n  Edit, \n  Trash2, \n  Play,\n  Bot,\n  ArrowLeft,\n  History,\n  Download,\n  Upload,\n  Globe,\n  FileJson,\n  ChevronDown\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { api, type Agent, type AgentRunWithMetrics } from \"@/lib/api\";\nimport { save, open } from \"@tauri-apps/plugin-dialog\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { cn } from \"@/lib/utils\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { CreateAgent } from \"./CreateAgent\";\nimport { AgentExecution } from \"./AgentExecution\";\nimport { AgentRunsList } from \"./AgentRunsList\";\nimport { GitHubAgentBrowser } from \"./GitHubAgentBrowser\";\nimport { ICON_MAP } from \"./IconPicker\";\n\ninterface CCAgentsProps {\n  /**\n   * Callback to go back to the main view\n   */\n  onBack: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n// Available icons for agents - now using all icons from IconPicker\nexport const AGENT_ICONS = ICON_MAP;\n\nexport type AgentIconName = keyof typeof AGENT_ICONS;\n\n/**\n * CCAgents component for managing Claude Code agents\n * \n * @example\n * <CCAgents onBack={() => setView('home')} />\n */\nexport const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {\n  const [agents, setAgents] = useState<Agent[]>([]);\n  const [runs, setRuns] = useState<AgentRunWithMetrics[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [runsLoading, setRunsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  const [currentPage, setCurrentPage] = useState(1);\n  const [view, setView] = useState<\"list\" | \"create\" | \"edit\" | \"execute\">(\"list\");\n  const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);\n  // const [selectedRunId, setSelectedRunId] = useState<number | null>(null);\n  const [showGitHubBrowser, setShowGitHubBrowser] = useState(false);\n  const [showDeleteDialog, setShowDeleteDialog] = useState(false);\n  const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);\n  const [isDeleting, setIsDeleting] = useState(false);\n\n  const AGENTS_PER_PAGE = 9; // 3x3 grid\n\n  useEffect(() => {\n    loadAgents();\n    loadRuns();\n  }, []);\n\n  const loadAgents = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const agentsList = await api.listAgents();\n      setAgents(agentsList);\n    } catch (err) {\n      console.error(\"Failed to load agents:\", err);\n      setError(\"Failed to load agents\");\n      setToast({ message: \"Failed to load agents\", type: \"error\" });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const loadRuns = async () => {\n    try {\n      setRunsLoading(true);\n      const runsList = await api.listAgentRuns();\n      setRuns(runsList);\n    } catch (err) {\n      console.error(\"Failed to load runs:\", err);\n    } finally {\n      setRunsLoading(false);\n    }\n  };\n\n  /**\n   * Initiates the delete agent process by showing the confirmation dialog\n   * @param agent - The agent to be deleted\n   */\n  const handleDeleteAgent = (agent: Agent) => {\n    setAgentToDelete(agent);\n    setShowDeleteDialog(true);\n  };\n\n  /**\n   * Confirms and executes the agent deletion\n   * Only called when user explicitly confirms the deletion\n   */\n  const confirmDeleteAgent = async () => {\n    if (!agentToDelete?.id) return;\n\n    try {\n      setIsDeleting(true);\n      await api.deleteAgent(agentToDelete.id);\n      setToast({ message: \"Agent deleted successfully\", type: \"success\" });\n      await loadAgents();\n      await loadRuns(); // Reload runs as they might be affected\n    } catch (err) {\n      console.error(\"Failed to delete agent:\", err);\n      setToast({ message: \"Failed to delete agent\", type: \"error\" });\n    } finally {\n      setIsDeleting(false);\n      setShowDeleteDialog(false);\n      setAgentToDelete(null);\n    }\n  };\n\n  /**\n   * Cancels the delete operation and closes the dialog\n   */\n  const cancelDeleteAgent = () => {\n    setShowDeleteDialog(false);\n    setAgentToDelete(null);\n  };\n\n  const handleEditAgent = (agent: Agent) => {\n    setSelectedAgent(agent);\n    setView(\"edit\");\n  };\n\n  const handleExecuteAgent = (agent: Agent) => {\n    setSelectedAgent(agent);\n    setView(\"execute\");\n  };\n\n  const handleAgentCreated = async () => {\n    setView(\"list\");\n    await loadAgents();\n    setToast({ message: \"Agent created successfully\", type: \"success\" });\n  };\n\n  const handleAgentUpdated = async () => {\n    setView(\"list\");\n    await loadAgents();\n    setToast({ message: \"Agent updated successfully\", type: \"success\" });\n  };\n\n  // const handleRunClick = (run: AgentRunWithMetrics) => {\n  //   if (run.id) {\n  //     setSelectedRunId(run.id);\n  //     setView(\"viewRun\");\n  //   }\n  // };\n\n  const handleExecutionComplete = async () => {\n    // Reload runs when returning from execution\n    await loadRuns();\n  };\n\n  const handleExportAgent = async (agent: Agent) => {\n    try {\n      // Show native save dialog\n      const filePath = await save({\n        defaultPath: `${agent.name.toLowerCase().replace(/\\s+/g, '-')}.opcode.json`,\n        filters: [{\n          name: 'opcode Agent',\n          extensions: ['opcode.json']\n        }]\n      });\n      \n      if (!filePath) {\n        // User cancelled the dialog\n        return;\n      }\n      \n      // Export the agent to the selected file\n      await invoke('export_agent_to_file', { \n        id: agent.id!,\n        filePath \n      });\n      \n      setToast({ message: `Agent \"${agent.name}\" exported successfully`, type: \"success\" });\n    } catch (err) {\n      console.error(\"Failed to export agent:\", err);\n      setToast({ message: \"Failed to export agent\", type: \"error\" });\n    }\n  };\n\n  const handleImportAgent = async () => {\n    try {\n      // Show native open dialog\n      const filePath = await open({\n        multiple: false,\n        filters: [{\n          name: 'opcode Agent',\n          extensions: ['opcode.json', 'json']\n        }]\n      });\n      \n      if (!filePath) {\n        // User cancelled the dialog\n        return;\n      }\n      \n      // Import the agent from the selected file\n      await api.importAgentFromFile(filePath as string);\n      \n      setToast({ message: \"Agent imported successfully\", type: \"success\" });\n      await loadAgents();\n    } catch (err) {\n      console.error(\"Failed to import agent:\", err);\n      const errorMessage = err instanceof Error ? err.message : \"Failed to import agent\";\n      setToast({ message: errorMessage, type: \"error\" });\n    }\n  };\n\n  // Pagination calculations\n  const totalPages = Math.ceil(agents.length / AGENTS_PER_PAGE);\n  const startIndex = (currentPage - 1) * AGENTS_PER_PAGE;\n  const paginatedAgents = agents.slice(startIndex, startIndex + AGENTS_PER_PAGE);\n\n  const renderIcon = (iconName: string) => {\n    const Icon = AGENT_ICONS[iconName as AgentIconName] || AGENT_ICONS.bot;\n    return <Icon className=\"h-12 w-12\" />;\n  };\n\n  if (view === \"create\") {\n    return (\n      <CreateAgent\n        onBack={() => setView(\"list\")}\n        onAgentCreated={handleAgentCreated}\n      />\n    );\n  }\n\n  if (view === \"edit\" && selectedAgent) {\n    return (\n      <CreateAgent\n        agent={selectedAgent}\n        onBack={() => setView(\"list\")}\n        onAgentCreated={handleAgentUpdated}\n      />\n    );\n  }\n\n  if (view === \"execute\" && selectedAgent) {\n    return (\n      <AgentExecution\n        agent={selectedAgent}\n        onBack={() => {\n          setView(\"list\");\n          handleExecutionComplete();\n        }}\n      />\n    );\n  }\n\n  // Removed viewRun case - now using modal preview in AgentRunsList\n\n  return (\n    <div className={cn(\"flex flex-col h-full bg-background\", className)}>\n      <div className=\"w-full max-w-6xl mx-auto flex flex-col h-full p-6\">\n        {/* Header */}\n        <motion.div\n          initial={{ opacity: 0, y: -20 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.3 }}\n          className=\"mb-6\"\n        >\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-3\">\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                onClick={onBack}\n                className=\"h-8 w-8\"\n              >\n                <ArrowLeft className=\"h-4 w-4\" />\n              </Button>\n              <div>\n                <h1 className=\"text-heading-1\">CC Agents</h1>\n                <p className=\"mt-1 text-body-small text-muted-foreground\">\n                  Manage your Claude Code agents\n                </p>\n              </div>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button\n                    size=\"default\"\n                    variant=\"outline\"\n                    className=\"flex items-center gap-2\"\n                  >\n                    <Download className=\"h-4 w-4\" />\n                    Import\n                    <ChevronDown className=\"h-3 w-3\" />\n                  </Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"end\">\n                  <DropdownMenuItem onClick={handleImportAgent}>\n                    <FileJson className=\"h-4 w-4 mr-2\" />\n                    From File\n                  </DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => setShowGitHubBrowser(true)}>\n                    <Globe className=\"h-4 w-4 mr-2\" />\n                    From GitHub\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n              <Button\n                onClick={() => setView(\"create\")}\n                size=\"default\"\n                className=\"flex items-center gap-2\"\n              >\n                <Plus className=\"h-4 w-4\" />\n                Create CC Agent\n              </Button>\n            </div>\n          </div>\n        </motion.div>\n\n        {/* Error display */}\n        {error && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            className=\"mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-body-small text-destructive\">\n            {error}\n          </motion.div>\n        )}\n\n        {/* Main Content */}\n        <div className=\"flex-1 overflow-y-auto\">\n          <AnimatePresence mode=\"wait\">\n            <motion.div\n              key=\"agents\"\n              initial={{ opacity: 0, y: 20 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -20 }}\n              transition={{ duration: 0.2 }}\n              className=\"pt-6 space-y-8\"\n            >\n              {/* Agents Grid */}\n              <div>\n                {loading ? (\n                  <div className=\"flex items-center justify-center h-64\">\n                    <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-primary\"></div>\n                  </div>\n                ) : agents.length === 0 ? (\n                  <div className=\"flex flex-col items-center justify-center h-64 text-center\">\n                    <Bot className=\"h-16 w-16 text-muted-foreground mb-4\" />\n                    <h3 className=\"text-heading-4 mb-2\">No agents yet</h3>\n                    <p className=\"text-body-small text-muted-foreground mb-4\">\n                      Create your first CC Agent to get started\n                    </p>\n                    <Button onClick={() => setView(\"create\")} size=\"default\">\n                      <Plus className=\"h-4 w-4 mr-2\" />\n                      Create CC Agent\n                    </Button>\n                  </div>\n                ) : (\n                  <>\n                    <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n                      <AnimatePresence mode=\"popLayout\">\n                        {paginatedAgents.map((agent, index) => (\n                          <motion.div\n                            key={agent.id}\n                            initial={{ opacity: 0, scale: 0.9 }}\n                            animate={{ opacity: 1, scale: 1 }}\n                            exit={{ opacity: 0, scale: 0.9 }}\n                            transition={{ duration: 0.2, delay: index * 0.05 }}\n                          >\n                            <Card className=\"h-full hover:shadow-lg transition-shadow\">\n                              <CardContent className=\"p-6 flex flex-col items-center text-center\">\n                                <div className=\"mb-4 p-4 rounded-full bg-primary/10 text-primary\">\n                                  {renderIcon(agent.icon)}\n                                </div>\n                                <h3 className=\"text-heading-4 mb-2\">\n                                  {agent.name}\n                                </h3>\n                                <p className=\"text-caption text-muted-foreground\">\n                                  Created: {new Date(agent.created_at).toLocaleDateString()}\n                                </p>\n                              </CardContent>\n                              <CardFooter className=\"p-4 pt-0 flex justify-center gap-1 flex-wrap\">\n                                <Button\n                                  size=\"sm\"\n                                  variant=\"ghost\"\n                                  onClick={() => handleExecuteAgent(agent)}\n                                  className=\"flex items-center gap-1\"\n                                  title=\"Execute agent\"\n                                >\n                                  <Play className=\"h-3 w-3\" />\n                                  Execute\n                                </Button>\n                                <Button\n                                  size=\"sm\"\n                                  variant=\"ghost\"\n                                  onClick={() => handleEditAgent(agent)}\n                                  className=\"flex items-center gap-1\"\n                                  title=\"Edit agent\"\n                                >\n                                  <Edit className=\"h-3 w-3\" />\n                                  Edit\n                                </Button>\n                                <Button\n                                  size=\"sm\"\n                                  variant=\"ghost\"\n                                  onClick={() => handleExportAgent(agent)}\n                                  className=\"flex items-center gap-1\"\n                                  title=\"Export agent to .opcode.json\"\n                                >\n                                  <Upload className=\"h-3 w-3\" />\n                                  Export\n                                </Button>\n                                <Button\n                                  size=\"sm\"\n                                  variant=\"ghost\"\n                                  onClick={() => handleDeleteAgent(agent)}\n                                  className=\"flex items-center gap-1 text-destructive hover:text-destructive\"\n                                  title=\"Delete agent\"\n                                >\n                                  <Trash2 className=\"h-3 w-3\" />\n                                  Delete\n                                </Button>\n                              </CardFooter>\n                            </Card>\n                          </motion.div>\n                        ))}\n                      </AnimatePresence>\n                    </div>\n\n                    {/* Pagination */}\n                    {totalPages > 1 && (\n                      <div className=\"mt-6 flex justify-center gap-2\">\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          onClick={() => setCurrentPage(p => Math.max(1, p - 1))}\n                          disabled={currentPage === 1}\n                        >\n                          Previous\n                        </Button>\n                        <span className=\"flex items-center px-3 text-body-small\">\n                          Page {currentPage} of {totalPages}\n                        </span>\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}\n                          disabled={currentPage === totalPages}\n                        >\n                          Next\n                        </Button>\n                      </div>\n                    )}\n                  </>\n                )}\n              </div>\n\n              {/* Execution History */}\n              {!loading && agents.length > 0 && (\n                <div className=\"overflow-hidden\">\n                  <div className=\"flex items-center gap-2 mb-4\">\n                    <History className=\"h-5 w-5 text-muted-foreground\" />\n                    <h2 className=\"text-heading-4\">Recent Executions</h2>\n                  </div>\n                  {runsLoading ? (\n                    <div className=\"flex items-center justify-center h-32\">\n                      <div className=\"animate-spin rounded-full h-6 w-6 border-b-2 border-primary\"></div>\n                    </div>\n                  ) : (\n                    <AgentRunsList \n                      runs={runs} \n                    />\n                  )}\n                </div>\n              )}\n            </motion.div>\n          </AnimatePresence>\n        </div>\n      </div>\n\n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n\n      {/* GitHub Agent Browser */}\n      <GitHubAgentBrowser\n        isOpen={showGitHubBrowser}\n        onClose={() => setShowGitHubBrowser(false)}\n        onImportSuccess={async () => {\n          setShowGitHubBrowser(false);\n          await loadAgents();\n          setToast({ message: \"Agent imported successfully from GitHub\", type: \"success\" });\n        }}\n      />\n\n      {/* Delete Confirmation Dialog */}\n      <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>\n        <DialogContent className=\"sm:max-w-md\">\n          <DialogHeader>\n            <DialogTitle className=\"flex items-center gap-2\">\n              <Trash2 className=\"h-5 w-5 text-destructive\" />\n              Delete Agent\n            </DialogTitle>\n            <DialogDescription>\n              Are you sure you want to delete the agent \"{agentToDelete?.name}\"? \n              This action cannot be undone and will permanently remove the agent and all its associated data.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter className=\"flex flex-col-reverse sm:flex-row sm:justify-end gap-2\">\n            <Button\n              variant=\"outline\"\n              onClick={cancelDeleteAgent}\n              disabled={isDeleting}\n              className=\"w-full sm:w-auto\"\n            >\n              Cancel\n            </Button>\n            <Button\n              variant=\"destructive\"\n              onClick={confirmDeleteAgent}\n              disabled={isDeleting}\n              className=\"w-full sm:w-auto\"\n            >\n              {isDeleting ? (\n                <>\n                  <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\" />\n                  Deleting...\n                </>\n              ) : (\n                <>\n                  <Trash2 className=\"h-4 w-4 mr-2\" />\n                  Delete Agent\n                </>\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/CheckpointSettings.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { \n  Wrench,\n  Save,\n  Trash2,\n  HardDrive,\n  AlertCircle,\n  Loader2\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { SelectComponent, type SelectOption } from \"@/components/ui/select\";\nimport { Input } from \"@/components/ui/input\";\nimport { Card } from \"@/components/ui/card\";\nimport { api, type CheckpointStrategy } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\ninterface CheckpointSettingsProps {\n  sessionId: string;\n  projectId: string;\n  projectPath: string;\n  onClose?: () => void;\n  className?: string;\n}\n\n/**\n * CheckpointSettings component for managing checkpoint configuration\n * \n * @example\n * <CheckpointSettings \n *   sessionId={session.id}\n *   projectId={session.project_id}\n *   projectPath={projectPath}\n * />\n */\nexport const CheckpointSettings: React.FC<CheckpointSettingsProps> = ({\n  sessionId,\n  projectId,\n  projectPath,\n  className,\n}) => {\n  const [autoCheckpointEnabled, setAutoCheckpointEnabled] = useState(true);\n  const [checkpointStrategy, setCheckpointStrategy] = useState<CheckpointStrategy>(\"smart\");\n  const [totalCheckpoints, setTotalCheckpoints] = useState(0);\n  const [keepCount, setKeepCount] = useState(10);\n  const [isLoading, setIsLoading] = useState(false);\n  const [isSaving, setIsSaving] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [successMessage, setSuccessMessage] = useState<string | null>(null);\n\n  const strategyOptions: SelectOption[] = [\n    { value: \"manual\", label: \"Manual Only\" },\n    { value: \"per_prompt\", label: \"After Each Prompt\" },\n    { value: \"per_tool_use\", label: \"After Tool Use\" },\n    { value: \"smart\", label: \"Smart (Recommended)\" },\n  ];\n\n  useEffect(() => {\n    loadSettings();\n  }, [sessionId, projectId, projectPath]);\n\n  const loadSettings = async () => {\n    try {\n      setIsLoading(true);\n      setError(null);\n      \n      const settings = await api.getCheckpointSettings(sessionId, projectId, projectPath);\n      setAutoCheckpointEnabled(settings.auto_checkpoint_enabled);\n      setCheckpointStrategy(settings.checkpoint_strategy);\n      setTotalCheckpoints(settings.total_checkpoints);\n    } catch (err) {\n      console.error(\"Failed to load checkpoint settings:\", err);\n      setError(\"Failed to load checkpoint settings\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleSaveSettings = async () => {\n    try {\n      setIsSaving(true);\n      setError(null);\n      setSuccessMessage(null);\n      \n      await api.updateCheckpointSettings(\n        sessionId,\n        projectId,\n        projectPath,\n        autoCheckpointEnabled,\n        checkpointStrategy\n      );\n      \n      setSuccessMessage(\"Settings saved successfully\");\n      setTimeout(() => setSuccessMessage(null), 3000);\n    } catch (err) {\n      console.error(\"Failed to save checkpoint settings:\", err);\n      setError(\"Failed to save checkpoint settings\");\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const handleCleanup = async () => {\n    try {\n      setIsLoading(true);\n      setError(null);\n      setSuccessMessage(null);\n      \n      const removed = await api.cleanupOldCheckpoints(\n        sessionId,\n        projectId,\n        projectPath,\n        keepCount\n      );\n      \n      setSuccessMessage(`Removed ${removed} old checkpoints`);\n      setTimeout(() => setSuccessMessage(null), 3000);\n      \n      // Reload settings to get updated count\n      await loadSettings();\n    } catch (err) {\n      console.error(\"Failed to cleanup checkpoints:\", err);\n      setError(\"Failed to cleanup checkpoints\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, y: 8 }}\n      animate={{ opacity: 1, y: 0 }}\n      exit={{ opacity: 0, y: -8 }}\n      transition={{ duration: 0.15 }}\n      className={cn(\"space-y-4\", className)}\n    >\n      {/* Header */}\n      <div className=\"flex items-center justify-between pb-2 border-b border-border\">\n        <div className=\"flex items-center gap-2.5\">\n          <div className=\"p-1.5 rounded-md bg-primary/10\">\n            <Wrench className=\"h-4 w-4 text-primary\" />\n          </div>\n          <div>\n            <h3 className=\"text-heading-4 font-semibold\">Checkpoint Settings</h3>\n            <p className=\"text-caption text-muted-foreground mt-0.5\">Manage session checkpoints and recovery</p>\n          </div>\n        </div>\n      </div>\n\n      {/* Experimental Feature Warning */}\n      <div className=\"rounded-md border border-amber-200 dark:border-amber-900/50 bg-amber-50 dark:bg-amber-950/20 p-3\">\n        <div className=\"flex items-start gap-2.5\">\n          <AlertCircle className=\"h-4 w-4 text-amber-600 dark:text-amber-500 mt-0.5 flex-shrink-0\" />\n          <div className=\"space-y-0.5\">\n            <p className=\"text-caption font-medium text-amber-900 dark:text-amber-100\">Experimental Feature</p>\n            <p className=\"text-caption text-amber-700 dark:text-amber-300\">\n              Checkpointing may affect directory structure or cause data loss. Use with caution.\n            </p>\n          </div>\n        </div>\n      </div>\n\n      {error && (\n        <motion.div\n          initial={{ opacity: 0, y: 4 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.15 }}\n          className=\"rounded-md border border-destructive/50 bg-destructive/10 p-3\"\n        >\n          <div className=\"flex items-center gap-2\">\n            <AlertCircle className=\"h-3.5 w-3.5 text-destructive\" />\n            <span className=\"text-caption text-destructive\">{error}</span>\n          </div>\n        </motion.div>\n      )}\n\n      {successMessage && (\n        <motion.div\n          initial={{ opacity: 0, y: 4 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.15 }}\n          className=\"rounded-md border border-green-600/50 bg-green-50 dark:bg-green-950/20 p-3\"\n        >\n          <span className=\"text-caption text-green-700 dark:text-green-400\">{successMessage}</span>\n        </motion.div>\n      )}\n\n      {/* Main Settings Card */}\n      <Card className=\"p-5 space-y-4\">\n        {/* Auto-checkpoint toggle */}\n        <div className=\"flex items-center justify-between\">\n          <div className=\"space-y-0.5\">\n            <Label htmlFor=\"auto-checkpoint\" className=\"text-label\">Automatic Checkpoints</Label>\n            <p className=\"text-caption text-muted-foreground\">\n              Automatically create checkpoints based on the selected strategy\n            </p>\n          </div>\n          <Switch\n            id=\"auto-checkpoint\"\n            checked={autoCheckpointEnabled}\n            onCheckedChange={setAutoCheckpointEnabled}\n            disabled={isLoading}\n          />\n        </div>\n\n        {/* Checkpoint strategy */}\n        <div className=\"space-y-2\">\n          <Label htmlFor=\"strategy\" className=\"text-label\">Checkpoint Strategy</Label>\n          <SelectComponent\n            value={checkpointStrategy}\n            onValueChange={(value: string) => setCheckpointStrategy(value as CheckpointStrategy)}\n            options={strategyOptions}\n            disabled={isLoading || !autoCheckpointEnabled}\n          />\n          <p className=\"text-caption text-muted-foreground\">\n            {checkpointStrategy === \"manual\" && \"Checkpoints will only be created manually\"}\n            {checkpointStrategy === \"per_prompt\" && \"A checkpoint will be created after each user prompt\"}\n            {checkpointStrategy === \"per_tool_use\" && \"A checkpoint will be created after each tool use\"}\n            {checkpointStrategy === \"smart\" && \"Checkpoints will be created after destructive operations\"}\n          </p>\n        </div>\n\n        {/* Save button */}\n        <motion.div\n          whileTap={{ scale: 0.97 }}\n          transition={{ duration: 0.15 }}\n        >\n          <Button\n            onClick={handleSaveSettings}\n            disabled={isLoading || isSaving}\n            className=\"w-full\"\n            size=\"default\"\n          >\n            {isSaving ? (\n              <>\n                <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                Saving...\n              </>\n            ) : (\n              <>\n                <Save className=\"h-4 w-4 mr-2\" />\n                Save Settings\n              </>\n            )}\n          </Button>\n        </motion.div>\n      </Card>\n\n      {/* Storage Management Card */}\n      <Card className=\"p-5 space-y-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"space-y-0.5\">\n            <div className=\"flex items-center gap-2\">\n              <HardDrive className=\"h-4 w-4 text-muted-foreground\" />\n              <Label className=\"text-label\">Storage Management</Label>\n            </div>\n            <p className=\"text-caption text-muted-foreground\">\n              Total checkpoints: <span className=\"font-medium text-foreground\">{totalCheckpoints}</span>\n            </p>\n          </div>\n        </div>\n\n        {/* Cleanup settings */}\n        <div className=\"space-y-2\">\n          <Label htmlFor=\"keep-count\" className=\"text-label\">Keep Recent Checkpoints</Label>\n          <div className=\"flex gap-2\">\n            <Input\n              id=\"keep-count\"\n              type=\"number\"\n              min=\"1\"\n              max=\"100\"\n              value={keepCount}\n              onChange={(e) => setKeepCount(parseInt(e.target.value) || 10)}\n              disabled={isLoading}\n              className=\"flex-1 h-9\"\n            />\n            <motion.div\n              whileTap={{ scale: 0.97 }}\n              transition={{ duration: 0.15 }}\n            >\n              <Button\n                variant=\"outline\"\n                onClick={handleCleanup}\n                disabled={isLoading || totalCheckpoints <= keepCount}\n                size=\"sm\"\n                className=\"hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50\"\n              >\n                <Trash2 className=\"h-3.5 w-3.5 mr-1.5\" />\n                Clean Up\n              </Button>\n            </motion.div>\n          </div>\n          <p className=\"text-caption text-muted-foreground\">\n            Remove old checkpoints, keeping only the most recent {keepCount}\n          </p>\n        </div>\n      </Card>\n    </motion.div>\n  );\n}; "
  },
  {
    "path": "src/components/ClaudeBinaryDialog.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { api, type ClaudeInstallation } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { ExternalLink, FileQuestion, Terminal, AlertCircle, Loader2 } from \"lucide-react\";\nimport { ClaudeVersionSelector } from \"./ClaudeVersionSelector\";\n\ninterface ClaudeBinaryDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSuccess: () => void;\n  onError: (message: string) => void;\n}\n\nexport function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: ClaudeBinaryDialogProps) {\n  const [selectedInstallation, setSelectedInstallation] = useState<ClaudeInstallation | null>(null);\n  const [isValidating, setIsValidating] = useState(false);\n  const [hasInstallations, setHasInstallations] = useState(true);\n  const [checkingInstallations, setCheckingInstallations] = useState(true);\n\n  useEffect(() => {\n    if (open) {\n      checkInstallations();\n    }\n  }, [open]);\n\n  const checkInstallations = async () => {\n    try {\n      setCheckingInstallations(true);\n      const installations = await api.listClaudeInstallations();\n      setHasInstallations(installations.length > 0);\n    } catch (error) {\n      // If the API call fails, it means no installations found\n      setHasInstallations(false);\n    } finally {\n      setCheckingInstallations(false);\n    }\n  };\n\n  const handleSave = async () => {\n    if (!selectedInstallation) {\n      onError(\"Please select a Claude installation\");\n      return;\n    }\n\n    setIsValidating(true);\n    try {\n      await api.setClaudeBinaryPath(selectedInstallation.path);\n      onSuccess();\n      onOpenChange(false);\n    } catch (error) {\n      console.error(\"Failed to save Claude binary path:\", error);\n      onError(error instanceof Error ? error.message : \"Failed to save Claude binary path\");\n    } finally {\n      setIsValidating(false);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"sm:max-w-[600px]\">\n        <DialogHeader>\n          <DialogTitle className=\"flex items-center gap-2\">\n            <FileQuestion className=\"w-5 h-5\" />\n            Select Claude Code Installation\n          </DialogTitle>\n          <DialogDescription className=\"space-y-3 mt-4\">\n            {checkingInstallations ? (\n              <div className=\"flex items-center justify-center py-8\">\n                <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n                <span className=\"ml-2 text-sm text-muted-foreground\">Searching for Claude installations...</span>\n              </div>\n            ) : hasInstallations ? (\n              <p>\n                Multiple Claude Code installations were found on your system. \n                Please select which one you'd like to use.\n              </p>\n            ) : (\n              <>\n                <p>\n                  Claude Code was not found in any of the common installation locations. \n                  Please install Claude Code to continue.\n                </p>\n                <div className=\"flex items-center gap-2 p-3 bg-muted rounded-md\">\n                  <AlertCircle className=\"w-4 h-4 text-muted-foreground\" />\n                  <p className=\"text-sm text-muted-foreground\">\n                    <span className=\"font-medium\">Searched locations:</span> PATH, /usr/local/bin, \n                    /opt/homebrew/bin, ~/.nvm/versions/node/*/bin, ~/.claude/local, ~/.local/bin\n                  </p>\n                </div>\n              </>\n            )}\n            {!checkingInstallations && (\n              <div className=\"flex items-center gap-2 p-3 bg-muted rounded-md\">\n                <Terminal className=\"w-4 h-4 text-muted-foreground\" />\n                <p className=\"text-sm text-muted-foreground\">\n                  <span className=\"font-medium\">Tip:</span> You can install Claude Code using{\" \"}\n                  <code className=\"px-1 py-0.5 bg-black/10 dark:bg-white/10 rounded\">npm install -g @claude</code>\n                </p>\n              </div>\n            )}\n          </DialogDescription>\n        </DialogHeader>\n\n        {!checkingInstallations && hasInstallations && (\n          <div className=\"py-4\">\n            <ClaudeVersionSelector\n              onSelect={(installation) => setSelectedInstallation(installation)}\n              selectedPath={null}\n            />\n          </div>\n        )}\n\n        <DialogFooter className=\"gap-3\">\n          <Button\n            variant=\"outline\"\n            onClick={() => window.open(\"https://docs.claude.ai/claude/how-to-install\", \"_blank\")}\n            className=\"mr-auto\"\n          >\n            <ExternalLink className=\"w-4 h-4 mr-2\" />\n            Installation Guide\n          </Button>\n          <Button\n            variant=\"outline\"\n            onClick={() => onOpenChange(false)}\n            disabled={isValidating}\n          >\n            Cancel\n          </Button>\n          <Button \n            onClick={handleSave} \n            disabled={isValidating || !selectedInstallation || !hasInstallations}\n          >\n            {isValidating ? \"Validating...\" : hasInstallations ? \"Save Selection\" : \"No Installations Found\"}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n} "
  },
  {
    "path": "src/components/ClaudeCodeSession.refactored.tsx",
    "content": "import React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { api, type Session } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { open } from \"@tauri-apps/plugin-dialog\";\nimport { FloatingPromptInput, type FloatingPromptInputRef } from \"./FloatingPromptInput\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\nimport { TimelineNavigator } from \"./TimelineNavigator\";\nimport { CheckpointSettings } from \"./CheckpointSettings\";\nimport { SlashCommandsManager } from \"./SlashCommandsManager\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from \"@/components/ui/dialog\";\nimport { SplitPane } from \"@/components/ui/split-pane\";\nimport { WebviewPreview } from \"./WebviewPreview\";\n\n// Import refactored components and hooks\nimport { useClaudeMessages } from \"./claude-code-session/useClaudeMessages\";\nimport { useCheckpoints } from \"./claude-code-session/useCheckpoints\";\nimport { SessionHeader } from \"./claude-code-session/SessionHeader\";\nimport { MessageList } from \"./claude-code-session/MessageList\";\nimport { PromptQueue } from \"./claude-code-session/PromptQueue\";\n\ninterface ClaudeCodeSessionProps {\n  session?: Session;\n  initialProjectPath?: string;\n  onBack: () => void;\n  onProjectSettings?: (projectPath: string) => void;\n  className?: string;\n  onStreamingChange?: (isStreaming: boolean, sessionId: string | null) => void;\n}\n\nexport const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({\n  session,\n  initialProjectPath = \"\",\n  onBack,\n  onProjectSettings,\n  className,\n  onStreamingChange,\n}) => {\n  const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || \"\");\n  const [error, setError] = useState<string | null>(null);\n  const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);\n  const [isFirstPrompt, setIsFirstPrompt] = useState(!session);\n  const [totalTokens, setTotalTokens] = useState(0);\n  const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);\n  const [showTimeline, setShowTimeline] = useState(false);\n  const [showSettings, setShowSettings] = useState(false);\n  const [showForkDialog, setShowForkDialog] = useState(false);\n  const [showSlashCommandsSettings, setShowSlashCommandsSettings] = useState(false);\n  const [forkCheckpointId, setForkCheckpointId] = useState<string | null>(null);\n  const [forkSessionName, setForkSessionName] = useState(\"\");\n  const [queuedPrompts, setQueuedPrompts] = useState<Array<{ id: string; prompt: string; model: \"sonnet\" | \"opus\" }>>([]);\n  const [showPreview, setShowPreview] = useState(false);\n  const [previewUrl, setPreviewUrl] = useState<string | null>(null);\n  const [isPreviewMaximized, setIsPreviewMaximized] = useState(false);\n  const promptInputRef = useRef<FloatingPromptInputRef>(null);\n  const processQueueTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n  // Use custom hooks\n  const {\n    messages,\n    rawJsonlOutput,\n    isStreaming,\n    currentSessionId: _currentSessionId,\n    clearMessages,\n    loadMessages\n  } = useClaudeMessages({\n    onSessionInfo: (info) => {\n      setClaudeSessionId(info.sessionId);\n    },\n    onTokenUpdate: setTotalTokens,\n    onStreamingChange\n  });\n\n  const {\n    checkpoints: _checkpoints,\n    timelineVersion,\n    loadCheckpoints,\n    createCheckpoint: _createCheckpoint,\n    restoreCheckpoint,\n    forkCheckpoint\n  } = useCheckpoints({\n    sessionId: claudeSessionId,\n    projectId: session?.project_id || '',\n    projectPath: projectPath,\n    onToast: (message: string, type: 'success' | 'error') => {\n      console.log(`Toast: ${type} - ${message}`);\n    }\n  });\n\n  // Handle path selection\n  const handleSelectPath = async () => {\n    const selected = await open({\n      directory: true,\n      multiple: false,\n      title: \"Select Project Directory\"\n    });\n    \n    if (selected && typeof selected === 'string') {\n      setProjectPath(selected);\n      setError(null);\n      setIsFirstPrompt(true);\n    }\n  };\n\n  // Handle sending prompts\n  const handleSendPrompt = useCallback(async (prompt: string, model: \"sonnet\" | \"opus\") => {\n    console.log('[TRACE] handleSendPrompt called:');\n    console.log('[TRACE]   prompt length:', prompt.length);\n    console.log('[TRACE]   model:', model);\n    console.log('[TRACE]   projectPath:', projectPath);\n    console.log('[TRACE]   isStreaming:', isStreaming);\n    console.log('[TRACE]   isFirstPrompt:', isFirstPrompt);\n    console.log('[TRACE]   claudeSessionId:', claudeSessionId);\n    \n    if (!projectPath || !prompt.trim()) {\n      console.log('[TRACE] Aborting - no project path or empty prompt');\n      return;\n    }\n\n    // Add to queue if streaming\n    if (isStreaming) {\n      console.log('[TRACE] Currently streaming - adding to queue');\n      const id = Date.now().toString();\n      setQueuedPrompts(prev => [...prev, { id, prompt, model }]);\n      return;\n    }\n\n    try {\n      console.log('[TRACE] Clearing error and starting prompt execution');\n      setError(null);\n      \n      if (isFirstPrompt) {\n        console.log('[TRACE] First prompt - calling api.executeClaudeCode');\n        await api.executeClaudeCode(projectPath, prompt, model);\n        setIsFirstPrompt(false);\n        console.log('[TRACE] executeClaudeCode completed');\n      } else if (claudeSessionId) {\n        console.log('[TRACE] Continue prompt - calling api.continueClaudeCode');\n        await api.continueClaudeCode(projectPath, prompt, model);\n        console.log('[TRACE] continueClaudeCode completed');\n      } else {\n        console.log('[TRACE] No claude session ID for continue');\n      }\n    } catch (error) {\n      console.error(\"[TRACE] Failed to send prompt:\", error);\n      setError(error instanceof Error ? error.message : \"Failed to send prompt\");\n    }\n  }, [projectPath, isStreaming, isFirstPrompt, claudeSessionId]);\n\n  // Process queued prompts\n  const processQueuedPrompts = useCallback(async () => {\n    if (queuedPrompts.length === 0 || isStreaming) return;\n\n    const nextPrompt = queuedPrompts[0];\n    setQueuedPrompts(prev => prev.slice(1));\n    \n    await handleSendPrompt(nextPrompt.prompt, nextPrompt.model);\n  }, [queuedPrompts, isStreaming, handleSendPrompt]);\n\n  // Effect to process queue when streaming stops\n  useEffect(() => {\n    if (!isStreaming && queuedPrompts.length > 0) {\n      processQueueTimeoutRef.current = setTimeout(processQueuedPrompts, 500);\n    }\n    \n    return () => {\n      if (processQueueTimeoutRef.current) {\n        clearTimeout(processQueueTimeoutRef.current);\n      }\n    };\n  }, [isStreaming, queuedPrompts.length, processQueuedPrompts]);\n\n  // Copy handlers\n  const handleCopyAsJsonl = async () => {\n    try {\n      await navigator.clipboard.writeText(rawJsonlOutput.join('\\n'));\n      setCopyPopoverOpen(false);\n      console.log(\"Session output copied as JSONL\");\n    } catch (error) {\n      console.error(\"Failed to copy:\", error);\n    }\n  };\n\n  const handleCopyAsMarkdown = async () => {\n    try {\n      const markdown = messages\n        .filter(msg => msg.type === 'user' || msg.type === 'assistant')\n        .map(msg => {\n          if (msg.type === 'user') {\n            return `## User\\n\\n${msg.message || ''}`;\n          } else if (msg.type === 'assistant' && msg.message?.content) {\n            const content = Array.isArray(msg.message.content) \n              ? msg.message.content.map((item: any) => {\n                  if (typeof item === 'string') return item;\n                  if (item.type === 'text') return item.text;\n                  return '';\n                }).filter(Boolean).join('')\n              : msg.message.content;\n            return `## Assistant\\n\\n${content}`;\n          }\n          return '';\n        })\n        .filter(Boolean)\n        .join('\\n\\n---\\n\\n');\n      \n      await navigator.clipboard.writeText(markdown);\n      setCopyPopoverOpen(false);\n      console.log(\"Session output copied as Markdown\");\n    } catch (error) {\n      console.error(\"Failed to copy:\", error);\n    }\n  };\n\n  // Fork dialog handlers\n  const handleFork = (checkpointId: string) => {\n    setForkCheckpointId(checkpointId);\n    setForkSessionName(\"\");\n    setShowForkDialog(true);\n  };\n\n  const handleConfirmFork = async () => {\n    if (!forkCheckpointId || !forkSessionName.trim()) return;\n\n    const forkedSession = await forkCheckpoint(forkCheckpointId, forkSessionName);\n    if (forkedSession) {\n      setShowForkDialog(false);\n      // Navigate to forked session\n      window.location.reload(); // Or use proper navigation\n    }\n  };\n\n  // Link detection handler\n  const handleLinkDetected = (url: string) => {\n    setPreviewUrl(url);\n    if (!showPreview) {\n      setShowPreview(true);\n    }\n  };\n\n  // Load session if provided\n  useEffect(() => {\n    if (session) {\n      setProjectPath(session.project_path);\n      setClaudeSessionId(session.id);\n      loadMessages(session.id);\n      loadCheckpoints();\n    }\n  }, [session, loadMessages, loadCheckpoints]);\n\n  return (\n    <ErrorBoundary>\n      <div className={cn(\"flex flex-col h-screen bg-background\", className)}>\n        {/* Header */}\n        <SessionHeader\n          projectPath={projectPath}\n          claudeSessionId={claudeSessionId}\n          totalTokens={totalTokens}\n          isStreaming={isStreaming}\n          hasMessages={messages.length > 0}\n          showTimeline={showTimeline}\n          copyPopoverOpen={copyPopoverOpen}\n          onBack={onBack}\n          onSelectPath={handleSelectPath}\n          onCopyAsJsonl={handleCopyAsJsonl}\n          onCopyAsMarkdown={handleCopyAsMarkdown}\n          onToggleTimeline={() => setShowTimeline(!showTimeline)}\n          onProjectSettings={onProjectSettings ? () => onProjectSettings(projectPath) : undefined}\n          onSlashCommandsSettings={() => setShowSlashCommandsSettings(true)}\n          setCopyPopoverOpen={setCopyPopoverOpen}\n        />\n\n        {/* Main content area */}\n        <div className=\"flex-1 flex\">\n          {showPreview ? (\n            <SplitPane\n              left={\n                <div className=\"flex flex-col h-full\">\n                  <MessageList\n                    messages={messages}\n                    projectPath={projectPath}\n                    isStreaming={isStreaming}\n                    onLinkDetected={handleLinkDetected}\n                    className=\"flex-1\"\n                  />\n                  <PromptQueue\n                    queuedPrompts={queuedPrompts}\n                    onRemove={(id) => setQueuedPrompts(prev => prev.filter(p => p.id !== id))}\n                  />\n                </div>\n              }\n              right={\n                <WebviewPreview\n                  initialUrl={previewUrl || \"\"}\n                  isMaximized={isPreviewMaximized}\n                  onClose={() => setShowPreview(false)}\n                  onUrlChange={setPreviewUrl}\n                  onToggleMaximize={() => setIsPreviewMaximized(!isPreviewMaximized)}\n                />\n              }\n              initialSplit={60}\n            />\n          ) : (\n            <div className=\"flex flex-col flex-1\">\n              <MessageList\n                messages={messages}\n                projectPath={projectPath}\n                isStreaming={isStreaming}\n                onLinkDetected={handleLinkDetected}\n                className=\"flex-1\"\n              />\n              <PromptQueue\n                queuedPrompts={queuedPrompts}\n                onRemove={(id) => setQueuedPrompts(prev => prev.filter(p => p.id !== id))}\n              />\n            </div>\n          )}\n        </div>\n\n        {/* Error display */}\n        {error && (\n          <motion.div\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            className=\"mx-4 mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md\"\n          >\n            <p className=\"text-sm text-destructive\">{error}</p>\n          </motion.div>\n        )}\n\n        {/* Floating prompt input */}\n        {projectPath && (\n          <FloatingPromptInput\n            ref={promptInputRef}\n            onSend={handleSendPrompt}\n            disabled={!projectPath}\n            isLoading={isStreaming}\n            onCancel={async () => {\n              if (claudeSessionId && isStreaming) {\n                await api.cancelClaudeExecution(claudeSessionId);\n              }\n            }}\n          />\n        )}\n\n        {/* Timeline Navigator */}\n        {showTimeline && claudeSessionId && session && (\n          <TimelineNavigator\n            sessionId={claudeSessionId}\n            projectId={session.project_id}\n            projectPath={projectPath}\n            currentMessageIndex={messages.length}\n            onCheckpointSelect={async (checkpoint) => {\n              const success = await restoreCheckpoint(checkpoint.id);\n              if (success) {\n                clearMessages();\n                loadMessages(claudeSessionId);\n              }\n            }}\n            onFork={handleFork}\n            refreshVersion={timelineVersion}\n          />\n        )}\n\n        {/* Settings dialogs */}\n        {showSettings && claudeSessionId && session && (\n          <CheckpointSettings\n            sessionId={claudeSessionId}\n            projectId={session.project_id}\n            projectPath={projectPath}\n            onClose={() => setShowSettings(false)}\n          />\n        )}\n\n        {showSlashCommandsSettings && projectPath && (\n          <SlashCommandsManager\n            projectPath={projectPath}\n          />\n        )}\n\n        {/* Fork dialog */}\n        <Dialog open={showForkDialog} onOpenChange={setShowForkDialog}>\n          <DialogContent>\n            <DialogHeader>\n              <DialogTitle>Fork Session from Checkpoint</DialogTitle>\n              <DialogDescription>\n                Create a new session branching from this checkpoint. The original session will remain unchanged.\n              </DialogDescription>\n            </DialogHeader>\n            <div className=\"space-y-4 py-4\">\n              <div>\n                <Label htmlFor=\"fork-name\">New Session Name</Label>\n                <Input\n                  id=\"fork-name\"\n                  value={forkSessionName}\n                  onChange={(e) => setForkSessionName(e.target.value)}\n                  placeholder=\"Enter a name for the forked session\"\n                  className=\"mt-2\"\n                />\n              </div>\n            </div>\n            <DialogFooter>\n              <Button variant=\"outline\" onClick={() => setShowForkDialog(false)}>\n                Cancel\n              </Button>\n              <Button \n                onClick={handleConfirmFork}\n                disabled={!forkSessionName.trim()}\n              >\n                Fork Session\n              </Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n      </div>\n    </ErrorBoundary>\n  );\n};"
  },
  {
    "path": "src/components/ClaudeCodeSession.tsx",
    "content": "import React, { useState, useEffect, useRef, useMemo } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { \n  Copy,\n  ChevronDown,\n  GitBranch,\n  ChevronUp,\n  X,\n  Hash,\n  Wrench\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Popover } from \"@/components/ui/popover\";\nimport { api, type Session } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\n// Conditional imports for Tauri APIs\nlet tauriListen: any;\ntype UnlistenFn = () => void;\n\ntry {\n  if (typeof window !== 'undefined' && window.__TAURI__) {\n    tauriListen = require(\"@tauri-apps/api/event\").listen;\n  }\n} catch (e) {\n  console.log('[ClaudeCodeSession] Tauri APIs not available, using web mode');\n}\n\n// Web-compatible replacements\nconst listen = tauriListen || ((eventName: string, callback: (event: any) => void) => {\n  console.log('[ClaudeCodeSession] Setting up DOM event listener for:', eventName);\n\n  // In web mode, listen for DOM events\n  const domEventHandler = (event: any) => {\n    console.log('[ClaudeCodeSession] DOM event received:', eventName, event.detail);\n    // Simulate Tauri event structure\n    callback({ payload: event.detail });\n  };\n\n  window.addEventListener(eventName, domEventHandler);\n\n  // Return unlisten function\n  return Promise.resolve(() => {\n    console.log('[ClaudeCodeSession] Removing DOM event listener for:', eventName);\n    window.removeEventListener(eventName, domEventHandler);\n  });\n});\nimport { StreamMessage } from \"./StreamMessage\";\nimport { FloatingPromptInput, type FloatingPromptInputRef } from \"./FloatingPromptInput\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\nimport { TimelineNavigator } from \"./TimelineNavigator\";\nimport { CheckpointSettings } from \"./CheckpointSettings\";\nimport { SlashCommandsManager } from \"./SlashCommandsManager\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from \"@/components/ui/dialog\";\nimport { TooltipProvider, TooltipSimple } from \"@/components/ui/tooltip-modern\";\nimport { SplitPane } from \"@/components/ui/split-pane\";\nimport { WebviewPreview } from \"./WebviewPreview\";\nimport type { ClaudeStreamMessage } from \"./AgentExecution\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { useTrackEvent, useComponentMetrics, useWorkflowTracking } from \"@/hooks\";\nimport { SessionPersistenceService } from \"@/services/sessionPersistence\";\n\ninterface ClaudeCodeSessionProps {\n  /**\n   * Optional session to resume (when clicking from SessionList)\n   */\n  session?: Session;\n  /**\n   * Initial project path (for new sessions)\n   */\n  initialProjectPath?: string;\n  /**\n   * Callback to go back\n   */\n  onBack: () => void;\n  /**\n   * Callback to open hooks configuration\n   */\n  onProjectSettings?: (projectPath: string) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n  /**\n   * Callback when streaming state changes\n   */\n  onStreamingChange?: (isStreaming: boolean, sessionId: string | null) => void;\n  /**\n   * Callback when project path changes\n   */\n  onProjectPathChange?: (path: string) => void;\n}\n\n/**\n * ClaudeCodeSession component for interactive Claude Code sessions\n * \n * @example\n * <ClaudeCodeSession onBack={() => setView('projects')} />\n */\nexport const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({\n  session,\n  initialProjectPath = \"\",\n  className,\n  onStreamingChange,\n  onProjectPathChange,\n}) => {\n  const [projectPath] = useState(initialProjectPath || session?.project_path || \"\");\n  const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);\n  const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);\n  const [isFirstPrompt, setIsFirstPrompt] = useState(!session);\n  const [totalTokens, setTotalTokens] = useState(0);\n  const [extractedSessionInfo, setExtractedSessionInfo] = useState<{ sessionId: string; projectId: string } | null>(null);\n  const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);\n  const [showTimeline, setShowTimeline] = useState(false);\n  const [timelineVersion, setTimelineVersion] = useState(0);\n  const [showSettings, setShowSettings] = useState(false);\n  const [showForkDialog, setShowForkDialog] = useState(false);\n  const [showSlashCommandsSettings, setShowSlashCommandsSettings] = useState(false);\n  const [forkCheckpointId, setForkCheckpointId] = useState<string | null>(null);\n  const [forkSessionName, setForkSessionName] = useState(\"\");\n  \n  // Queued prompts state\n  const [queuedPrompts, setQueuedPrompts] = useState<Array<{ id: string; prompt: string; model: \"sonnet\" | \"opus\" }>>([]);\n  \n  // New state for preview feature\n  const [showPreview, setShowPreview] = useState(false);\n  const [previewUrl, setPreviewUrl] = useState(\"\");\n  const [showPreviewPrompt, setShowPreviewPrompt] = useState(false);\n  const [splitPosition, setSplitPosition] = useState(50);\n  const [isPreviewMaximized, setIsPreviewMaximized] = useState(false);\n  \n  // Add collapsed state for queued prompts\n  const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false);\n\n  const parentRef = useRef<HTMLDivElement>(null);\n  const unlistenRefs = useRef<UnlistenFn[]>([]);\n  const hasActiveSessionRef = useRef(false);\n  const floatingPromptRef = useRef<FloatingPromptInputRef>(null);\n  const queuedPromptsRef = useRef<Array<{ id: string; prompt: string; model: \"sonnet\" | \"opus\" }>>([]);\n  const isMountedRef = useRef(true);\n  const isListeningRef = useRef(false);\n  const sessionStartTime = useRef<number>(Date.now());\n  const isIMEComposingRef = useRef(false);\n  \n  // Session metrics state for enhanced analytics\n  const sessionMetrics = useRef({\n    firstMessageTime: null as number | null,\n    promptsSent: 0,\n    toolsExecuted: 0,\n    toolsFailed: 0,\n    filesCreated: 0,\n    filesModified: 0,\n    filesDeleted: 0,\n    codeBlocksGenerated: 0,\n    errorsEncountered: 0,\n    lastActivityTime: Date.now(),\n    toolExecutionTimes: [] as number[],\n    checkpointCount: 0,\n    wasResumed: !!session,\n    modelChanges: [] as Array<{ from: string; to: string; timestamp: number }>,\n  });\n\n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n  useComponentMetrics('ClaudeCodeSession');\n  // const aiTracking = useAIInteractionTracking('sonnet'); // Default model\n  const workflowTracking = useWorkflowTracking('claude_session');\n  \n  // Call onProjectPathChange when component mounts with initial path\n  useEffect(() => {\n    if (onProjectPathChange && projectPath) {\n      onProjectPathChange(projectPath);\n    }\n  }, []); // Only run on mount\n  \n  // Keep ref in sync with state\n  useEffect(() => {\n    queuedPromptsRef.current = queuedPrompts;\n  }, [queuedPrompts]);\n\n  // Get effective session info (from prop or extracted) - use useMemo to ensure it updates\n  const effectiveSession = useMemo(() => {\n    if (session) return session;\n    if (extractedSessionInfo) {\n      return {\n        id: extractedSessionInfo.sessionId,\n        project_id: extractedSessionInfo.projectId,\n        project_path: projectPath,\n        created_at: Date.now(),\n      } as Session;\n    }\n    return null;\n  }, [session, extractedSessionInfo, projectPath]);\n\n  // Filter out messages that shouldn't be displayed\n  const displayableMessages = useMemo(() => {\n    return messages.filter((message, index) => {\n      // Skip meta messages that don't have meaningful content\n      if (message.isMeta && !message.leafUuid && !message.summary) {\n        return false;\n      }\n\n      // Skip user messages that only contain tool results that are already displayed\n      if (message.type === \"user\" && message.message) {\n        if (message.isMeta) return false;\n\n        const msg = message.message;\n        if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) {\n          return false;\n        }\n\n        if (Array.isArray(msg.content)) {\n          let hasVisibleContent = false;\n          for (const content of msg.content) {\n            if (content.type === \"text\") {\n              hasVisibleContent = true;\n              break;\n            }\n            if (content.type === \"tool_result\") {\n              let willBeSkipped = false;\n              if (content.tool_use_id) {\n                // Look for the matching tool_use in previous assistant messages\n                for (let i = index - 1; i >= 0; i--) {\n                  const prevMsg = messages[i];\n                  if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {\n                    const toolUse = prevMsg.message.content.find((c: any) => \n                      c.type === 'tool_use' && c.id === content.tool_use_id\n                    );\n                    if (toolUse) {\n                      const toolName = toolUse.name?.toLowerCase();\n                      const toolsWithWidgets = [\n                        'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read', \n                        'glob', 'bash', 'write', 'grep'\n                      ];\n                      if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {\n                        willBeSkipped = true;\n                      }\n                      break;\n                    }\n                  }\n                }\n              }\n              if (!willBeSkipped) {\n                hasVisibleContent = true;\n                break;\n              }\n            }\n          }\n          if (!hasVisibleContent) {\n            return false;\n          }\n        }\n      }\n      return true;\n    });\n  }, [messages]);\n\n  const rowVirtualizer = useVirtualizer({\n    count: displayableMessages.length,\n    getScrollElement: () => parentRef.current,\n    estimateSize: () => 150, // Estimate, will be dynamically measured\n    overscan: 5,\n  });\n\n  // Debug logging\n  useEffect(() => {\n    console.log('[ClaudeCodeSession] State update:', {\n      projectPath,\n      session,\n      extractedSessionInfo,\n      effectiveSession,\n      messagesCount: messages.length,\n      isLoading\n    });\n  }, [projectPath, session, extractedSessionInfo, effectiveSession, messages.length, isLoading]);\n\n  // Load session history if resuming\n  useEffect(() => {\n    if (session) {\n      // Set the claudeSessionId immediately when we have a session\n      setClaudeSessionId(session.id);\n      \n      // Load session history first, then check for active session\n      const initializeSession = async () => {\n        await loadSessionHistory();\n        // After loading history, check if the session is still active\n        if (isMountedRef.current) {\n          await checkForActiveSession();\n        }\n      };\n      \n      initializeSession();\n    }\n  }, [session]); // Remove hasLoadedSession dependency to ensure it runs on mount\n\n  // Report streaming state changes\n  useEffect(() => {\n    onStreamingChange?.(isLoading, claudeSessionId);\n  }, [isLoading, claudeSessionId, onStreamingChange]);\n\n  // Auto-scroll to bottom when new messages arrive\n  useEffect(() => {\n    if (displayableMessages.length > 0) {\n      // Use a more precise scrolling method to ensure content is fully visible\n      setTimeout(() => {\n        const scrollElement = parentRef.current;\n        if (scrollElement) {\n          // First, scroll using virtualizer to get close to the bottom\n          rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'auto' });\n\n          // Then use direct scroll to ensure we reach the absolute bottom\n          requestAnimationFrame(() => {\n            scrollElement.scrollTo({\n              top: scrollElement.scrollHeight,\n              behavior: 'smooth'\n            });\n          });\n        }\n      }, 50);\n    }\n  }, [displayableMessages.length, rowVirtualizer]);\n\n  // Calculate total tokens from messages\n  useEffect(() => {\n    const tokens = messages.reduce((total, msg) => {\n      if (msg.message?.usage) {\n        return total + msg.message.usage.input_tokens + msg.message.usage.output_tokens;\n      }\n      if (msg.usage) {\n        return total + msg.usage.input_tokens + msg.usage.output_tokens;\n      }\n      return total;\n    }, 0);\n    setTotalTokens(tokens);\n  }, [messages]);\n\n  const loadSessionHistory = async () => {\n    if (!session) return;\n    \n    try {\n      setIsLoading(true);\n      setError(null);\n      \n      const history = await api.loadSessionHistory(session.id, session.project_id);\n      \n      // Save session data for restoration\n      if (history && history.length > 0) {\n        SessionPersistenceService.saveSession(\n          session.id,\n          session.project_id,\n          session.project_path,\n          history.length\n        );\n      }\n      \n      // Convert history to messages format\n      const loadedMessages: ClaudeStreamMessage[] = history.map(entry => ({\n        ...entry,\n        type: entry.type || \"assistant\"\n      }));\n      \n      setMessages(loadedMessages);\n      setRawJsonlOutput(history.map(h => JSON.stringify(h)));\n      \n      // After loading history, we're continuing a conversation\n      setIsFirstPrompt(false);\n      \n      // Scroll to bottom after loading history\n      setTimeout(() => {\n        if (loadedMessages.length > 0) {\n          const scrollElement = parentRef.current;\n          if (scrollElement) {\n            // Use the same improved scrolling method\n            rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' });\n            requestAnimationFrame(() => {\n              scrollElement.scrollTo({\n                top: scrollElement.scrollHeight,\n                behavior: 'auto'\n              });\n            });\n          }\n        }\n      }, 100);\n    } catch (err) {\n      console.error(\"Failed to load session history:\", err);\n      setError(\"Failed to load session history\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const checkForActiveSession = async () => {\n    // If we have a session prop, check if it's still active\n    if (session) {\n      try {\n        const activeSessions = await api.listRunningClaudeSessions();\n        const activeSession = activeSessions.find((s: any) => {\n          if ('process_type' in s && s.process_type && 'ClaudeSession' in s.process_type) {\n            return (s.process_type as any).ClaudeSession.session_id === session.id;\n          }\n          return false;\n        });\n        \n        if (activeSession) {\n          // Session is still active, reconnect to its stream\n          console.log('[ClaudeCodeSession] Found active session, reconnecting:', session.id);\n          // IMPORTANT: Set claudeSessionId before reconnecting\n          setClaudeSessionId(session.id);\n          \n          // Don't add buffered messages here - they've already been loaded by loadSessionHistory\n          // Just set up listeners for new messages\n          \n          // Set up listeners for the active session\n          reconnectToSession(session.id);\n        }\n      } catch (err) {\n        console.error('Failed to check for active sessions:', err);\n      }\n    }\n  };\n\n  const reconnectToSession = async (sessionId: string) => {\n    console.log('[ClaudeCodeSession] Reconnecting to session:', sessionId);\n    \n    // Prevent duplicate listeners\n    if (isListeningRef.current) {\n      console.log('[ClaudeCodeSession] Already listening to session, skipping reconnect');\n      return;\n    }\n    \n    // Clean up previous listeners\n    unlistenRefs.current.forEach(unlisten => unlisten());\n    unlistenRefs.current = [];\n    \n    // IMPORTANT: Set the session ID before setting up listeners\n    setClaudeSessionId(sessionId);\n    \n    // Mark as listening\n    isListeningRef.current = true;\n    \n    // Set up session-specific listeners\n    const outputUnlisten = await listen(`claude-output:${sessionId}`, async (event: any) => {\n      try {\n        console.log('[ClaudeCodeSession] Received claude-output on reconnect:', event.payload);\n        \n        if (!isMountedRef.current) return;\n        \n        // Store raw JSONL\n        setRawJsonlOutput(prev => [...prev, event.payload]);\n        \n        // Parse and display\n        const message = JSON.parse(event.payload) as ClaudeStreamMessage;\n        setMessages(prev => [...prev, message]);\n      } catch (err) {\n        console.error(\"Failed to parse message:\", err, event.payload);\n      }\n    });\n\n    const errorUnlisten = await listen(`claude-error:${sessionId}`, (event: any) => {\n      console.error(\"Claude error:\", event.payload);\n      if (isMountedRef.current) {\n        setError(event.payload);\n      }\n    });\n\n    const completeUnlisten = await listen(`claude-complete:${sessionId}`, async (event: any) => {\n      console.log('[ClaudeCodeSession] Received claude-complete on reconnect:', event.payload);\n      if (isMountedRef.current) {\n        setIsLoading(false);\n        hasActiveSessionRef.current = false;\n      }\n    });\n\n    unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];\n    \n    // Mark as loading to show the session is active\n    if (isMountedRef.current) {\n      setIsLoading(true);\n      hasActiveSessionRef.current = true;\n    }\n  };\n\n  // Project path selection handled by parent tab controls\n\n  const handleSendPrompt = async (prompt: string, model: \"sonnet\" | \"opus\") => {\n    console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath, claudeSessionId, effectiveSession });\n    \n    if (!projectPath) {\n      setError(\"Please select a project directory first\");\n      return;\n    }\n\n    // If already loading, queue the prompt\n    if (isLoading) {\n      const newPrompt = {\n        id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n        prompt,\n        model\n      };\n      setQueuedPrompts(prev => [...prev, newPrompt]);\n      return;\n    }\n\n    try {\n      setIsLoading(true);\n      setError(null);\n      hasActiveSessionRef.current = true;\n      \n      // For resuming sessions, ensure we have the session ID\n      if (effectiveSession && !claudeSessionId) {\n        setClaudeSessionId(effectiveSession.id);\n      }\n      \n      // Only clean up and set up new listeners if not already listening\n      if (!isListeningRef.current) {\n        // Clean up previous listeners\n        unlistenRefs.current.forEach(unlisten => unlisten());\n        unlistenRefs.current = [];\n        \n        // Mark as setting up listeners\n        isListeningRef.current = true;\n        \n        // --------------------------------------------------------------------\n        // 1️⃣  Event Listener Setup Strategy\n        // --------------------------------------------------------------------\n        // Claude Code may emit a *new* session_id even when we pass --resume. If\n        // we listen only on the old session-scoped channel we will miss the\n        // stream until the user navigates away & back. To avoid this we:\n        //   • Always start with GENERIC listeners (no suffix) so we catch the\n        //     very first \"system:init\" message regardless of the session id.\n        //   • Once that init message provides the *actual* session_id, we\n        //     dynamically switch to session-scoped listeners and stop the\n        //     generic ones to prevent duplicate handling.\n        // --------------------------------------------------------------------\n\n        console.log('[ClaudeCodeSession] Setting up generic event listeners first');\n\n        let currentSessionId: string | null = claudeSessionId || effectiveSession?.id || null;\n\n        // Helper to attach session-specific listeners **once we are sure**\n        const attachSessionSpecificListeners = async (sid: string) => {\n          console.log('[ClaudeCodeSession] Attaching session-specific listeners for', sid);\n\n          const specificOutputUnlisten = await listen(`claude-output:${sid}`, (evt: any) => {\n            handleStreamMessage(evt.payload);\n          });\n\n          const specificErrorUnlisten = await listen(`claude-error:${sid}`, (evt: any) => {\n            console.error('Claude error (scoped):', evt.payload);\n            setError(evt.payload);\n          });\n\n          const specificCompleteUnlisten = await listen(`claude-complete:${sid}`, (evt: any) => {\n            console.log('[ClaudeCodeSession] Received claude-complete (scoped):', evt.payload);\n            processComplete(evt.payload);\n          });\n\n          // Replace existing unlisten refs with these new ones (after cleaning up)\n          unlistenRefs.current.forEach((u) => u());\n          unlistenRefs.current = [specificOutputUnlisten, specificErrorUnlisten, specificCompleteUnlisten];\n        };\n\n        // Generic listeners (catch-all)\n        const genericOutputUnlisten = await listen('claude-output', async (event: any) => {\n          handleStreamMessage(event.payload);\n\n          // Attempt to extract session_id on the fly (for the very first init)\n          try {\n            const msg = JSON.parse(event.payload) as ClaudeStreamMessage;\n            if (msg.type === 'system' && msg.subtype === 'init' && msg.session_id) {\n              if (!currentSessionId || currentSessionId !== msg.session_id) {\n                console.log('[ClaudeCodeSession] Detected new session_id from generic listener:', msg.session_id);\n                currentSessionId = msg.session_id;\n                setClaudeSessionId(msg.session_id);\n\n                // If we haven't extracted session info before, do it now\n                if (!extractedSessionInfo) {\n                  const projectId = projectPath.replace(/[^a-zA-Z0-9]/g, '-');\n                  setExtractedSessionInfo({ sessionId: msg.session_id, projectId });\n                  \n                  // Save session data for restoration\n                  SessionPersistenceService.saveSession(\n                    msg.session_id,\n                    projectId,\n                    projectPath,\n                    messages.length\n                  );\n                }\n\n                // Switch to session-specific listeners\n                await attachSessionSpecificListeners(msg.session_id);\n              }\n            }\n          } catch {\n            /* ignore parse errors */\n          }\n        });\n\n        // Helper to process any JSONL stream message string or object\n        function handleStreamMessage(payload: string | ClaudeStreamMessage) {\n          try {\n            // Don't process if component unmounted\n            if (!isMountedRef.current) return;\n            \n            let message: ClaudeStreamMessage;\n            let rawPayload: string;\n            \n            if (typeof payload === 'string') {\n              // Tauri mode: payload is a JSON string\n              rawPayload = payload;\n              message = JSON.parse(payload) as ClaudeStreamMessage;\n            } else {\n              // Web mode: payload is already parsed object\n              message = payload;\n              rawPayload = JSON.stringify(payload);\n            }\n            \n            console.log('[ClaudeCodeSession] handleStreamMessage - message type:', message.type);\n\n            // Store raw JSONL\n            setRawJsonlOutput((prev) => [...prev, rawPayload]);\n\n            // Track enhanced tool execution\n            if (message.type === 'assistant' && message.message?.content) {\n              const toolUses = message.message.content.filter((c: any) => c.type === 'tool_use');\n              toolUses.forEach((toolUse: any) => {\n                // Increment tools executed counter\n                sessionMetrics.current.toolsExecuted += 1;\n                sessionMetrics.current.lastActivityTime = Date.now();\n\n                // Track file operations\n                const toolName = toolUse.name?.toLowerCase() || '';\n                if (toolName.includes('create') || toolName.includes('write')) {\n                  sessionMetrics.current.filesCreated += 1;\n                } else if (toolName.includes('edit') || toolName.includes('multiedit') || toolName.includes('search_replace')) {\n                  sessionMetrics.current.filesModified += 1;\n                } else if (toolName.includes('delete')) {\n                  sessionMetrics.current.filesDeleted += 1;\n                }\n\n                // Track tool start - we'll track completion when we get the result\n                workflowTracking.trackStep(toolUse.name);\n              });\n            }\n\n            // Track tool results\n            if (message.type === 'user' && message.message?.content) {\n              const toolResults = message.message.content.filter((c: any) => c.type === 'tool_result');\n              toolResults.forEach((result: any) => {\n                const isError = result.is_error || false;\n                // Note: We don't have execution time here, but we can track success/failure\n                if (isError) {\n                  sessionMetrics.current.toolsFailed += 1;\n                  sessionMetrics.current.errorsEncountered += 1;\n\n                  trackEvent.enhancedError({\n                    error_type: 'tool_execution',\n                    error_code: 'tool_failed',\n                    error_message: result.content,\n                    context: `Tool execution failed`,\n                    user_action_before_error: 'executing_tool',\n                    recovery_attempted: false,\n                    recovery_successful: false,\n                    error_frequency: 1,\n                    stack_trace_hash: undefined\n                  });\n                }\n              });\n            }\n\n            // Track code blocks generated\n            if (message.type === 'assistant' && message.message?.content) {\n              const codeBlocks = message.message.content.filter((c: any) =>\n                c.type === 'text' && c.text?.includes('```')\n              );\n              if (codeBlocks.length > 0) {\n                // Count code blocks in text content\n                codeBlocks.forEach((block: any) => {\n                  const matches = (block.text.match(/```/g) || []).length;\n                  sessionMetrics.current.codeBlocksGenerated += Math.floor(matches / 2);\n                });\n              }\n            }\n\n            // Track errors in system messages\n            if (message.type === 'system' && (message.subtype === 'error' || message.error)) {\n              sessionMetrics.current.errorsEncountered += 1;\n            }\n\n            setMessages((prev) => [...prev, message]);\n          } catch (err) {\n            console.error('Failed to parse message:', err, payload);\n          }\n        }\n\n        // Helper to handle completion events (both generic and scoped)\n        const processComplete = async (success: boolean) => {\n          setIsLoading(false);\n          hasActiveSessionRef.current = false;\n          isListeningRef.current = false; // Reset listening state\n          \n          // Track enhanced session stopped metrics when session completes\n          if (effectiveSession && claudeSessionId) {\n            const sessionStartTimeValue = messages.length > 0 ? messages[0].timestamp || Date.now() : Date.now();\n            const duration = Date.now() - sessionStartTimeValue;\n            const metrics = sessionMetrics.current;\n            const timeToFirstMessage = metrics.firstMessageTime \n              ? metrics.firstMessageTime - sessionStartTime.current \n              : undefined;\n            const idleTime = Date.now() - metrics.lastActivityTime;\n            const avgResponseTime = metrics.toolExecutionTimes.length > 0\n              ? metrics.toolExecutionTimes.reduce((a, b) => a + b, 0) / metrics.toolExecutionTimes.length\n              : undefined;\n            \n            trackEvent.enhancedSessionStopped({\n              // Basic metrics\n              duration_ms: duration,\n              messages_count: messages.length,\n              reason: success ? 'completed' : 'error',\n              \n              // Timing metrics\n              time_to_first_message_ms: timeToFirstMessage,\n              average_response_time_ms: avgResponseTime,\n              idle_time_ms: idleTime,\n              \n              // Interaction metrics\n              prompts_sent: metrics.promptsSent,\n              tools_executed: metrics.toolsExecuted,\n              tools_failed: metrics.toolsFailed,\n              files_created: metrics.filesCreated,\n              files_modified: metrics.filesModified,\n              files_deleted: metrics.filesDeleted,\n              \n              // Content metrics\n              total_tokens_used: totalTokens,\n              code_blocks_generated: metrics.codeBlocksGenerated,\n              errors_encountered: metrics.errorsEncountered,\n              \n              // Session context\n              model: metrics.modelChanges.length > 0 \n                ? metrics.modelChanges[metrics.modelChanges.length - 1].to \n                : 'sonnet',\n              has_checkpoints: metrics.checkpointCount > 0,\n              checkpoint_count: metrics.checkpointCount,\n              was_resumed: metrics.wasResumed,\n              \n              // Agent context (if applicable)\n              agent_type: undefined, // TODO: Pass from agent execution\n              agent_name: undefined, // TODO: Pass from agent execution\n              agent_success: success,\n              \n              // Stop context\n              stop_source: 'completed',\n              final_state: success ? 'success' : 'failed',\n              has_pending_prompts: queuedPrompts.length > 0,\n              pending_prompts_count: queuedPrompts.length,\n            });\n          }\n\n          if (effectiveSession && success) {\n            try {\n              const settings = await api.getCheckpointSettings(\n                effectiveSession.id,\n                effectiveSession.project_id,\n                projectPath\n              );\n\n              if (settings.auto_checkpoint_enabled) {\n                await api.checkAutoCheckpoint(\n                  effectiveSession.id,\n                  effectiveSession.project_id,\n                  projectPath,\n                  prompt\n                );\n                // Reload timeline to show new checkpoint\n                setTimelineVersion((v) => v + 1);\n              }\n            } catch (err) {\n              console.error('Failed to check auto checkpoint:', err);\n            }\n          }\n\n          // Process queued prompts after completion\n          if (queuedPromptsRef.current.length > 0) {\n            const [nextPrompt, ...remainingPrompts] = queuedPromptsRef.current;\n            setQueuedPrompts(remainingPrompts);\n            \n            // Small delay to ensure UI updates\n            setTimeout(() => {\n              handleSendPrompt(nextPrompt.prompt, nextPrompt.model);\n            }, 100);\n          }\n        };\n\n        const genericErrorUnlisten = await listen('claude-error', (evt: any) => {\n          console.error('Claude error:', evt.payload);\n          setError(evt.payload);\n        });\n\n        const genericCompleteUnlisten = await listen('claude-complete', (evt: any) => {\n          console.log('[ClaudeCodeSession] Received claude-complete (generic):', evt.payload);\n          processComplete(evt.payload);\n        });\n\n        // Store the generic unlisteners for now; they may be replaced later.\n        unlistenRefs.current = [genericOutputUnlisten, genericErrorUnlisten, genericCompleteUnlisten];\n\n        // --------------------------------------------------------------------\n        // 2️⃣  Auto-checkpoint logic moved after listener setup (unchanged)\n        // --------------------------------------------------------------------\n\n        // Add the user message immediately to the UI (after setting up listeners)\n        const userMessage: ClaudeStreamMessage = {\n          type: \"user\",\n          message: {\n            content: [\n              {\n                type: \"text\",\n                text: prompt\n              }\n            ]\n          }\n        };\n        setMessages(prev => [...prev, userMessage]);\n        \n        // Update session metrics\n        sessionMetrics.current.promptsSent += 1;\n        sessionMetrics.current.lastActivityTime = Date.now();\n        if (!sessionMetrics.current.firstMessageTime) {\n          sessionMetrics.current.firstMessageTime = Date.now();\n        }\n        \n        // Track model changes\n        const lastModel = sessionMetrics.current.modelChanges.length > 0 \n          ? sessionMetrics.current.modelChanges[sessionMetrics.current.modelChanges.length - 1].to\n          : (sessionMetrics.current.wasResumed ? 'sonnet' : model); // Default to sonnet if resumed\n        \n        if (lastModel !== model) {\n          sessionMetrics.current.modelChanges.push({\n            from: lastModel,\n            to: model,\n            timestamp: Date.now()\n          });\n        }\n        \n        // Track enhanced prompt submission\n        const codeBlockMatches = prompt.match(/```[\\s\\S]*?```/g) || [];\n        const hasCode = codeBlockMatches.length > 0;\n        const conversationDepth = messages.filter(m => m.user_message).length;\n        const sessionAge = sessionStartTime.current ? Date.now() - sessionStartTime.current : 0;\n        const wordCount = prompt.split(/\\s+/).filter(word => word.length > 0).length;\n        \n        trackEvent.enhancedPromptSubmitted({\n          prompt_length: prompt.length,\n          model: model,\n          has_attachments: false, // TODO: Add attachment support when implemented\n          source: 'keyboard', // TODO: Track actual source (keyboard vs button)\n          word_count: wordCount,\n          conversation_depth: conversationDepth,\n          prompt_complexity: wordCount < 20 ? 'simple' : wordCount < 100 ? 'moderate' : 'complex',\n          contains_code: hasCode,\n          language_detected: hasCode ? codeBlockMatches?.[0]?.match(/```(\\w+)/)?.[1] : undefined,\n          session_age_ms: sessionAge\n        });\n\n        // Execute the appropriate command\n        if (effectiveSession && !isFirstPrompt) {\n          console.log('[ClaudeCodeSession] Resuming session:', effectiveSession.id);\n          trackEvent.sessionResumed(effectiveSession.id);\n          trackEvent.modelSelected(model);\n          await api.resumeClaudeCode(projectPath, effectiveSession.id, prompt, model);\n        } else {\n          console.log('[ClaudeCodeSession] Starting new session');\n          setIsFirstPrompt(false);\n          trackEvent.sessionCreated(model, 'prompt_input');\n          trackEvent.modelSelected(model);\n          await api.executeClaudeCode(projectPath, prompt, model);\n        }\n      }\n    } catch (err) {\n      console.error(\"Failed to send prompt:\", err);\n      setError(\"Failed to send prompt\");\n      setIsLoading(false);\n      hasActiveSessionRef.current = false;\n    }\n  };\n\n  const handleCopyAsJsonl = async () => {\n    const jsonl = rawJsonlOutput.join('\\n');\n    await navigator.clipboard.writeText(jsonl);\n    setCopyPopoverOpen(false);\n  };\n\n  const handleCopyAsMarkdown = async () => {\n    let markdown = `# Claude Code Session\\n\\n`;\n    markdown += `**Project:** ${projectPath}\\n`;\n    markdown += `**Date:** ${new Date().toISOString()}\\n\\n`;\n    markdown += `---\\n\\n`;\n\n    for (const msg of messages) {\n      if (msg.type === \"system\" && msg.subtype === \"init\") {\n        markdown += `## System Initialization\\n\\n`;\n        markdown += `- Session ID: \\`${msg.session_id || 'N/A'}\\`\\n`;\n        markdown += `- Model: \\`${msg.model || 'default'}\\`\\n`;\n        if (msg.cwd) markdown += `- Working Directory: \\`${msg.cwd}\\`\\n`;\n        if (msg.tools?.length) markdown += `- Tools: ${msg.tools.join(', ')}\\n`;\n        markdown += `\\n`;\n      } else if (msg.type === \"assistant\" && msg.message) {\n        markdown += `## Assistant\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            const textContent = typeof content.text === 'string' \n              ? content.text \n              : (content.text?.text || JSON.stringify(content.text || content));\n            markdown += `${textContent}\\n\\n`;\n          } else if (content.type === \"tool_use\") {\n            markdown += `### Tool: ${content.name}\\n\\n`;\n            markdown += `\\`\\`\\`json\\n${JSON.stringify(content.input, null, 2)}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n        if (msg.message.usage) {\n          markdown += `*Tokens: ${msg.message.usage.input_tokens} in, ${msg.message.usage.output_tokens} out*\\n\\n`;\n        }\n      } else if (msg.type === \"user\" && msg.message) {\n        markdown += `## User\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            const textContent = typeof content.text === 'string' \n              ? content.text \n              : (content.text?.text || JSON.stringify(content.text));\n            markdown += `${textContent}\\n\\n`;\n          } else if (content.type === \"tool_result\") {\n            markdown += `### Tool Result\\n\\n`;\n            let contentText = '';\n            if (typeof content.content === 'string') {\n              contentText = content.content;\n            } else if (content.content && typeof content.content === 'object') {\n              if (content.content.text) {\n                contentText = content.content.text;\n              } else if (Array.isArray(content.content)) {\n                contentText = content.content\n                  .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n                  .join('\\n');\n              } else {\n                contentText = JSON.stringify(content.content, null, 2);\n              }\n            }\n            markdown += `\\`\\`\\`\\n${contentText}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n      } else if (msg.type === \"result\") {\n        markdown += `## Execution Result\\n\\n`;\n        if (msg.result) {\n          markdown += `${msg.result}\\n\\n`;\n        }\n        if (msg.error) {\n          markdown += `**Error:** ${msg.error}\\n\\n`;\n        }\n      }\n    }\n\n    await navigator.clipboard.writeText(markdown);\n    setCopyPopoverOpen(false);\n  };\n\n  const handleCheckpointSelect = async () => {\n    // Reload messages from the checkpoint\n    await loadSessionHistory();\n    // Ensure timeline reloads to highlight current checkpoint\n    setTimelineVersion((v) => v + 1);\n  };\n  \n  const handleCheckpointCreated = () => {\n    // Update checkpoint count in session metrics\n    sessionMetrics.current.checkpointCount += 1;\n  };\n\n  const handleCancelExecution = async () => {\n    if (!claudeSessionId || !isLoading) return;\n    \n    try {\n      const sessionStartTime = messages.length > 0 ? messages[0].timestamp || Date.now() : Date.now();\n      const duration = Date.now() - sessionStartTime;\n      \n      await api.cancelClaudeExecution(claudeSessionId);\n      \n      // Calculate metrics for enhanced analytics\n      const metrics = sessionMetrics.current;\n      const timeToFirstMessage = metrics.firstMessageTime \n        ? metrics.firstMessageTime - sessionStartTime.current \n        : undefined;\n      const idleTime = Date.now() - metrics.lastActivityTime;\n      const avgResponseTime = metrics.toolExecutionTimes.length > 0\n        ? metrics.toolExecutionTimes.reduce((a, b) => a + b, 0) / metrics.toolExecutionTimes.length\n        : undefined;\n      \n      // Track enhanced session stopped\n      trackEvent.enhancedSessionStopped({\n        // Basic metrics\n        duration_ms: duration,\n        messages_count: messages.length,\n        reason: 'user_stopped',\n        \n        // Timing metrics\n        time_to_first_message_ms: timeToFirstMessage,\n        average_response_time_ms: avgResponseTime,\n        idle_time_ms: idleTime,\n        \n        // Interaction metrics\n        prompts_sent: metrics.promptsSent,\n        tools_executed: metrics.toolsExecuted,\n        tools_failed: metrics.toolsFailed,\n        files_created: metrics.filesCreated,\n        files_modified: metrics.filesModified,\n        files_deleted: metrics.filesDeleted,\n        \n        // Content metrics\n        total_tokens_used: totalTokens,\n        code_blocks_generated: metrics.codeBlocksGenerated,\n        errors_encountered: metrics.errorsEncountered,\n        \n        // Session context\n        model: metrics.modelChanges.length > 0 \n          ? metrics.modelChanges[metrics.modelChanges.length - 1].to \n          : 'sonnet', // Default to sonnet\n        has_checkpoints: metrics.checkpointCount > 0,\n        checkpoint_count: metrics.checkpointCount,\n        was_resumed: metrics.wasResumed,\n        \n        // Agent context (if applicable)\n        agent_type: undefined, // TODO: Pass from agent execution\n        agent_name: undefined, // TODO: Pass from agent execution\n        agent_success: undefined, // TODO: Pass from agent execution\n        \n        // Stop context\n        stop_source: 'user_button',\n        final_state: 'cancelled',\n        has_pending_prompts: queuedPrompts.length > 0,\n        pending_prompts_count: queuedPrompts.length,\n      });\n      \n      // Clean up listeners\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      unlistenRefs.current = [];\n      \n      // Reset states\n      setIsLoading(false);\n      hasActiveSessionRef.current = false;\n      isListeningRef.current = false;\n      setError(null);\n      \n      // Clear queued prompts\n      setQueuedPrompts([]);\n      \n      // Add a message indicating the session was cancelled\n      const cancelMessage: ClaudeStreamMessage = {\n        type: \"system\",\n        subtype: \"info\",\n        result: \"Session cancelled by user\",\n        timestamp: new Date().toISOString()\n      };\n      setMessages(prev => [...prev, cancelMessage]);\n    } catch (err) {\n      console.error(\"Failed to cancel execution:\", err);\n      \n      // Even if backend fails, we should update UI to reflect stopped state\n      // Add error message but still stop the UI loading state\n      const errorMessage: ClaudeStreamMessage = {\n        type: \"system\",\n        subtype: \"error\",\n        result: `Failed to cancel execution: ${err instanceof Error ? err.message : 'Unknown error'}. The process may still be running in the background.`,\n        timestamp: new Date().toISOString()\n      };\n      setMessages(prev => [...prev, errorMessage]);\n      \n      // Clean up listeners anyway\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      unlistenRefs.current = [];\n      \n      // Reset states to allow user to continue\n      setIsLoading(false);\n      hasActiveSessionRef.current = false;\n      isListeningRef.current = false;\n      setError(null);\n    }\n  };\n\n  const handleFork = (checkpointId: string) => {\n    setForkCheckpointId(checkpointId);\n    setForkSessionName(`Fork-${new Date().toISOString().slice(0, 10)}`);\n    setShowForkDialog(true);\n  };\n\n  const handleCompositionStart = () => {\n    isIMEComposingRef.current = true;\n  };\n\n  const handleCompositionEnd = () => {\n    setTimeout(() => {\n      isIMEComposingRef.current = false;\n    }, 0);\n  };\n\n  const handleConfirmFork = async () => {\n    if (!forkCheckpointId || !forkSessionName.trim() || !effectiveSession) return;\n    \n    try {\n      setIsLoading(true);\n      setError(null);\n      \n      const newSessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n      await api.forkFromCheckpoint(\n        forkCheckpointId,\n        effectiveSession.id,\n        effectiveSession.project_id,\n        projectPath,\n        newSessionId,\n        forkSessionName\n      );\n      \n      // Open the new forked session\n      // You would need to implement navigation to the new session\n      console.log(\"Forked to new session:\", newSessionId);\n      \n      setShowForkDialog(false);\n      setForkCheckpointId(null);\n      setForkSessionName(\"\");\n    } catch (err) {\n      console.error(\"Failed to fork checkpoint:\", err);\n      setError(\"Failed to fork checkpoint\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  // Handle URL detection from terminal output\n  const handleLinkDetected = (url: string) => {\n    if (!showPreview && !showPreviewPrompt) {\n      setPreviewUrl(url);\n      setShowPreviewPrompt(true);\n    }\n  };\n\n  const handleClosePreview = () => {\n    setShowPreview(false);\n    setIsPreviewMaximized(false);\n    // Keep the previewUrl so it can be restored when reopening\n  };\n\n  const handlePreviewUrlChange = (url: string) => {\n    console.log('[ClaudeCodeSession] Preview URL changed to:', url);\n    setPreviewUrl(url);\n  };\n\n  const handleTogglePreviewMaximize = () => {\n    setIsPreviewMaximized(!isPreviewMaximized);\n    // Reset split position when toggling maximize\n    if (isPreviewMaximized) {\n      setSplitPosition(50);\n    }\n  };\n\n  // Cleanup event listeners and track mount state\n  useEffect(() => {\n    isMountedRef.current = true;\n    \n    return () => {\n      console.log('[ClaudeCodeSession] Component unmounting, cleaning up listeners');\n      isMountedRef.current = false;\n      isListeningRef.current = false;\n      \n      // Track session completion with engagement metrics\n      if (effectiveSession) {\n        trackEvent.sessionCompleted();\n        \n        // Track session engagement\n        const sessionDuration = sessionStartTime.current ? Date.now() - sessionStartTime.current : 0;\n        const messageCount = messages.filter(m => m.user_message).length;\n        const toolsUsed = new Set<string>();\n        messages.forEach(msg => {\n          if (msg.type === 'assistant' && msg.message?.content) {\n            const tools = msg.message.content.filter((c: any) => c.type === 'tool_use');\n            tools.forEach((tool: any) => toolsUsed.add(tool.name));\n          }\n        });\n        \n        // Calculate engagement score (0-100)\n        const engagementScore = Math.min(100, \n          (messageCount * 10) + \n          (toolsUsed.size * 5) + \n          (sessionDuration > 300000 ? 20 : sessionDuration / 15000) // 5+ min session gets 20 points\n        );\n        \n        trackEvent.sessionEngagement({\n          session_duration_ms: sessionDuration,\n          messages_sent: messageCount,\n          tools_used: Array.from(toolsUsed),\n          files_modified: 0, // TODO: Track file modifications\n          engagement_score: Math.round(engagementScore)\n        });\n      }\n      \n      // Clean up listeners\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      unlistenRefs.current = [];\n      \n      // Clear checkpoint manager when session ends\n      if (effectiveSession) {\n        api.clearCheckpointManager(effectiveSession.id).catch(err => {\n          console.error(\"Failed to clear checkpoint manager:\", err);\n        });\n      }\n    };\n  }, [effectiveSession, projectPath]);\n\n  const messagesList = (\n    <div\n      ref={parentRef}\n      className=\"flex-1 overflow-y-auto relative pb-20\"\n      style={{\n        contain: 'strict',\n      }}\n    >\n      <div\n        className=\"relative w-full max-w-6xl mx-auto px-4 pt-8 pb-4\"\n        style={{\n          height: `${Math.max(rowVirtualizer.getTotalSize(), 100)}px`,\n          minHeight: '100px',\n        }}\n      >\n        <AnimatePresence>\n          {rowVirtualizer.getVirtualItems().map((virtualItem) => {\n            const message = displayableMessages[virtualItem.index];\n            return (\n              <motion.div\n                key={virtualItem.key}\n                data-index={virtualItem.index}\n                ref={(el) => el && rowVirtualizer.measureElement(el)}\n                initial={{ opacity: 0, y: 8 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: -8 }}\n                transition={{ duration: 0.3 }}\n                className=\"absolute inset-x-4 pb-4\"\n                style={{\n                  top: virtualItem.start,\n                }}\n              >\n                <StreamMessage \n                  message={message} \n                  streamMessages={messages}\n                  onLinkDetected={handleLinkDetected}\n                />\n              </motion.div>\n            );\n          })}\n        </AnimatePresence>\n      </div>\n\n      {/* Loading indicator under the latest message */}\n      {isLoading && (\n        <motion.div\n          initial={{ opacity: 0, y: 8 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.15 }}\n          className=\"flex items-center justify-center py-4 mb-20\"\n        >\n          <div className=\"rotating-symbol text-primary\" />\n        </motion.div>\n      )}\n\n      {/* Error indicator */}\n      {error && (\n        <motion.div\n          initial={{ opacity: 0, y: 8 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.15 }}\n          className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive mb-20 w-full max-w-6xl mx-auto\"\n        >\n          {error}\n        </motion.div>\n      )}\n    </div>\n  );\n\n  const projectPathInput = null; // Removed project path display\n\n  // If preview is maximized, render only the WebviewPreview in full screen\n  if (showPreview && isPreviewMaximized) {\n    return (\n      <AnimatePresence>\n        <motion.div \n          className=\"fixed inset-0 z-50 bg-background\"\n          initial={{ opacity: 0, y: 8 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.2 }}\n        >\n          <WebviewPreview\n            initialUrl={previewUrl}\n            onClose={handleClosePreview}\n            isMaximized={isPreviewMaximized}\n            onToggleMaximize={handleTogglePreviewMaximize}\n            onUrlChange={handlePreviewUrlChange}\n            className=\"h-full\"\n          />\n        </motion.div>\n      </AnimatePresence>\n    );\n  }\n\n  return (\n    <TooltipProvider>\n      <div className={cn(\"flex flex-col h-full bg-background\", className)}>\n        <div className=\"w-full h-full flex flex-col\">\n\n        {/* Main Content Area */}\n        <div className={cn(\n          \"flex-1 overflow-hidden transition-all duration-300\",\n          showTimeline && \"sm:mr-96\"\n        )}>\n          {showPreview ? (\n            // Split pane layout when preview is active\n            <SplitPane\n              left={\n                <div className=\"h-full flex flex-col\">\n                  {projectPathInput}\n                  {messagesList}\n                </div>\n              }\n              right={\n                <WebviewPreview\n                  initialUrl={previewUrl}\n                  onClose={handleClosePreview}\n                  isMaximized={isPreviewMaximized}\n                  onToggleMaximize={handleTogglePreviewMaximize}\n                  onUrlChange={handlePreviewUrlChange}\n                />\n              }\n              initialSplit={splitPosition}\n              onSplitChange={setSplitPosition}\n              minLeftWidth={400}\n              minRightWidth={400}\n              className=\"h-full\"\n            />\n          ) : (\n            // Original layout when no preview\n            <div className=\"h-full flex flex-col max-w-6xl mx-auto px-6\">\n              {projectPathInput}\n              {messagesList}\n              \n              {isLoading && messages.length === 0 && (\n                <div className=\"flex items-center justify-center h-full\">\n                  <div className=\"flex items-center gap-3\">\n                    <div className=\"rotating-symbol text-primary\" />\n                    <span className=\"text-sm text-muted-foreground\">\n                      {session ? \"Loading session history...\" : \"Initializing Claude Code...\"}\n                    </span>\n                  </div>\n                </div>\n              )}\n            </div>\n          )}\n        </div>\n\n        {/* Floating Prompt Input - Always visible */}\n        <ErrorBoundary>\n          {/* Queued Prompts Display */}\n          <AnimatePresence>\n            {queuedPrompts.length > 0 && (\n              <motion.div\n                initial={{ opacity: 0, y: 20 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 20 }}\n                className=\"fixed bottom-24 left-1/2 -translate-x-1/2 z-30 w-full max-w-3xl px-4\"\n              >\n                <div className=\"bg-background/95 backdrop-blur-md border rounded-lg shadow-lg p-3 space-y-2\">\n                  <div className=\"flex items-center justify-between\">\n                    <div className=\"text-xs font-medium text-muted-foreground mb-1\">\n                      Queued Prompts ({queuedPrompts.length})\n                    </div>\n                    <TooltipSimple content={queuedPromptsCollapsed ? \"Expand queue\" : \"Collapse queue\"} side=\"top\">\n                      <motion.div\n                        whileTap={{ scale: 0.97 }}\n                        transition={{ duration: 0.15 }}\n                      >\n                        <Button variant=\"ghost\" size=\"icon\" onClick={() => setQueuedPromptsCollapsed(prev => !prev)}>\n                          {queuedPromptsCollapsed ? <ChevronUp className=\"h-3 w-3\" /> : <ChevronDown className=\"h-3 w-3\" />}\n                        </Button>\n                      </motion.div>\n                    </TooltipSimple>\n                  </div>\n                  {!queuedPromptsCollapsed && queuedPrompts.map((queuedPrompt, index) => (\n                    <motion.div\n                      key={queuedPrompt.id}\n                      initial={{ opacity: 0, y: 4 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      exit={{ opacity: 0, y: -4 }}\n                      transition={{ duration: 0.15, delay: index * 0.02 }}\n                      className=\"flex items-start gap-2 bg-muted/50 rounded-md p-2\"\n                    >\n                      <div className=\"flex-1 min-w-0\">\n                        <div className=\"flex items-center gap-2 mb-1\">\n                          <span className=\"text-xs font-medium text-muted-foreground\">#{index + 1}</span>\n                          <span className=\"text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded\">\n                            {queuedPrompt.model === \"opus\" ? \"Opus\" : \"Sonnet\"}\n                          </span>\n                        </div>\n                        <p className=\"text-sm line-clamp-2 break-words\">{queuedPrompt.prompt}</p>\n                      </div>\n                      <motion.div\n                        whileTap={{ scale: 0.97 }}\n                        transition={{ duration: 0.15 }}\n                      >\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          className=\"h-6 w-6 flex-shrink-0\"\n                          onClick={() => setQueuedPrompts(prev => prev.filter(p => p.id !== queuedPrompt.id))}\n                        >\n                          <X className=\"h-3 w-3\" />\n                        </Button>\n                      </motion.div>\n                    </motion.div>\n                  ))}\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n\n          {/* Navigation Arrows - positioned above prompt bar with spacing */}\n          {displayableMessages.length > 5 && (\n            <motion.div\n              initial={{ opacity: 0, scale: 0.8 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: 0.8 }}\n              transition={{ delay: 0.5 }}\n              className=\"fixed bottom-32 right-6 z-50\"\n            >\n              <div className=\"flex items-center bg-background/95 backdrop-blur-md border rounded-full shadow-lg overflow-hidden\">\n                <TooltipSimple content=\"Scroll to top\" side=\"top\">\n                  <motion.div\n                    whileTap={{ scale: 0.97 }}\n                    transition={{ duration: 0.15 }}\n                  >\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      onClick={() => {\n                      // Use virtualizer to scroll to the first item\n                      if (displayableMessages.length > 0) {\n                        // Scroll to top of the container\n                        parentRef.current?.scrollTo({\n                          top: 0,\n                          behavior: 'smooth'\n                        });\n                        \n                        // After smooth scroll completes, trigger a small scroll to ensure rendering\n                        setTimeout(() => {\n                          if (parentRef.current) {\n                            // Scroll down 1px then back to 0 to trigger virtualizer update\n                            parentRef.current.scrollTop = 1;\n                            requestAnimationFrame(() => {\n                              if (parentRef.current) {\n                                parentRef.current.scrollTop = 0;\n                              }\n                            });\n                          }\n                        }, 500); // Wait for smooth scroll to complete\n                      }\n                    }}\n                      className=\"px-3 py-2 hover:bg-accent rounded-none\"\n                    >\n                      <ChevronUp className=\"h-4 w-4\" />\n                    </Button>\n                  </motion.div>\n                </TooltipSimple>\n                <div className=\"w-px h-4 bg-border\" />\n                <TooltipSimple content=\"Scroll to bottom\" side=\"top\">\n                  <motion.div\n                    whileTap={{ scale: 0.97 }}\n                    transition={{ duration: 0.15 }}\n                  >\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      onClick={() => {\n                        // Use the improved scrolling method for manual scroll to bottom\n                        if (displayableMessages.length > 0) {\n                          const scrollElement = parentRef.current;\n                          if (scrollElement) {\n                            // First, scroll using virtualizer to get close to the bottom\n                            rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'auto' });\n\n                            // Then use direct scroll to ensure we reach the absolute bottom\n                            requestAnimationFrame(() => {\n                              scrollElement.scrollTo({\n                                top: scrollElement.scrollHeight,\n                                behavior: 'smooth'\n                              });\n                            });\n                          }\n                        }\n                      }}\n                      className=\"px-3 py-2 hover:bg-accent rounded-none\"\n                    >\n                      <ChevronDown className=\"h-4 w-4\" />\n                    </Button>\n                  </motion.div>\n                </TooltipSimple>\n              </div>\n            </motion.div>\n          )}\n\n          <div className={cn(\n            \"fixed bottom-0 left-0 right-0 transition-all duration-300 z-50\",\n            showTimeline && \"sm:right-96\"\n          )}>\n            <FloatingPromptInput\n              ref={floatingPromptRef}\n              onSend={handleSendPrompt}\n              onCancel={handleCancelExecution}\n              isLoading={isLoading}\n              disabled={!projectPath}\n              projectPath={projectPath}\n              extraMenuItems={\n                <>\n                  {effectiveSession && (\n                    <TooltipSimple content=\"Session Timeline\" side=\"top\">\n                      <motion.div\n                        whileTap={{ scale: 0.97 }}\n                        transition={{ duration: 0.15 }}\n                      >\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          onClick={() => setShowTimeline(!showTimeline)}\n                          className=\"h-9 w-9 text-muted-foreground hover:text-foreground\"\n                        >\n                          <GitBranch className={cn(\"h-3.5 w-3.5\", showTimeline && \"text-primary\")} />\n                        </Button>\n                      </motion.div>\n                    </TooltipSimple>\n                  )}\n                  {messages.length > 0 && (\n                    <Popover\n                      trigger={\n                        <TooltipSimple content=\"Copy conversation\" side=\"top\">\n                          <motion.div\n                            whileTap={{ scale: 0.97 }}\n                            transition={{ duration: 0.15 }}\n                          >\n                            <Button\n                              variant=\"ghost\"\n                              size=\"icon\"\n                              className=\"h-9 w-9 text-muted-foreground hover:text-foreground\"\n                            >\n                              <Copy className=\"h-3.5 w-3.5\" />\n                            </Button>\n                          </motion.div>\n                        </TooltipSimple>\n                      }\n                      content={\n                        <div className=\"w-44 p-1\">\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={handleCopyAsMarkdown}\n                            className=\"w-full justify-start text-xs\"\n                          >\n                            Copy as Markdown\n                          </Button>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={handleCopyAsJsonl}\n                            className=\"w-full justify-start text-xs\"\n                          >\n                            Copy as JSONL\n                          </Button>\n                        </div>\n                      }\n                      open={copyPopoverOpen}\n                      onOpenChange={setCopyPopoverOpen}\n                      side=\"top\"\n                      align=\"end\"\n                    />\n                  )}\n                  <TooltipSimple content=\"Checkpoint Settings\" side=\"top\">\n                    <motion.div\n                      whileTap={{ scale: 0.97 }}\n                      transition={{ duration: 0.15 }}\n                    >\n                      <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        onClick={() => setShowSettings(!showSettings)}\n                        className=\"h-8 w-8 text-muted-foreground hover:text-foreground\"\n                      >\n                        <Wrench className={cn(\"h-3.5 w-3.5\", showSettings && \"text-primary\")} />\n                      </Button>\n                    </motion.div>\n                  </TooltipSimple>\n                </>\n              }\n            />\n          </div>\n\n          {/* Token Counter - positioned under the Send button */}\n          {totalTokens > 0 && (\n            <div className=\"fixed bottom-0 left-0 right-0 z-30 pointer-events-none\">\n              <div className=\"max-w-6xl mx-auto\">\n                <div className=\"flex justify-end px-4 pb-2\">\n                  <motion.div\n                    initial={{ opacity: 0, scale: 0.8 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    exit={{ opacity: 0, scale: 0.8 }}\n                    className=\"bg-background/95 backdrop-blur-md border rounded-full px-3 py-1 shadow-lg pointer-events-auto\"\n                  >\n                    <div className=\"flex items-center gap-1.5 text-xs\">\n                      <Hash className=\"h-3 w-3 text-muted-foreground\" />\n                      <span className=\"font-mono\">{totalTokens.toLocaleString()}</span>\n                      <span className=\"text-muted-foreground\">tokens</span>\n                    </div>\n                  </motion.div>\n                </div>\n              </div>\n            </div>\n          )}\n        </ErrorBoundary>\n\n        {/* Timeline */}\n        <AnimatePresence>\n          {showTimeline && effectiveSession && (\n            <motion.div\n              initial={{ x: \"100%\" }}\n              animate={{ x: 0 }}\n              exit={{ x: \"100%\" }}\n              transition={{ duration: 0.2, ease: \"easeOut\" }}\n              className=\"fixed right-0 top-0 h-full w-full sm:w-96 bg-background border-l border-border shadow-xl z-30 overflow-hidden\"\n            >\n              <div className=\"h-full flex flex-col\">\n                {/* Timeline Header */}\n                <div className=\"flex items-center justify-between p-4 border-b border-border\">\n                  <h3 className=\"text-lg font-semibold\">Session Timeline</h3>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    onClick={() => setShowTimeline(false)}\n                    className=\"h-8 w-8\"\n                  >\n                    <X className=\"h-4 w-4\" />\n                  </Button>\n                </div>\n                \n                {/* Timeline Content */}\n                <div className=\"flex-1 overflow-y-auto p-4\">\n                  <TimelineNavigator\n                    sessionId={effectiveSession.id}\n                    projectId={effectiveSession.project_id}\n                    projectPath={projectPath}\n                    currentMessageIndex={messages.length - 1}\n                    onCheckpointSelect={handleCheckpointSelect}\n                    onFork={handleFork}\n                    onCheckpointCreated={handleCheckpointCreated}\n                    refreshVersion={timelineVersion}\n                  />\n                </div>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n\n      {/* Fork Dialog */}\n      <Dialog open={showForkDialog} onOpenChange={setShowForkDialog}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Fork Session</DialogTitle>\n            <DialogDescription>\n              Create a new session branch from the selected checkpoint.\n            </DialogDescription>\n          </DialogHeader>\n          \n          <div className=\"space-y-4 py-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"fork-name\">New Session Name</Label>\n              <Input\n                id=\"fork-name\"\n                placeholder=\"e.g., Alternative approach\"\n                value={forkSessionName}\n                onChange={(e) => setForkSessionName(e.target.value)}\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\" && !isLoading) {\n                    if (e.nativeEvent.isComposing || isIMEComposingRef.current) {\n                      return;\n                    }\n                    handleConfirmFork();\n                  }\n                }}\n                onCompositionStart={handleCompositionStart}\n                onCompositionEnd={handleCompositionEnd}\n              />\n            </div>\n          </div>\n          \n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => setShowForkDialog(false)}\n              disabled={isLoading}\n            >\n              Cancel\n            </Button>\n            <Button\n              onClick={handleConfirmFork}\n              disabled={isLoading || !forkSessionName.trim()}\n            >\n              Create Fork\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Settings Dialog */}\n      {showSettings && effectiveSession && (\n        <Dialog open={showSettings} onOpenChange={setShowSettings}>\n          <DialogContent className=\"max-w-2xl\">\n            <CheckpointSettings\n              sessionId={effectiveSession.id}\n              projectId={effectiveSession.project_id}\n              projectPath={projectPath}\n              onClose={() => setShowSettings(false)}\n            />\n          </DialogContent>\n        </Dialog>\n      )}\n\n      {/* Slash Commands Settings Dialog */}\n      {showSlashCommandsSettings && (\n        <Dialog open={showSlashCommandsSettings} onOpenChange={setShowSlashCommandsSettings}>\n          <DialogContent className=\"max-w-4xl max-h-[80vh] overflow-hidden\">\n            <DialogHeader>\n              <DialogTitle>Slash Commands</DialogTitle>\n              <DialogDescription>\n                Manage project-specific slash commands for {projectPath}\n              </DialogDescription>\n            </DialogHeader>\n            <div className=\"flex-1 overflow-y-auto\">\n              <SlashCommandsManager projectPath={projectPath} />\n            </div>\n          </DialogContent>\n        </Dialog>\n      )}\n      </div>\n    </TooltipProvider>\n  );\n};\n"
  },
  {
    "path": "src/components/ClaudeFileEditor.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport MDEditor from \"@uiw/react-md-editor\";\nimport { motion } from \"framer-motion\";\nimport { ArrowLeft, Save, Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { api, type ClaudeMdFile } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ClaudeFileEditorProps {\n  /**\n   * The CLAUDE.md file to edit\n   */\n  file: ClaudeMdFile;\n  /**\n   * Callback to go back to the previous view\n   */\n  onBack: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * ClaudeFileEditor component for editing project-specific CLAUDE.md files\n * \n * @example\n * <ClaudeFileEditor \n *   file={claudeMdFile} \n *   onBack={() => setEditingFile(null)} \n * />\n */\nexport const ClaudeFileEditor: React.FC<ClaudeFileEditorProps> = ({\n  file,\n  onBack,\n  className,\n}) => {\n  const [content, setContent] = useState<string>(\"\");\n  const [originalContent, setOriginalContent] = useState<string>(\"\");\n  const [loading, setLoading] = useState(true);\n  const [saving, setSaving] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  \n  const hasChanges = content !== originalContent;\n  \n  // Load the file content on mount\n  useEffect(() => {\n    loadFileContent();\n  }, [file.absolute_path]);\n  \n  const loadFileContent = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const fileContent = await api.readClaudeMdFile(file.absolute_path);\n      setContent(fileContent);\n      setOriginalContent(fileContent);\n    } catch (err) {\n      console.error(\"Failed to load file:\", err);\n      setError(\"Failed to load CLAUDE.md file\");\n    } finally {\n      setLoading(false);\n    }\n  };\n  \n  const handleSave = async () => {\n    try {\n      setSaving(true);\n      setError(null);\n      setToast(null);\n      await api.saveClaudeMdFile(file.absolute_path, content);\n      setOriginalContent(content);\n      setToast({ message: \"File saved successfully\", type: \"success\" });\n    } catch (err) {\n      console.error(\"Failed to save file:\", err);\n      setError(\"Failed to save CLAUDE.md file\");\n      setToast({ message: \"Failed to save file\", type: \"error\" });\n    } finally {\n      setSaving(false);\n    }\n  };\n  \n  const handleBack = () => {\n    if (hasChanges) {\n      const confirmLeave = window.confirm(\n        \"You have unsaved changes. Are you sure you want to leave?\"\n      );\n      if (!confirmLeave) return;\n    }\n    onBack();\n  };\n  \n  return (\n    <div className={cn(\"flex flex-col h-full bg-background\", className)}>\n      <div className=\"w-full max-w-5xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <motion.div\n          initial={{ opacity: 0, y: -20 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.3 }}\n          className=\"flex items-center justify-between p-4 border-b border-border\"\n        >\n          <div className=\"flex items-center space-x-3\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={handleBack}\n              className=\"h-8 w-8\"\n            >\n              <ArrowLeft className=\"h-4 w-4\" />\n            </Button>\n            <div className=\"min-w-0 flex-1\">\n              <h2 className=\"text-lg font-semibold truncate\">{file.relative_path}</h2>\n              <p className=\"text-xs text-muted-foreground\">\n                Edit project-specific Claude Code system prompt\n              </p>\n            </div>\n          </div>\n          \n          <Button\n            onClick={handleSave}\n            disabled={!hasChanges || saving}\n            size=\"sm\"\n          >\n            {saving ? (\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n            ) : (\n              <Save className=\"mr-2 h-4 w-4\" />\n            )}\n            {saving ? \"Saving...\" : \"Save\"}\n          </Button>\n        </motion.div>\n        \n        {/* Error display */}\n        {error && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            className=\"mx-4 mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-xs text-destructive\"\n          >\n            {error}\n          </motion.div>\n        )}\n        \n        {/* Editor */}\n        <div className=\"flex-1 p-4 overflow-hidden\">\n          {loading ? (\n            <div className=\"flex items-center justify-center h-full\">\n              <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n            </div>\n          ) : (\n            <div className=\"h-full rounded-lg border border-border overflow-hidden shadow-sm\" data-color-mode=\"dark\">\n              <MDEditor\n                value={content}\n                onChange={(val) => setContent(val || \"\")}\n                preview=\"edit\"\n                height=\"100%\"\n                visibleDragbar={false}\n              />\n            </div>\n          )}\n        </div>\n      </div>\n      \n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/ClaudeMemoriesDropdown.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { ChevronDown, Edit2, FileText, Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport { api, type ClaudeMdFile } from \"@/lib/api\";\nimport { formatUnixTimestamp } from \"@/lib/date-utils\";\n\ninterface ClaudeMemoriesDropdownProps {\n  /**\n   * The project path to search for CLAUDE.md files\n   */\n  projectPath: string;\n  /**\n   * Callback when an edit button is clicked\n   */\n  onEditFile: (file: ClaudeMdFile) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * ClaudeMemoriesDropdown component - Shows all CLAUDE.md files in a project\n * \n * @example\n * <ClaudeMemoriesDropdown\n *   projectPath=\"/Users/example/project\"\n *   onEditFile={(file) => console.log('Edit file:', file)}\n * />\n */\nexport const ClaudeMemoriesDropdown: React.FC<ClaudeMemoriesDropdownProps> = ({\n  projectPath,\n  onEditFile,\n  className,\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [files, setFiles] = useState<ClaudeMdFile[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  \n  // Load CLAUDE.md files when dropdown opens\n  useEffect(() => {\n    if (isOpen && files.length === 0) {\n      loadClaudeMdFiles();\n    }\n  }, [isOpen]);\n  \n  const loadClaudeMdFiles = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const foundFiles = await api.findClaudeMdFiles(projectPath);\n      setFiles(foundFiles);\n    } catch (err) {\n      console.error(\"Failed to load CLAUDE.md files:\", err);\n      setError(\"Failed to load CLAUDE.md files\");\n    } finally {\n      setLoading(false);\n    }\n  };\n  \n  const formatFileSize = (bytes: number): string => {\n    if (bytes < 1024) return `${bytes} B`;\n    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n  };\n  \n  return (\n    <div className={cn(\"w-full\", className)}>\n      <Card className=\"overflow-hidden\">\n        {/* Dropdown Header */}\n        <button\n          onClick={() => setIsOpen(!isOpen)}\n          className=\"w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors\"\n        >\n          <div className=\"flex items-center space-x-2\">\n            <FileText className=\"h-4 w-4 text-muted-foreground\" />\n            <span className=\"text-sm font-medium\">CLAUDE.md Memories</span>\n            {files.length > 0 && !loading && (\n              <span className=\"text-xs text-muted-foreground\">({files.length})</span>\n            )}\n          </div>\n          <motion.div\n            animate={{ rotate: isOpen ? 180 : 0 }}\n            transition={{ duration: 0.2 }}\n          >\n            <ChevronDown className=\"h-4 w-4 text-muted-foreground\" />\n          </motion.div>\n        </button>\n        \n        {/* Dropdown Content */}\n        <AnimatePresence>\n          {isOpen && (\n            <motion.div\n              initial={{ height: 0 }}\n              animate={{ height: \"auto\" }}\n              exit={{ height: 0 }}\n              transition={{ duration: 0.2 }}\n              className=\"overflow-hidden\"\n            >\n              <div className=\"border-t border-border\">\n                {loading ? (\n                  <div className=\"p-4 flex items-center justify-center\">\n                    <Loader2 className=\"h-5 w-5 animate-spin text-muted-foreground\" />\n                  </div>\n                ) : error ? (\n                  <div className=\"p-3 text-xs text-destructive\">{error}</div>\n                ) : files.length === 0 ? (\n                  <div className=\"p-3 text-xs text-muted-foreground text-center\">\n                    No CLAUDE.md files found in this project\n                  </div>\n                ) : (\n                  <div className=\"max-h-64 overflow-y-auto\">\n                    {files.map((file, index) => (\n                      <motion.div\n                        key={file.absolute_path}\n                        initial={{ opacity: 0, x: -10 }}\n                        animate={{ opacity: 1, x: 0 }}\n                        transition={{ delay: index * 0.05 }}\n                        className=\"flex items-center justify-between p-3 hover:bg-accent/50 transition-colors border-b border-border last:border-b-0\"\n                      >\n                        <div className=\"flex-1 min-w-0 mr-2\">\n                          <p className=\"text-xs font-mono truncate\">{file.relative_path}</p>\n                          <div className=\"flex items-center space-x-3 mt-1\">\n                            <span className=\"text-xs text-muted-foreground\">\n                              {formatFileSize(file.size)}\n                            </span>\n                            <span className=\"text-xs text-muted-foreground\">\n                              Modified {formatUnixTimestamp(file.modified)}\n                            </span>\n                          </div>\n                        </div>\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          className=\"h-7 w-7 flex-shrink-0\"\n                          onClick={(e) => {\n                            e.stopPropagation();\n                            onEditFile(file);\n                          }}\n                        >\n                          <Edit2 className=\"h-3 w-3\" />\n                        </Button>\n                      </motion.div>\n                    ))}\n                  </div>\n                )}\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </Card>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/ClaudeVersionSelector.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Label } from \"@/components/ui/label\";\nimport { api, type ClaudeInstallation } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckCircle, HardDrive, Settings, Terminal, Info } from \"lucide-react\";\n\ninterface ClaudeVersionSelectorProps {\n  /**\n   * Currently selected installation path\n   */\n  selectedPath?: string | null;\n  /**\n   * Callback when an installation is selected\n   */\n  onSelect: (installation: ClaudeInstallation) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n  /**\n   * Whether to show the save button\n   */\n  showSaveButton?: boolean;\n  /**\n   * Callback when save is clicked\n   */\n  onSave?: () => void;\n  /**\n   * Whether save is in progress\n   */\n  isSaving?: boolean;\n  /**\n   * Simplified mode for cleaner UI\n   */\n  simplified?: boolean;\n}\n\n/**\n * ClaudeVersionSelector component for selecting Claude Code installations\n * Supports system installations and user preferences\n * \n * @example\n * <ClaudeVersionSelector\n *   selectedPath={currentPath}\n *   onSelect={(installation) => setSelectedInstallation(installation)}\n * />\n */\nexport const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({\n  selectedPath,\n  onSelect,\n  className,\n  showSaveButton = false,\n  onSave,\n  isSaving = false,\n  simplified = false,\n}) => {\n  const [installations, setInstallations] = useState<ClaudeInstallation[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [selectedInstallation, setSelectedInstallation] = useState<ClaudeInstallation | null>(null);\n\n  useEffect(() => {\n    loadInstallations();\n  }, []);\n\n  useEffect(() => {\n    // Update selected installation when selectedPath changes\n    if (selectedPath && installations.length > 0) {\n      const found = installations.find(i => i.path === selectedPath);\n      if (found) {\n        setSelectedInstallation(found);\n      }\n    }\n  }, [selectedPath, installations]);\n\n  const loadInstallations = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const foundInstallations = await api.listClaudeInstallations();\n      setInstallations(foundInstallations);\n      \n      // If we have a selected path, find and select it\n      if (selectedPath) {\n        const found = foundInstallations.find(i => i.path === selectedPath);\n        if (found) {\n          setSelectedInstallation(found);\n        }\n      } else if (foundInstallations.length > 0) {\n        // Auto-select the first (best) installation\n        setSelectedInstallation(foundInstallations[0]);\n        onSelect(foundInstallations[0]);\n      }\n    } catch (err) {\n      console.error(\"Failed to load Claude installations:\", err);\n      setError(err instanceof Error ? err.message : \"Failed to load Claude installations\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleInstallationChange = (installationPath: string) => {\n    const installation = installations.find(i => i.path === installationPath);\n    if (installation) {\n      setSelectedInstallation(installation);\n      onSelect(installation);\n    }\n  };\n\n  const getInstallationIcon = (installation: ClaudeInstallation) => {\n    switch (installation.installation_type) {\n      case \"System\":\n        return <HardDrive className=\"h-4 w-4\" />;\n      case \"Custom\":\n        return <Settings className=\"h-4 w-4\" />;\n      default:\n        return <HardDrive className=\"h-4 w-4\" />;\n    }\n  };\n\n  const getInstallationTypeColor = (installation: ClaudeInstallation) => {\n    switch (installation.installation_type) {\n      case \"System\":\n        return \"default\";\n      case \"Custom\":\n        return \"secondary\";\n      default:\n        return \"outline\";\n    }\n  };\n\n  if (loading) {\n    if (simplified) {\n      return (\n        <div className=\"space-y-2\">\n          <Label className=\"text-sm font-medium\">Claude Installation</Label>\n          <div className=\"flex items-center justify-center py-3 border rounded-lg\">\n            <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-primary\"></div>\n          </div>\n        </div>\n      );\n    }\n    return (\n      <Card className={className}>\n        <CardHeader>\n          <CardTitle>Claude Code Installation</CardTitle>\n          <CardDescription>Loading available installations...</CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"flex items-center justify-center py-4\">\n            <div className=\"animate-spin rounded-full h-6 w-6 border-b-2 border-primary\"></div>\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  if (error) {\n    if (simplified) {\n      return (\n        <div className=\"space-y-2\">\n          <Label className=\"text-sm font-medium\">Claude Installation</Label>\n          <div className=\"p-3 border border-destructive/50 rounded-lg bg-destructive/10\">\n            <p className=\"text-sm text-destructive mb-2\">{error}</p>\n            <Button onClick={loadInstallations} variant=\"outline\" size=\"sm\">\n              Retry\n            </Button>\n          </div>\n        </div>\n      );\n    }\n    return (\n      <Card className={className}>\n        <CardHeader>\n          <CardTitle>Claude Code Installation</CardTitle>\n          <CardDescription>Error loading installations</CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"text-sm text-destructive mb-4\">{error}</div>\n          <Button onClick={loadInstallations} variant=\"outline\" size=\"sm\">\n            Retry\n          </Button>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  const systemInstallations = installations.filter(i => i.installation_type === \"System\");\n  const customInstallations = installations.filter(i => i.installation_type === \"Custom\");\n\n  // Simplified mode - more streamlined UI\n  if (simplified) {\n    return (\n      <div className={cn(\"space-y-3\", className)}>\n        <div className=\"flex items-center justify-between\">\n          <div className=\"space-y-0.5\">\n            <Label htmlFor=\"claude-installation\" className=\"text-sm font-medium\">Claude Installation</Label>\n            <p className=\"text-xs text-muted-foreground\">\n              Select which version of Claude to use\n            </p>\n          </div>\n          {selectedInstallation && (\n            <Badge variant={getInstallationTypeColor(selectedInstallation)} className=\"text-xs\">\n              {selectedInstallation.installation_type}\n            </Badge>\n          )}\n        </div>\n        \n        <Select value={selectedInstallation?.path || \"\"} onValueChange={handleInstallationChange}>\n          <SelectTrigger id=\"claude-installation\" className=\"w-full\">\n            <SelectValue placeholder=\"Choose Claude installation\">\n              {selectedInstallation && (\n                <div className=\"flex items-center gap-2\">\n                  <Terminal className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                  <span className=\"font-mono text-sm\">{selectedInstallation.path.split('/').pop() || selectedInstallation.path}</span>\n                  {selectedInstallation.version && (\n                    <span className=\"text-xs text-muted-foreground\">({selectedInstallation.version})</span>\n                  )}\n                </div>\n              )}\n            </SelectValue>\n          </SelectTrigger>\n          <SelectContent side=\"bottom\" align=\"start\" sideOffset={5}>\n            {installations.length === 0 ? (\n              <div className=\"p-4 text-center text-sm text-muted-foreground\">\n                No Claude installations found\n              </div>\n            ) : (\n              <>\n                {installations.map((installation) => (\n                  <SelectItem key={installation.path} value={installation.path} className=\"cursor-pointer hover:bg-accent focus:bg-accent\">\n                    <div className=\"flex items-center gap-2 py-1\">\n                      <Terminal className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                      <div className=\"flex-1\">\n                        <div className=\"font-mono text-sm\">{installation.path}</div>\n                        <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n                          <span>{installation.version || \"Unknown version\"}</span>\n                          <span>•</span>\n                          <span>{installation.source}</span>\n                          <Badge variant={getInstallationTypeColor(installation)} className=\"text-xs ml-2\">\n                            {installation.installation_type}\n                          </Badge>\n                        </div>\n                      </div>\n                    </div>\n                  </SelectItem>\n                ))}\n              </>\n            )}\n          </SelectContent>\n        </Select>\n        \n        {selectedInstallation && (\n          <div className=\"flex items-start gap-2 p-2 bg-muted/50 rounded-md\">\n            <Info className=\"h-3.5 w-3.5 text-muted-foreground mt-0.5\" />\n            <div className=\"text-xs text-muted-foreground\">\n              <span className=\"font-medium\">Path:</span> <code className=\"font-mono\">{selectedInstallation.path}</code>\n            </div>\n          </div>\n        )}\n      </div>\n    );\n  }\n\n  // Original card-based UI\n  return (\n    <Card className={className}>\n      <CardHeader>\n        <CardTitle className=\"flex items-center gap-2\">\n          <CheckCircle className=\"h-5 w-5\" />\n          Claude Code Installation\n        </CardTitle>\n        <CardDescription>\n          Choose your preferred Claude Code installation.\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-6\">\n        {/* Available Installations */}\n        <div className=\"space-y-3\">\n          <Label className=\"text-sm font-medium\">Available Installations</Label>\n          <Select value={selectedInstallation?.path || \"\"} onValueChange={handleInstallationChange}>\n            <SelectTrigger>\n              <SelectValue placeholder=\"Select Claude installation\">\n                {selectedInstallation && (\n                  <div className=\"flex items-center gap-2\">\n                    {getInstallationIcon(selectedInstallation)}\n                    <span className=\"truncate\">{selectedInstallation.path}</span>\n                    <Badge variant={getInstallationTypeColor(selectedInstallation)} className=\"text-xs\">\n                      {selectedInstallation.installation_type}\n                    </Badge>\n                  </div>\n                )}\n              </SelectValue>\n            </SelectTrigger>\n            <SelectContent side=\"bottom\" align=\"start\" sideOffset={5}>\n              {systemInstallations.length > 0 && (\n                <>\n                  <div className=\"px-2 py-1.5 text-xs font-semibold text-muted-foreground\">System Installations</div>\n                  {systemInstallations.map((installation) => (\n                    <SelectItem key={installation.path} value={installation.path} className=\"cursor-pointer hover:bg-accent focus:bg-accent\">\n                      <div className=\"flex items-center gap-2 w-full\">\n                        {getInstallationIcon(installation)}\n                        <div className=\"flex-1 min-w-0\">\n                          <div className=\"font-medium truncate\">{installation.path}</div>\n                          <div className=\"text-xs text-muted-foreground\">\n                            {installation.version || \"Version unknown\"} • {installation.source}\n                          </div>\n                        </div>\n                        <Badge variant=\"outline\" className=\"text-xs\">\n                          System\n                        </Badge>\n                      </div>\n                    </SelectItem>\n                  ))}\n                </>\n              )}\n\n              {customInstallations.length > 0 && (\n                <>\n                  <div className=\"px-2 py-1.5 text-xs font-semibold text-muted-foreground\">Custom Installations</div>\n                  {customInstallations.map((installation) => (\n                    <SelectItem key={installation.path} value={installation.path} className=\"cursor-pointer hover:bg-accent focus:bg-accent\">\n                      <div className=\"flex items-center gap-2 w-full\">\n                        {getInstallationIcon(installation)}\n                        <div className=\"flex-1 min-w-0\">\n                          <div className=\"font-medium truncate\">{installation.path}</div>\n                          <div className=\"text-xs text-muted-foreground\">\n                            {installation.version || \"Version unknown\"} • {installation.source}\n                          </div>\n                        </div>\n                        <Badge variant=\"outline\" className=\"text-xs\">\n                          Custom\n                        </Badge>\n                      </div>\n                    </SelectItem>\n                  ))}\n                </>\n              )}\n            </SelectContent>\n          </Select>\n        </div>\n\n        {/* Installation Details */}\n        {selectedInstallation && (\n          <div className=\"p-3 bg-muted rounded-lg space-y-2\">\n            <div className=\"flex items-center justify-between\">\n              <span className=\"text-sm font-medium\">Selected Installation</span>\n              <Badge variant={getInstallationTypeColor(selectedInstallation)} className=\"text-xs\">\n                {selectedInstallation.installation_type}\n              </Badge>\n            </div>\n            <div className=\"text-sm text-muted-foreground\">\n              <div><strong>Path:</strong> {selectedInstallation.path}</div>\n              <div><strong>Source:</strong> {selectedInstallation.source}</div>\n              {selectedInstallation.version && (\n                <div><strong>Version:</strong> {selectedInstallation.version}</div>\n              )}\n            </div>\n          </div>\n        )}\n\n        {/* Save Button */}\n        {showSaveButton && (\n          <Button \n            onClick={onSave} \n            disabled={isSaving || !selectedInstallation}\n            className=\"w-full\"\n          >\n            {isSaving ? \"Saving...\" : \"Save Selection\"}\n          </Button>\n        )}\n      </CardContent>\n    </Card>\n  );\n}; \n"
  },
  {
    "path": "src/components/CreateAgent.tsx",
    "content": "import React, { useState } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { ArrowLeft, Save, Loader2, ChevronDown, Zap, AlertCircle } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card } from \"@/components/ui/card\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { api, type Agent } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport MDEditor from \"@uiw/react-md-editor\";\nimport { type AgentIconName } from \"./CCAgents\";\nimport { IconPicker, ICON_MAP } from \"./IconPicker\";\n\n\ninterface CreateAgentProps {\n  /**\n   * Optional agent to edit (if provided, component is in edit mode)\n   */\n  agent?: Agent;\n  /**\n   * Callback to go back to the agents list\n   */\n  onBack: () => void;\n  /**\n   * Callback when agent is created/updated\n   */\n  onAgentCreated: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * CreateAgent component for creating or editing a CC agent\n * \n * @example\n * <CreateAgent onBack={() => setView('list')} onAgentCreated={handleCreated} />\n */\nexport const CreateAgent: React.FC<CreateAgentProps> = ({\n  agent,\n  onBack,\n  onAgentCreated,\n  className,\n}) => {\n  const [name, setName] = useState(agent?.name || \"\");\n  const [selectedIcon, setSelectedIcon] = useState<AgentIconName>((agent?.icon as AgentIconName) || \"bot\");\n  const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || \"\");\n  const [defaultTask, setDefaultTask] = useState(agent?.default_task || \"\");\n  const [model, setModel] = useState(agent?.model || \"sonnet\");\n  const [saving, setSaving] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  const [showIconPicker, setShowIconPicker] = useState(false);\n\n  const isEditMode = !!agent;\n\n  const handleSave = async () => {\n    if (!name.trim()) {\n      setError(\"Agent name is required\");\n      return;\n    }\n\n    if (!systemPrompt.trim()) {\n      setError(\"System prompt is required\");\n      return;\n    }\n\n    try {\n      setSaving(true);\n      setError(null);\n      \n      if (isEditMode && agent.id) {\n        await api.updateAgent(\n          agent.id, \n          name, \n          selectedIcon, \n          systemPrompt, \n          defaultTask || undefined, \n          model\n        );\n      } else {\n        await api.createAgent(\n          name, \n          selectedIcon, \n          systemPrompt, \n          defaultTask || undefined, \n          model\n        );\n      }\n      \n      onAgentCreated();\n    } catch (err) {\n      console.error(\"Failed to save agent:\", err);\n      setError(isEditMode ? \"Failed to update agent\" : \"Failed to create agent\");\n      setToast({ \n        message: isEditMode ? \"Failed to update agent\" : \"Failed to create agent\", \n        type: \"error\" \n      });\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  const handleBack = () => {\n    if ((name !== (agent?.name || \"\") || \n         selectedIcon !== (agent?.icon || \"bot\") || \n         systemPrompt !== (agent?.system_prompt || \"\") ||\n         defaultTask !== (agent?.default_task || \"\") ||\n         model !== (agent?.model || \"sonnet\")) && \n        !confirm(\"You have unsaved changes. Are you sure you want to leave?\")) {\n      return;\n    }\n    onBack();\n  };\n\n  return (\n    <motion.div \n      initial={{ opacity: 0, y: 8 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.15 }}\n      className={cn(\"h-full overflow-y-auto bg-background\", className)}\n    >\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6 border-b border-border\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-3\">\n              <motion.div\n                whileTap={{ scale: 0.97 }}\n                transition={{ duration: 0.15 }}\n              >\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  onClick={handleBack}\n                  className=\"h-9 w-9 -ml-2\"\n                  title=\"Back to Agents\"\n                >\n                  <ArrowLeft className=\"h-4 w-4\" />\n                </Button>\n              </motion.div>\n              <div>\n                <h1 className=\"text-heading-1\">\n                  {isEditMode ? \"Edit Agent\" : \"Create New Agent\"}\n                </h1>\n                <p className=\"mt-1 text-body-small text-muted-foreground\">\n                  {isEditMode ? \"Update your Claude Code agent configuration\" : \"Configure a new Claude Code agent\"}\n                </p>\n              </div>\n            </div>\n            \n            <motion.div\n              whileTap={{ scale: 0.97 }}\n              transition={{ duration: 0.15 }}\n            >\n              <Button\n                onClick={handleSave}\n                disabled={saving || !name.trim() || !systemPrompt.trim()}\n                size=\"default\"\n              >\n                {saving ? (\n                  <>\n                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                    Saving...\n                  </>\n                ) : (\n                  <>\n                    <Save className=\"mr-2 h-4 w-4\" />\n                    Save Agent\n                  </>\n                )}\n              </Button>\n            </motion.div>\n          </div>\n        </div>\n        \n        {/* Error display */}\n        {error && (\n          <motion.div\n            initial={{ opacity: 0, y: 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -4 }}\n            transition={{ duration: 0.15 }}\n            className=\"mx-6 mt-4 p-3 rounded-md bg-destructive/10 border border-destructive/50 flex items-center gap-2\"\n          >\n            <AlertCircle className=\"h-3.5 w-3.5 text-destructive flex-shrink-0\" />\n            <span className=\"text-caption text-destructive\">{error}</span>\n          </motion.div>\n        )}\n        \n        {/* Content */}\n        <div className=\"flex-1 overflow-y-auto p-6\">\n          <div className=\"space-y-4\">\n            {/* Basic Information */}\n            <Card className=\"p-5\">\n              <div className=\"flex items-center gap-2 mb-4\">\n                <h3 className=\"text-heading-4\">Basic Information</h3>\n              </div>\n              <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n                <div className=\"space-y-2\">\n                  <Label htmlFor=\"name\" className=\"text-caption text-muted-foreground\">Agent Name</Label>\n                  <Input\n                    id=\"name\"\n                    value={name}\n                    onChange={(e) => setName(e.target.value)}\n                    placeholder=\"e.g., Code Assistant\"\n                    required\n                    className=\"h-9\"\n                  />\n                </div>\n                \n                <div className=\"space-y-2\">\n                  <Label className=\"text-caption text-muted-foreground\">Agent Icon</Label>\n                  <motion.div\n                    whileTap={{ scale: 0.97 }}\n                    transition={{ duration: 0.15 }}\n                    onClick={() => setShowIconPicker(true)}\n                    className=\"h-9 px-3 py-2 bg-background border border-input rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors flex items-center justify-between\"\n                  >\n                    <div className=\"flex items-center gap-2\">\n                      {(() => {\n                        const Icon = ICON_MAP[selectedIcon] || ICON_MAP.bot;\n                        return (\n                          <>\n                            <Icon className=\"h-4 w-4\" />\n                            <span className=\"text-sm\">{selectedIcon}</span>\n                          </>\n                        );\n                      })()}\n                    </div>\n                    <ChevronDown className=\"h-4 w-4 text-muted-foreground\" />\n                  </motion.div>\n                </div>\n              </div>\n\n              {/* Model Selection */}\n              <div className=\"space-y-2 mt-4\">\n                <Label className=\"text-caption text-muted-foreground\">Model</Label>\n                <div className=\"flex flex-col sm:flex-row gap-2\">\n                  <motion.button\n                    type=\"button\"\n                    onClick={() => setModel(\"sonnet\")}\n                    whileTap={{ scale: 0.97 }}\n                    transition={{ duration: 0.15 }}\n                    className={cn(\n                      \"flex-1 px-4 py-3 rounded-md border transition-all\",\n                      model === \"sonnet\" \n                        ? \"border-primary bg-primary/10 text-primary\" \n                        : \"border-border hover:border-primary/50 hover:bg-accent\"\n                    )}\n                  >\n                    <div className=\"flex items-center gap-3\">\n                      <Zap className={cn(\n                        \"h-4 w-4\",\n                        model === \"sonnet\" ? \"text-primary\" : \"text-muted-foreground\"\n                      )} />\n                      <div className=\"text-left\">\n                        <div className=\"text-body-small font-medium\">Claude 4 Sonnet</div>\n                        <div className=\"text-caption text-muted-foreground\">Faster, efficient for most tasks</div>\n                      </div>\n                    </div>\n                  </motion.button>\n                  \n                  <motion.button\n                    type=\"button\"\n                    onClick={() => setModel(\"opus\")}\n                    whileTap={{ scale: 0.97 }}\n                    transition={{ duration: 0.15 }}\n                    className={cn(\n                      \"flex-1 px-4 py-3 rounded-md border transition-all\",\n                      model === \"opus\" \n                        ? \"border-primary bg-primary/10 text-primary\" \n                        : \"border-border hover:border-primary/50 hover:bg-accent\"\n                    )}\n                  >\n                    <div className=\"flex items-center gap-3\">\n                      <Zap className={cn(\n                        \"h-4 w-4\",\n                        model === \"opus\" ? \"text-primary\" : \"text-muted-foreground\"\n                      )} />\n                      <div className=\"text-left\">\n                        <div className=\"text-body-small font-medium\">Claude 4 Opus</div>\n                        <div className=\"text-caption text-muted-foreground\">More capable, better for complex tasks</div>\n                      </div>\n                    </div>\n                  </motion.button>\n                </div>\n              </div>\n            </Card>\n\n            {/* Configuration */}\n            <Card className=\"p-5\">\n              <h3 className=\"text-heading-4 mb-4\">Configuration</h3>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"default-task\" className=\"text-caption text-muted-foreground\">Default Task (Optional)</Label>\n                <Input\n                  id=\"default-task\"\n                  type=\"text\"\n                  placeholder=\"e.g., Review this code for security issues\"\n                  value={defaultTask}\n                  onChange={(e) => setDefaultTask(e.target.value)}\n                  className=\"h-9\"\n                />\n                <p className=\"text-caption text-muted-foreground\">\n                  This will be used as the default task placeholder when executing the agent\n                </p>\n              </div>\n            </Card>\n\n            {/* System Prompt */}\n            <Card className=\"p-5\">\n              <div className=\"mb-4\">\n                <h3 className=\"text-heading-4 mb-1\">System Prompt</h3>\n                <p className=\"text-caption text-muted-foreground\">\n                  Define the behavior and capabilities of your Claude Code agent\n                </p>\n              </div>\n              <div className=\"rounded-md border border-border overflow-hidden\" data-color-mode=\"dark\">\n                <MDEditor\n                  value={systemPrompt}\n                  onChange={(val) => setSystemPrompt(val || \"\")}\n                  preview=\"edit\"\n                  height={350}\n                  visibleDragbar={false}\n                />\n              </div>\n            </Card>\n          </div>\n        </div>\n      </div>\n  \n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n\n      {/* Icon Picker Dialog */}\n      <IconPicker\n        value={selectedIcon}\n        onSelect={(iconName) => {\n          setSelectedIcon(iconName as AgentIconName);\n          setShowIconPicker(false);\n        }}\n        isOpen={showIconPicker}\n        onClose={() => setShowIconPicker(false)}\n      />\n    </motion.div>\n  );\n}; \n"
  },
  {
    "path": "src/components/CustomTitlebar.tsx",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { motion } from 'framer-motion';\nimport { Settings, Minus, Square, X, Bot, BarChart3, FileText, Network, Info, MoreVertical } from 'lucide-react';\nimport { getCurrentWindow } from '@tauri-apps/api/window';\nimport { TooltipProvider, TooltipSimple } from '@/components/ui/tooltip-modern';\n\ninterface CustomTitlebarProps {\n  onSettingsClick?: () => void;\n  onAgentsClick?: () => void;\n  onUsageClick?: () => void;\n  onClaudeClick?: () => void;\n  onMCPClick?: () => void;\n  onInfoClick?: () => void;\n}\n\nexport const CustomTitlebar: React.FC<CustomTitlebarProps> = ({\n  onSettingsClick,\n  onAgentsClick,\n  onUsageClick,\n  onClaudeClick,\n  onMCPClick,\n  onInfoClick\n}) => {\n  const [isHovered, setIsHovered] = useState(false);\n  const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n        setIsDropdownOpen(false);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, []);\n\n  const handleMinimize = async () => {\n    try {\n      const window = getCurrentWindow();\n      await window.minimize();\n      console.log('Window minimized successfully');\n    } catch (error) {\n      console.error('Failed to minimize window:', error);\n    }\n  };\n\n  const handleMaximize = async () => {\n    try {\n      const window = getCurrentWindow();\n      const isMaximized = await window.isMaximized();\n      if (isMaximized) {\n        await window.unmaximize();\n        console.log('Window unmaximized successfully');\n      } else {\n        await window.maximize();\n        console.log('Window maximized successfully');\n      }\n    } catch (error) {\n      console.error('Failed to maximize/unmaximize window:', error);\n    }\n  };\n\n  const handleClose = async () => {\n    try {\n      const window = getCurrentWindow();\n      await window.close();\n      console.log('Window closed successfully');\n    } catch (error) {\n      console.error('Failed to close window:', error);\n    }\n  };\n\n  return (\n    <TooltipProvider>\n    <div \n      className=\"relative z-[200] h-11 bg-background/95 backdrop-blur-sm flex items-center justify-between select-none border-b border-border/50 tauri-drag\"\n      data-tauri-drag-region\n      onMouseEnter={() => setIsHovered(true)}\n      onMouseLeave={() => setIsHovered(false)}\n    >\n      {/* Left side - macOS Traffic Light buttons */}\n      <div className=\"flex items-center space-x-2 pl-5\">\n        <div className=\"flex items-center space-x-2\">\n          {/* Close button */}\n          <button\n            onClick={(e) => {\n              e.stopPropagation();\n              handleClose();\n            }}\n            className=\"group relative w-3 h-3 rounded-full bg-red-500 hover:bg-red-600 transition-all duration-200 flex items-center justify-center tauri-no-drag\"\n            title=\"Close\"\n          >\n            {isHovered && (\n              <X size={8} className=\"text-red-900 opacity-60 group-hover:opacity-100\" />\n            )}\n          </button>\n\n          {/* Minimize button */}\n          <button\n            onClick={(e) => {\n              e.stopPropagation();\n              handleMinimize();\n            }}\n            className=\"group relative w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600 transition-all duration-200 flex items-center justify-center tauri-no-drag\"\n            title=\"Minimize\"\n          >\n            {isHovered && (\n              <Minus size={8} className=\"text-yellow-900 opacity-60 group-hover:opacity-100\" />\n            )}\n          </button>\n\n          {/* Maximize button */}\n          <button\n            onClick={(e) => {\n              e.stopPropagation();\n              handleMaximize();\n            }}\n            className=\"group relative w-3 h-3 rounded-full bg-green-500 hover:bg-green-600 transition-all duration-200 flex items-center justify-center tauri-no-drag\"\n            title=\"Maximize\"\n          >\n            {isHovered && (\n              <Square size={6} className=\"text-green-900 opacity-60 group-hover:opacity-100\" />\n            )}\n          </button>\n        </div>\n      </div>\n\n      {/* Center - Title (hidden) */}\n      {/* <div \n        className=\"absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none\"\n        data-tauri-drag-region\n      >\n        <span className=\"text-sm font-medium text-foreground/80\">{title}</span>\n      </div> */}\n\n      {/* Right side - Navigation icons with improved spacing */}\n      <div className=\"flex items-center pr-5 gap-3 tauri-no-drag\">\n        {/* Primary actions group */}\n        <div className=\"flex items-center gap-1\">\n          {onAgentsClick && (\n            <TooltipSimple content=\"Agents\" side=\"bottom\">\n              <motion.button\n                onClick={onAgentsClick}\n                whileTap={{ scale: 0.97 }}\n                transition={{ duration: 0.15 }}\n                className=\"p-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors tauri-no-drag\"\n              >\n                <Bot size={16} />\n              </motion.button>\n            </TooltipSimple>\n          )}\n          \n          {onUsageClick && (\n            <TooltipSimple content=\"Usage Dashboard\" side=\"bottom\">\n              <motion.button\n                onClick={onUsageClick}\n                whileTap={{ scale: 0.97 }}\n                transition={{ duration: 0.15 }}\n                className=\"p-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors tauri-no-drag\"\n              >\n                <BarChart3 size={16} />\n              </motion.button>\n            </TooltipSimple>\n          )}\n        </div>\n\n        {/* Visual separator */}\n        <div className=\"w-px h-5 bg-border/50\" />\n\n        {/* Secondary actions group */}\n        <div className=\"flex items-center gap-1\">\n          {onSettingsClick && (\n            <TooltipSimple content=\"Settings\" side=\"bottom\">\n              <motion.button\n                onClick={onSettingsClick}\n                whileTap={{ scale: 0.97 }}\n                transition={{ duration: 0.15 }}\n                className=\"p-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors tauri-no-drag\"\n              >\n                <Settings size={16} />\n              </motion.button>\n            </TooltipSimple>\n          )}\n\n          {/* Dropdown menu for additional options */}\n          <div className=\"relative\" ref={dropdownRef}>\n            <TooltipSimple content=\"More options\" side=\"bottom\">\n              <motion.button\n                onClick={() => setIsDropdownOpen(!isDropdownOpen)}\n                whileTap={{ scale: 0.97 }}\n                transition={{ duration: 0.15 }}\n                className=\"p-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-1\"\n              >\n                <MoreVertical size={16} />\n              </motion.button>\n            </TooltipSimple>\n\n            {isDropdownOpen && (\n              <div className=\"absolute right-0 mt-2 w-48 bg-popover border border-border rounded-lg shadow-lg z-[250]\">\n                <div className=\"py-1\">\n                  {onClaudeClick && (\n                    <button\n                      onClick={() => {\n                        onClaudeClick();\n                        setIsDropdownOpen(false);\n                      }}\n                      className=\"w-full px-4 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-3\"\n                    >\n                      <FileText size={14} />\n                      <span>CLAUDE.md</span>\n                    </button>\n                  )}\n                  \n                  {onMCPClick && (\n                    <button\n                      onClick={() => {\n                        onMCPClick();\n                        setIsDropdownOpen(false);\n                      }}\n                      className=\"w-full px-4 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-3\"\n                    >\n                      <Network size={14} />\n                      <span>MCP Servers</span>\n                    </button>\n                  )}\n                  \n                  {onInfoClick && (\n                    <button\n                      onClick={() => {\n                        onInfoClick();\n                        setIsDropdownOpen(false);\n                      }}\n                      className=\"w-full px-4 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-3\"\n                    >\n                      <Info size={14} />\n                      <span>About</span>\n                    </button>\n                  )}\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n    </TooltipProvider>\n  );\n};\n"
  },
  {
    "path": "src/components/ErrorBoundary.tsx",
    "content": "import React, { Component, ReactNode } from \"react\";\nimport { AlertCircle } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\n\ninterface ErrorBoundaryProps {\n  children: ReactNode;\n  fallback?: (error: Error, reset: () => void) => ReactNode;\n}\n\ninterface ErrorBoundaryState {\n  hasError: boolean;\n  error: Error | null;\n}\n\n/**\n * Error Boundary component to catch and display React rendering errors\n */\nexport class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {\n  constructor(props: ErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false, error: null };\n  }\n\n  static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n    // Update state so the next render will show the fallback UI\n    return { hasError: true, error };\n  }\n\n  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n    // Log the error to console\n    console.error(\"Error caught by boundary:\", error, errorInfo);\n  }\n\n  reset = () => {\n    this.setState({ hasError: false, error: null });\n  };\n\n  render() {\n    if (this.state.hasError && this.state.error) {\n      // Use custom fallback if provided\n      if (this.props.fallback) {\n        return this.props.fallback(this.state.error, this.reset);\n      }\n\n      // Default error UI\n      return (\n        <div className=\"flex items-center justify-center min-h-[200px] p-4\">\n          <Card className=\"max-w-md w-full\">\n            <CardContent className=\"p-6\">\n              <div className=\"flex items-start gap-4\">\n                <AlertCircle className=\"h-8 w-8 text-destructive flex-shrink-0 mt-0.5\" />\n                <div className=\"flex-1 space-y-2\">\n                  <h3 className=\"text-lg font-semibold\">Something went wrong</h3>\n                  <p className=\"text-sm text-muted-foreground\">\n                    An error occurred while rendering this component.\n                  </p>\n                  {this.state.error.message && (\n                    <details className=\"mt-2\">\n                      <summary className=\"text-sm cursor-pointer text-muted-foreground hover:text-foreground\">\n                        Error details\n                      </summary>\n                      <pre className=\"mt-2 text-xs bg-muted p-2 rounded overflow-auto\">\n                        {this.state.error.message}\n                      </pre>\n                    </details>\n                  )}\n                  <Button\n                    onClick={this.reset}\n                    size=\"sm\"\n                    className=\"mt-4\"\n                  >\n                    Try again\n                  </Button>\n                </div>\n              </div>\n            </CardContent>\n          </Card>\n        </div>\n      );\n    }\n\n    return this.props.children;\n  }\n} "
  },
  {
    "path": "src/components/ExecutionControlBar.tsx",
    "content": "import React from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { StopCircle, Clock, Hash } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ExecutionControlBarProps {\n  isExecuting: boolean;\n  onStop: () => void;\n  totalTokens?: number;\n  elapsedTime?: number; // in seconds\n  className?: string;\n}\n\n/**\n * Floating control bar shown during agent execution\n * Provides stop functionality and real-time statistics\n */\nexport const ExecutionControlBar: React.FC<ExecutionControlBarProps> = ({ \n  isExecuting, \n  onStop, \n  totalTokens = 0,\n  elapsedTime = 0,\n  className \n}) => {\n  // Format elapsed time\n  const formatTime = (seconds: number) => {\n    const mins = Math.floor(seconds / 60);\n    const secs = seconds % 60;\n    if (mins > 0) {\n      return `${mins}m ${secs.toFixed(0)}s`;\n    }\n    return `${secs.toFixed(1)}s`;\n  };\n\n  // Format token count\n  const formatTokens = (tokens: number) => {\n    if (tokens >= 1000) {\n      return `${(tokens / 1000).toFixed(1)}k`;\n    }\n    return tokens.toString();\n  };\n\n  return (\n    <AnimatePresence>\n      {isExecuting && (\n        <motion.div\n          initial={{ y: 100, opacity: 0 }}\n          animate={{ y: 0, opacity: 1 }}\n          exit={{ y: 100, opacity: 0 }}\n          transition={{ type: \"spring\", stiffness: 300, damping: 30 }}\n          className={cn(\n            \"fixed bottom-6 left-1/2 -translate-x-1/2 z-50\",\n            \"bg-background/95 backdrop-blur-md border rounded-full shadow-lg\",\n            \"px-6 py-3 flex items-center gap-4\",\n            className\n          )}\n        >\n          {/* Rotating symbol indicator */}\n          <div className=\"relative flex items-center justify-center\">\n            <div className=\"rotating-symbol text-primary\"></div>\n          </div>\n\n          {/* Status text */}\n          <span className=\"text-sm font-medium\">Executing...</span>\n\n          {/* Divider */}\n          <div className=\"h-4 w-px bg-border\" />\n\n          {/* Stats */}\n          <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n            {/* Time */}\n            <div className=\"flex items-center gap-1.5\">\n              <Clock className=\"h-3.5 w-3.5\" />\n              <span>{formatTime(elapsedTime)}</span>\n            </div>\n\n            {/* Tokens */}\n            <div className=\"flex items-center gap-1.5\">\n              <Hash className=\"h-3.5 w-3.5\" />\n              <span>{formatTokens(totalTokens)} tokens</span>\n            </div>\n          </div>\n\n          {/* Divider */}\n          <div className=\"h-4 w-px bg-border\" />\n\n          {/* Stop button */}\n          <Button\n            size=\"sm\"\n            variant=\"destructive\"\n            onClick={onStop}\n            className=\"gap-2\"\n          >\n            <StopCircle className=\"h-3.5 w-3.5\" />\n            Stop\n          </Button>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}; "
  },
  {
    "path": "src/components/FilePicker.optimized.tsx",
    "content": "import React, { useState, useEffect, useRef, useCallback, useMemo } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { Button } from \"@/components/ui/button\";\nimport { api } from \"@/lib/api\";\nimport { \n  X, \n  Folder, \n  File, \n  ArrowLeft,\n  FileCode,\n  FileText,\n  FileImage,\n  Search,\n  ChevronRight\n} from \"lucide-react\";\nimport type { FileEntry } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\n// Global caches that persist across component instances\nconst globalDirectoryCache = new Map<string, FileEntry[]>();\nconst globalSearchCache = new Map<string, FileEntry[]>();\n\ninterface FilePickerProps {\n  basePath: string;\n  onSelect: (entry: FileEntry) => void;\n  onClose: () => void;\n  initialQuery?: string;\n  className?: string;\n  allowDirectorySelection?: boolean;\n}\n\n// Memoized file icon selector\nconst getFileIcon = (entry: FileEntry) => {\n  if (entry.is_directory) return Folder;\n  \n  const ext = entry.name.split('.').pop()?.toLowerCase();\n  switch (ext) {\n    case 'js':\n    case 'jsx':\n    case 'ts':\n    case 'tsx':\n    case 'py':\n    case 'java':\n    case 'cpp':\n    case 'c':\n    case 'go':\n    case 'rs':\n      return FileCode;\n    case 'md':\n    case 'txt':\n    case 'json':\n    case 'xml':\n    case 'yaml':\n    case 'yml':\n      return FileText;\n    case 'png':\n    case 'jpg':\n    case 'jpeg':\n    case 'gif':\n    case 'svg':\n    case 'webp':\n      return FileImage;\n    default:\n      return File;\n  }\n};\n\nconst formatFileSize = (bytes: number): string => {\n  if (bytes === 0) return '0 B';\n  const k = 1024;\n  const sizes = ['B', 'KB', 'MB', 'GB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;\n};\n\nexport const FilePicker: React.FC<FilePickerProps> = React.memo(({\n  basePath,\n  onSelect,\n  onClose,\n  initialQuery = \"\",\n  className,\n  allowDirectorySelection = false\n}) => {\n  const [currentPath, setCurrentPath] = useState(basePath);\n  const [entries, setEntries] = useState<FileEntry[]>([]);\n  const [searchQuery, setSearchQuery] = useState(initialQuery);\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  \n  const searchInputRef = useRef<HTMLInputElement>(null);\n  const scrollContainerRef = useRef<HTMLDivElement>(null);\n  const searchDebounceRef = useRef<NodeJS.Timeout>();\n\n  // Filter and sort entries\n  const displayEntries = useMemo(() => {\n    const filtered = searchQuery.trim()\n      ? entries.filter(entry => \n          entry.name.toLowerCase().includes(searchQuery.toLowerCase())\n        )\n      : entries;\n    \n    return filtered.sort((a, b) => {\n      if (a.is_directory !== b.is_directory) {\n        return a.is_directory ? -1 : 1;\n      }\n      return a.name.localeCompare(b.name);\n    });\n  }, [entries, searchQuery]);\n\n  // Virtual scrolling setup\n  const virtualizer = useVirtualizer({\n    count: displayEntries.length,\n    getScrollElement: () => scrollContainerRef.current,\n    estimateSize: () => 32, // Height of each item\n    overscan: 10, // Number of items to render outside viewport\n  });\n\n  const virtualItems = virtualizer.getVirtualItems();\n\n  // Load directory contents\n  const loadDirectory = useCallback(async (path: string) => {\n    const cacheKey = path;\n    \n    // Check cache first\n    if (globalDirectoryCache.has(cacheKey)) {\n      setEntries(globalDirectoryCache.get(cacheKey)!);\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n    \n    try {\n      const result = await api.listDirectoryContents(path);\n      globalDirectoryCache.set(cacheKey, result);\n      setEntries(result);\n    } catch (err) {\n      setError(err instanceof Error ? err.message : 'Failed to load directory');\n    } finally {\n      setIsLoading(false);\n    }\n  }, []);\n\n  // Search functionality\n  const performSearch = useCallback(async (query: string) => {\n    if (!query.trim()) {\n      loadDirectory(currentPath);\n      return;\n    }\n\n    const cacheKey = `${currentPath}:${query}`;\n    \n    if (globalSearchCache.has(cacheKey)) {\n      setEntries(globalSearchCache.get(cacheKey)!);\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n    \n    try {\n      const result = await api.searchFiles(currentPath, query);\n      globalSearchCache.set(cacheKey, result);\n      setEntries(result);\n    } catch (err) {\n      setError(err instanceof Error ? err.message : 'Search failed');\n    } finally {\n      setIsLoading(false);\n    }\n  }, [currentPath, loadDirectory]);\n\n  // Handle entry click\n  const handleEntryClick = useCallback((entry: FileEntry) => {\n    if (!entry.is_directory || allowDirectorySelection) {\n      onSelect(entry);\n    }\n  }, [onSelect, allowDirectorySelection]);\n\n  // Handle entry double click\n  const handleEntryDoubleClick = useCallback((entry: FileEntry) => {\n    if (entry.is_directory) {\n      setCurrentPath(entry.path);\n      setSearchQuery(\"\");\n      setSelectedIndex(0);\n    } else {\n      onSelect(entry);\n    }\n  }, [onSelect]);\n\n  // Keyboard navigation\n  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {\n    if (displayEntries.length === 0) return;\n\n    switch (e.key) {\n      case 'ArrowUp':\n        e.preventDefault();\n        setSelectedIndex(prev => Math.max(0, prev - 1));\n        break;\n      case 'ArrowDown':\n        e.preventDefault();\n        setSelectedIndex(prev => Math.min(displayEntries.length - 1, prev + 1));\n        break;\n      case 'Enter':\n        e.preventDefault();\n        const selectedEntry = displayEntries[selectedIndex];\n        if (selectedEntry) {\n          if (e.shiftKey || !selectedEntry.is_directory) {\n            handleEntryClick(selectedEntry);\n          } else {\n            handleEntryDoubleClick(selectedEntry);\n          }\n        }\n        break;\n      case 'Escape':\n        e.preventDefault();\n        onClose();\n        break;\n    }\n  }, [displayEntries, selectedIndex, handleEntryClick, handleEntryDoubleClick, onClose]);\n\n  // Debounced search\n  useEffect(() => {\n    if (searchDebounceRef.current) {\n      clearTimeout(searchDebounceRef.current);\n    }\n\n    searchDebounceRef.current = setTimeout(() => {\n      performSearch(searchQuery);\n    }, 300);\n\n    return () => {\n      if (searchDebounceRef.current) {\n        clearTimeout(searchDebounceRef.current);\n      }\n    };\n  }, [searchQuery, performSearch]);\n\n  // Load initial directory\n  useEffect(() => {\n    loadDirectory(currentPath);\n  }, [currentPath, loadDirectory]);\n\n  // Focus search input on mount\n  useEffect(() => {\n    searchInputRef.current?.focus();\n  }, []);\n\n  // Scroll selected item into view\n  useEffect(() => {\n    const item = virtualizer.getVirtualItems().find(\n      vItem => vItem.index === selectedIndex\n    );\n    if (item) {\n      virtualizer.scrollToIndex(selectedIndex, { align: 'center' });\n    }\n  }, [selectedIndex, virtualizer]);\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, scale: 0.95 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={{ opacity: 0, scale: 0.95 }}\n      className={cn(\"flex flex-col bg-background rounded-lg shadow-lg\", className)}\n      onKeyDown={handleKeyDown}\n    >\n      {/* Header */}\n      <div className=\"flex items-center gap-2 p-4 border-b\">\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          onClick={() => {\n            const parentPath = currentPath.split('/').slice(0, -1).join('/') || '/';\n            setCurrentPath(parentPath);\n            setSearchQuery(\"\");\n          }}\n          disabled={currentPath === '/' || currentPath === basePath}\n          className=\"h-8 w-8\"\n        >\n          <ArrowLeft className=\"h-4 w-4\" />\n        </Button>\n\n        <div className=\"flex-1 flex items-center gap-2\">\n          <Search className=\"h-4 w-4 text-muted-foreground\" />\n          <input\n            ref={searchInputRef}\n            type=\"text\"\n            value={searchQuery}\n            onChange={(e) => setSearchQuery(e.target.value)}\n            placeholder=\"Search files...\"\n            className=\"flex-1 bg-transparent outline-none text-sm\"\n          />\n        </div>\n\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          onClick={onClose}\n          className=\"h-8 w-8\"\n        >\n          <X className=\"h-4 w-4\" />\n        </Button>\n      </div>\n\n      {/* Current path */}\n      <div className=\"px-4 py-2 border-b\">\n        <div className=\"text-xs text-muted-foreground truncate\">\n          {currentPath}\n        </div>\n      </div>\n\n      {/* File list with virtual scrolling */}\n      <div \n        ref={scrollContainerRef}\n        className=\"flex-1 overflow-auto\"\n        style={{ height: '400px' }}\n      >\n        {isLoading && (\n          <div className=\"flex items-center justify-center h-full\">\n            <div className=\"text-sm text-muted-foreground\">Loading...</div>\n          </div>\n        )}\n\n        {error && (\n          <div className=\"flex items-center justify-center h-full\">\n            <div className=\"text-sm text-destructive\">{error}</div>\n          </div>\n        )}\n\n        {!isLoading && !error && displayEntries.length === 0 && (\n          <div className=\"flex flex-col items-center justify-center h-full\">\n            <Search className=\"h-8 w-8 text-muted-foreground mb-2\" />\n            <span className=\"text-sm text-muted-foreground\">\n              {searchQuery.trim() ? 'No files found' : 'Empty directory'}\n            </span>\n          </div>\n        )}\n\n        {displayEntries.length > 0 && (\n          <div\n            style={{\n              height: `${virtualizer.getTotalSize()}px`,\n              width: '100%',\n              position: 'relative',\n            }}\n          >\n            {virtualItems.map((virtualRow) => {\n              const entry = displayEntries[virtualRow.index];\n              const Icon = getFileIcon(entry);\n              const isSelected = virtualRow.index === selectedIndex;\n              \n              return (\n                <div\n                  key={virtualRow.key}\n                  style={{\n                    position: 'absolute',\n                    top: 0,\n                    left: 0,\n                    width: '100%',\n                    height: `${virtualRow.size}px`,\n                    transform: `translateY(${virtualRow.start}px)`,\n                  }}\n                >\n                  <button\n                    onClick={() => handleEntryClick(entry)}\n                    onDoubleClick={() => handleEntryDoubleClick(entry)}\n                    onMouseEnter={() => setSelectedIndex(virtualRow.index)}\n                    className={cn(\n                      \"w-full flex items-center gap-2 px-2 py-1.5\",\n                      \"hover:bg-accent transition-colors\",\n                      \"text-left text-sm h-8\",\n                      isSelected && \"bg-accent\"\n                    )}\n                    title={entry.is_directory ? \"Click to select • Double-click to enter\" : \"Click to select\"}\n                  >\n                    <Icon className={cn(\n                      \"h-4 w-4 flex-shrink-0\",\n                      entry.is_directory ? \"text-blue-500\" : \"text-muted-foreground\"\n                    )} />\n                    \n                    <span className=\"flex-1 truncate\">\n                      {entry.name}\n                    </span>\n                    \n                    {!entry.is_directory && entry.size > 0 && (\n                      <span className=\"text-xs text-muted-foreground\">\n                        {formatFileSize(entry.size)}\n                      </span>\n                    )}\n                    \n                    {entry.is_directory && (\n                      <ChevronRight className=\"h-4 w-4 text-muted-foreground\" />\n                    )}\n                  </button>\n                </div>\n              );\n            })}\n          </div>\n        )}\n      </div>\n\n      {/* Footer */}\n      <div className=\"flex items-center justify-between p-4 border-t\">\n        <div className=\"text-xs text-muted-foreground\">\n          {displayEntries.length} {displayEntries.length === 1 ? 'item' : 'items'}\n        </div>\n        {allowDirectorySelection && (\n          <div className=\"text-xs text-muted-foreground\">\n            Shift+Enter to select directory\n          </div>\n        )}\n      </div>\n    </motion.div>\n  );\n});"
  },
  {
    "path": "src/components/FilePicker.tsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { api } from \"@/lib/api\";\nimport { \n  X, \n  Folder, \n  File, \n  ArrowLeft,\n  FileCode,\n  FileText,\n  FileImage,\n  Search,\n  ChevronRight\n} from \"lucide-react\";\nimport type { FileEntry } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\n// Global caches that persist across component instances\nconst globalDirectoryCache = new Map<string, FileEntry[]>();\nconst globalSearchCache = new Map<string, FileEntry[]>();\n\n// Note: These caches persist for the lifetime of the application.\n// In a production app, you might want to:\n// 1. Add TTL (time-to-live) to expire old entries\n// 2. Implement LRU (least recently used) eviction\n// 3. Clear caches when the working directory changes\n// 4. Add a maximum cache size limit\n\ninterface FilePickerProps {\n  /**\n   * The base directory path to browse\n   */\n  basePath: string;\n  /**\n   * Callback when a file/directory is selected\n   */\n  onSelect: (entry: FileEntry) => void;\n  /**\n   * Callback to close the picker\n   */\n  onClose: () => void;\n  /**\n   * Initial search query\n   */\n  initialQuery?: string;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n// File icon mapping based on extension\nconst getFileIcon = (entry: FileEntry) => {\n  if (entry.is_directory) return Folder;\n  \n  const ext = entry.extension?.toLowerCase();\n  if (!ext) return File;\n  \n  // Code files\n  if (['ts', 'tsx', 'js', 'jsx', 'py', 'rs', 'go', 'java', 'cpp', 'c', 'h'].includes(ext)) {\n    return FileCode;\n  }\n  \n  // Text/Markdown files\n  if (['md', 'txt', 'json', 'yaml', 'yml', 'toml', 'xml', 'html', 'css'].includes(ext)) {\n    return FileText;\n  }\n  \n  // Image files\n  if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico'].includes(ext)) {\n    return FileImage;\n  }\n  \n  return File;\n};\n\n// Format file size to human readable\nconst formatFileSize = (bytes: number): string => {\n  if (bytes === 0) return '';\n  const k = 1024;\n  const sizes = ['B', 'KB', 'MB', 'GB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n};\n\n/**\n * FilePicker component - File browser with fuzzy search\n * \n * @example\n * <FilePicker\n *   basePath=\"/Users/example/project\"\n *   onSelect={(entry) => console.log('Selected:', entry)}\n *   onClose={() => setShowPicker(false)}\n * />\n */\nexport const FilePicker: React.FC<FilePickerProps> = ({\n  basePath,\n  onSelect,\n  onClose,\n  initialQuery = \"\",\n  className,\n}) => {\n  const searchQuery = initialQuery;\n  \n  const [currentPath, setCurrentPath] = useState(basePath);\n  const [entries, setEntries] = useState<FileEntry[]>(() => \n    searchQuery.trim() ? [] : globalDirectoryCache.get(basePath) || []\n  );\n  const [searchResults, setSearchResults] = useState<FileEntry[]>(() => {\n    if (searchQuery.trim()) {\n      const cacheKey = `${basePath}:${searchQuery}`;\n      return globalSearchCache.get(cacheKey) || [];\n    }\n    return [];\n  });\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [pathHistory, setPathHistory] = useState<string[]>([basePath]);\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [isShowingCached, setIsShowingCached] = useState(() => {\n    // Check if we're showing cached data on mount\n    if (searchQuery.trim()) {\n      const cacheKey = `${basePath}:${searchQuery}`;\n      return globalSearchCache.has(cacheKey);\n    }\n    return globalDirectoryCache.has(basePath);\n  });\n  \n  const searchDebounceRef = useRef<NodeJS.Timeout | null>(null);\n  const fileListRef = useRef<HTMLDivElement>(null);\n  \n  // Computed values\n  const displayEntries = searchQuery.trim() ? searchResults : entries;\n  const canGoBack = pathHistory.length > 1;\n  \n  // Get relative path for display\n  const relativePath = currentPath.startsWith(basePath) \n    ? currentPath.slice(basePath.length) || '/'\n    : currentPath;\n\n  // Load directory contents\n  useEffect(() => {\n    loadDirectory(currentPath);\n  }, [currentPath]);\n\n  // Debounced search\n  useEffect(() => {\n    if (searchDebounceRef.current) {\n      clearTimeout(searchDebounceRef.current);\n    }\n\n    if (searchQuery.trim()) {\n      const cacheKey = `${basePath}:${searchQuery}`;\n      \n      // Immediately show cached results if available\n      if (globalSearchCache.has(cacheKey)) {\n        console.log('[FilePicker] Immediately showing cached search results for:', searchQuery);\n        setSearchResults(globalSearchCache.get(cacheKey) || []);\n        setIsShowingCached(true);\n        setError(null);\n      }\n      \n      // Schedule fresh search after debounce\n      searchDebounceRef.current = setTimeout(() => {\n        performSearch(searchQuery);\n      }, 300);\n    } else {\n      setSearchResults([]);\n      setIsShowingCached(false);\n    }\n\n    return () => {\n      if (searchDebounceRef.current) {\n        clearTimeout(searchDebounceRef.current);\n      }\n    };\n  }, [searchQuery, basePath]);\n\n  // Reset selected index when entries change\n  useEffect(() => {\n    setSelectedIndex(0);\n  }, [entries, searchResults]);\n\n  // Keyboard navigation\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      const displayEntries = searchQuery.trim() ? searchResults : entries;\n      \n      switch (e.key) {\n        case 'Escape':\n          e.preventDefault();\n          onClose();\n          break;\n          \n        case 'Enter':\n          e.preventDefault();\n          // Enter always selects the current item (file or directory)\n          if (displayEntries.length > 0 && selectedIndex < displayEntries.length) {\n            onSelect(displayEntries[selectedIndex]);\n          }\n          break;\n          \n        case 'ArrowUp':\n          e.preventDefault();\n          setSelectedIndex(prev => Math.max(0, prev - 1));\n          break;\n          \n        case 'ArrowDown':\n          e.preventDefault();\n          setSelectedIndex(prev => Math.min(displayEntries.length - 1, prev + 1));\n          break;\n          \n        case 'ArrowRight':\n          e.preventDefault();\n          // Right arrow enters directories\n          if (displayEntries.length > 0 && selectedIndex < displayEntries.length) {\n            const entry = displayEntries[selectedIndex];\n            if (entry.is_directory) {\n              navigateToDirectory(entry.path);\n            }\n          }\n          break;\n          \n        case 'ArrowLeft':\n          e.preventDefault();\n          // Left arrow goes back to parent directory\n          if (canGoBack) {\n            navigateBack();\n          }\n          break;\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [entries, searchResults, selectedIndex, searchQuery, canGoBack]);\n\n  // Scroll selected item into view\n  useEffect(() => {\n    if (fileListRef.current) {\n      const selectedElement = fileListRef.current.querySelector(`[data-index=\"${selectedIndex}\"]`);\n      if (selectedElement) {\n        selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n      }\n    }\n  }, [selectedIndex]);\n\n  const loadDirectory = async (path: string) => {\n    try {\n      console.log('[FilePicker] Loading directory:', path);\n      \n      // Check cache first and show immediately\n      if (globalDirectoryCache.has(path)) {\n        console.log('[FilePicker] Showing cached contents for:', path);\n        setEntries(globalDirectoryCache.get(path) || []);\n        setIsShowingCached(true);\n        setError(null);\n      } else {\n        // Only show loading if we don't have cached data\n        setIsLoading(true);\n      }\n      \n      // Always fetch fresh data in background\n      const contents = await api.listDirectoryContents(path);\n      console.log('[FilePicker] Loaded fresh contents:', contents.length, 'items');\n      \n      // Cache the results\n      globalDirectoryCache.set(path, contents);\n      \n      // Update with fresh data\n      setEntries(contents);\n      setIsShowingCached(false);\n      setError(null);\n    } catch (err) {\n      console.error('[FilePicker] Failed to load directory:', path, err);\n      console.error('[FilePicker] Error details:', err);\n      // Only set error if we don't have cached data to show\n      if (!globalDirectoryCache.has(path)) {\n        setError(err instanceof Error ? err.message : 'Failed to load directory');\n      }\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const performSearch = async (query: string) => {\n    try {\n      console.log('[FilePicker] Searching for:', query, 'in:', basePath);\n      \n      // Create cache key that includes both query and basePath\n      const cacheKey = `${basePath}:${query}`;\n      \n      // Check cache first and show immediately\n      if (globalSearchCache.has(cacheKey)) {\n        console.log('[FilePicker] Showing cached search results for:', query);\n        setSearchResults(globalSearchCache.get(cacheKey) || []);\n        setIsShowingCached(true);\n        setError(null);\n      } else {\n        // Only show loading if we don't have cached data\n        setIsLoading(true);\n      }\n      \n      // Always fetch fresh results in background\n      const results = await api.searchFiles(basePath, query);\n      console.log('[FilePicker] Fresh search results:', results.length, 'items');\n      \n      // Cache the results\n      globalSearchCache.set(cacheKey, results);\n      \n      // Update with fresh results\n      setSearchResults(results);\n      setIsShowingCached(false);\n      setError(null);\n    } catch (err) {\n      console.error('[FilePicker] Search failed:', query, err);\n      // Only set error if we don't have cached data to show\n      const cacheKey = `${basePath}:${query}`;\n      if (!globalSearchCache.has(cacheKey)) {\n        setError(err instanceof Error ? err.message : 'Search failed');\n      }\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const navigateToDirectory = (path: string) => {\n    setCurrentPath(path);\n    setPathHistory(prev => [...prev, path]);\n  };\n\n  const navigateBack = () => {\n    if (pathHistory.length > 1) {\n      const newHistory = [...pathHistory];\n      newHistory.pop(); // Remove current\n      const previousPath = newHistory[newHistory.length - 1];\n      \n      // Don't go beyond the base path\n      if (previousPath.startsWith(basePath) || previousPath === basePath) {\n        setCurrentPath(previousPath);\n        setPathHistory(newHistory);\n      }\n    }\n  };\n\n  const handleEntryClick = (entry: FileEntry) => {\n    // Single click always selects (file or directory)\n    onSelect(entry);\n  };\n  \n  const handleEntryDoubleClick = (entry: FileEntry) => {\n    // Double click navigates into directories\n    if (entry.is_directory) {\n      navigateToDirectory(entry.path);\n    }\n  };\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, scale: 0.95 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={{ opacity: 0, scale: 0.95 }}\n      className={cn(\n        \"absolute bottom-full mb-2 left-0 z-50\",\n        \"w-[500px] h-[400px]\",\n        \"bg-background border border-border rounded-lg shadow-lg\",\n        \"flex flex-col overflow-hidden\",\n        className\n      )}\n    >\n      {/* Header */}\n      <div className=\"border-b border-border p-3\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-2\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={navigateBack}\n              disabled={!canGoBack}\n              className=\"h-8 w-8\"\n            >\n              <ArrowLeft className=\"h-4 w-4\" />\n            </Button>\n            <span className=\"text-sm font-mono text-muted-foreground truncate max-w-[300px]\">\n              {relativePath}\n            </span>\n          </div>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={onClose}\n            className=\"h-8 w-8\"\n          >\n            <X className=\"h-4 w-4\" />\n          </Button>\n        </div>\n      </div>\n\n      {/* File List */}\n      <div className=\"flex-1 overflow-y-auto relative\">\n        {/* Show loading only if no cached data */}\n        {isLoading && displayEntries.length === 0 && (\n          <div className=\"flex items-center justify-center h-full\">\n            <span className=\"text-sm text-muted-foreground\">Loading...</span>\n          </div>\n        )}\n\n        {/* Show subtle indicator when displaying cached data while fetching fresh */}\n        {isShowingCached && isLoading && displayEntries.length > 0 && (\n          <div className=\"absolute top-1 right-2 text-xs text-muted-foreground/50 italic\">\n            updating...\n          </div>\n        )}\n\n        {error && displayEntries.length === 0 && (\n          <div className=\"flex items-center justify-center h-full\">\n            <span className=\"text-sm text-destructive\">{error}</span>\n          </div>\n        )}\n\n        {!isLoading && !error && displayEntries.length === 0 && (\n          <div className=\"flex flex-col items-center justify-center h-full\">\n            <Search className=\"h-8 w-8 text-muted-foreground mb-2\" />\n            <span className=\"text-sm text-muted-foreground\">\n              {searchQuery.trim() ? 'No files found' : 'Empty directory'}\n            </span>\n          </div>\n        )}\n\n        {displayEntries.length > 0 && (\n          <div className=\"p-2 space-y-0.5\" ref={fileListRef}>\n            {displayEntries.map((entry, index) => {\n              const Icon = getFileIcon(entry);\n              const isSearching = searchQuery.trim() !== '';\n              const isSelected = index === selectedIndex;\n              \n              return (\n                <button\n                  key={entry.path}\n                  data-index={index}\n                  onClick={() => handleEntryClick(entry)}\n                  onDoubleClick={() => handleEntryDoubleClick(entry)}\n                  onMouseEnter={() => setSelectedIndex(index)}\n                  className={cn(\n                    \"w-full flex items-center gap-2 px-2 py-1.5 rounded-md\",\n                    \"hover:bg-accent transition-colors\",\n                    \"text-left text-sm\",\n                    isSelected && \"bg-accent\"\n                  )}\n                  title={entry.is_directory ? \"Click to select • Double-click to enter\" : \"Click to select\"}\n                >\n                  <Icon className={cn(\n                    \"h-4 w-4 flex-shrink-0\",\n                    entry.is_directory ? \"text-blue-500\" : \"text-muted-foreground\"\n                  )} />\n                  \n                  <span className=\"flex-1 truncate\">\n                    {entry.name}\n                  </span>\n                  \n                  {!entry.is_directory && entry.size > 0 && (\n                    <span className=\"text-xs text-muted-foreground\">\n                      {formatFileSize(entry.size)}\n                    </span>\n                  )}\n                  \n                  {entry.is_directory && (\n                    <ChevronRight className=\"h-4 w-4 text-muted-foreground\" />\n                  )}\n                  \n                  {isSearching && (\n                    <span className=\"text-xs text-muted-foreground font-mono truncate max-w-[150px]\">\n                      {entry.path.replace(basePath, '').replace(/^\\//, '')}\n                    </span>\n                  )}\n                </button>\n              );\n            })}\n          </div>\n        )}\n      </div>\n\n      {/* Footer */}\n      <div className=\"border-t border-border p-2\">\n        <p className=\"text-xs text-muted-foreground text-center\">\n          ↑↓ Navigate • Enter Select • → Enter Directory • ← Go Back • Esc Close\n        </p>\n      </div>\n    </motion.div>\n  );\n}; "
  },
  {
    "path": "src/components/FloatingPromptInput.tsx",
    "content": "import React, { useState, useRef, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport {\n  Send,\n  Maximize2,\n  Minimize2,\n  ChevronUp,\n  Sparkles,\n  Zap,\n  Square,\n  Brain,\n  Lightbulb,\n  Cpu,\n  Rocket,\n  \n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Popover } from \"@/components/ui/popover\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { TooltipProvider, TooltipSimple, Tooltip, TooltipTrigger, TooltipContent } from \"@/components/ui/tooltip-modern\";\nimport { FilePicker } from \"./FilePicker\";\nimport { SlashCommandPicker } from \"./SlashCommandPicker\";\nimport { ImagePreview } from \"./ImagePreview\";\nimport { type FileEntry, type SlashCommand } from \"@/lib/api\";\n\n// Conditional import for Tauri webview window\nlet tauriGetCurrentWebviewWindow: any;\ntry {\n  if (typeof window !== 'undefined' && window.__TAURI__) {\n    tauriGetCurrentWebviewWindow = require(\"@tauri-apps/api/webviewWindow\").getCurrentWebviewWindow;\n  }\n} catch (e) {\n  console.log('[FloatingPromptInput] Tauri webview API not available, using web mode');\n}\n\n// Web-compatible replacement\nconst getCurrentWebviewWindow = tauriGetCurrentWebviewWindow || (() => ({ listen: () => Promise.resolve(() => {}) }));\n\ninterface FloatingPromptInputProps {\n  /**\n   * Callback when prompt is sent\n   */\n  onSend: (prompt: string, model: \"sonnet\" | \"opus\") => void;\n  /**\n   * Whether the input is loading\n   */\n  isLoading?: boolean;\n  /**\n   * Whether the input is disabled\n   */\n  disabled?: boolean;\n  /**\n   * Default model to select\n   */\n  defaultModel?: \"sonnet\" | \"opus\";\n  /**\n   * Project path for file picker\n   */\n  projectPath?: string;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n  /**\n   * Callback when cancel is clicked (only during loading)\n   */\n  onCancel?: () => void;\n  /**\n   * Extra menu items to display in the prompt bar\n   */\n  extraMenuItems?: React.ReactNode;\n}\n\nexport interface FloatingPromptInputRef {\n  addImage: (imagePath: string) => void;\n}\n\n/**\n * Thinking mode type definition\n */\ntype ThinkingMode = \"auto\" | \"think\" | \"think_hard\" | \"think_harder\" | \"ultrathink\";\n\n/**\n * Thinking mode configuration\n */\ntype ThinkingModeConfig = {\n  id: ThinkingMode;\n  name: string;\n  description: string;\n  level: number; // 0-4 for visual indicator\n  phrase?: string; // The phrase to append\n  icon: React.ReactNode;\n  color: string;\n  shortName: string;\n};\n\nconst THINKING_MODES: ThinkingModeConfig[] = [\n  {\n    id: \"auto\",\n    name: \"Auto\",\n    description: \"Let Claude decide\",\n    level: 0,\n    icon: <Sparkles className=\"h-3.5 w-3.5\" />,\n    color: \"text-muted-foreground\",\n    shortName: \"A\"\n  },\n  {\n    id: \"think\",\n    name: \"Think\",\n    description: \"Basic reasoning\",\n    level: 1,\n    phrase: \"think\",\n    icon: <Lightbulb className=\"h-3.5 w-3.5\" />,\n    color: \"text-primary\",\n    shortName: \"T\"\n  },\n  {\n    id: \"think_hard\",\n    name: \"Think Hard\",\n    description: \"Deeper analysis\",\n    level: 2,\n    phrase: \"think hard\",\n    icon: <Brain className=\"h-3.5 w-3.5\" />,\n    color: \"text-primary\",\n    shortName: \"T+\"\n  },\n  {\n    id: \"think_harder\",\n    name: \"Think Harder\",\n    description: \"Extensive reasoning\",\n    level: 3,\n    phrase: \"think harder\",\n    icon: <Cpu className=\"h-3.5 w-3.5\" />,\n    color: \"text-primary\",\n    shortName: \"T++\"\n  },\n  {\n    id: \"ultrathink\",\n    name: \"Ultrathink\",\n    description: \"Maximum computation\",\n    level: 4,\n    phrase: \"ultrathink\",\n    icon: <Rocket className=\"h-3.5 w-3.5\" />,\n    color: \"text-primary\",\n    shortName: \"Ultra\"\n  }\n];\n\n/**\n * ThinkingModeIndicator component - Shows visual indicator bars for thinking level\n */\nconst ThinkingModeIndicator: React.FC<{ level: number; color?: string }> = ({ level, color: _color }) => {\n  const getBarColor = (barIndex: number) => {\n    if (barIndex > level) return \"bg-muted\";\n    return \"bg-primary\";\n  };\n  \n  return (\n    <div className=\"flex items-center gap-0.5\">\n      {[1, 2, 3, 4].map((i) => (\n        <div\n          key={i}\n          className={cn(\n            \"w-1 h-3 rounded-full transition-all duration-200\",\n            getBarColor(i),\n            i <= level && \"shadow-sm\"\n          )}\n        />\n      ))}\n    </div>\n  );\n};\n\ntype Model = {\n  id: \"sonnet\" | \"opus\";\n  name: string;\n  description: string;\n  icon: React.ReactNode;\n  shortName: string;\n  color: string;\n};\n\nconst MODELS: Model[] = [\n  {\n    id: \"sonnet\",\n    name: \"Claude 4 Sonnet\",\n    description: \"Faster, efficient for most tasks\",\n    icon: <Zap className=\"h-3.5 w-3.5\" />,\n    shortName: \"S\",\n    color: \"text-primary\"\n  },\n  {\n    id: \"opus\",\n    name: \"Claude 4 Opus\",\n    description: \"More capable, better for complex tasks\",\n    icon: <Zap className=\"h-3.5 w-3.5\" />,\n    shortName: \"O\",\n    color: \"text-primary\"\n  }\n];\n\n/**\n * FloatingPromptInput component - Fixed position prompt input with model picker\n * \n * @example\n * const promptRef = useRef<FloatingPromptInputRef>(null);\n * <FloatingPromptInput\n *   ref={promptRef}\n *   onSend={(prompt, model) => console.log('Send:', prompt, model)}\n *   isLoading={false}\n * />\n */\nconst FloatingPromptInputInner = (\n  {\n    onSend,\n    isLoading = false,\n    disabled = false,\n    defaultModel = \"sonnet\",\n    projectPath,\n    className,\n    onCancel,\n    extraMenuItems,\n  }: FloatingPromptInputProps,\n  ref: React.Ref<FloatingPromptInputRef>,\n) => {\n  const [prompt, setPrompt] = useState(\"\");\n  const [selectedModel, setSelectedModel] = useState<\"sonnet\" | \"opus\">(defaultModel);\n  const [selectedThinkingMode, setSelectedThinkingMode] = useState<ThinkingMode>(\"auto\");\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [modelPickerOpen, setModelPickerOpen] = useState(false);\n  const [thinkingModePickerOpen, setThinkingModePickerOpen] = useState(false);\n  const [showFilePicker, setShowFilePicker] = useState(false);\n  const [filePickerQuery, setFilePickerQuery] = useState(\"\");\n  const [showSlashCommandPicker, setShowSlashCommandPicker] = useState(false);\n  const [slashCommandQuery, setSlashCommandQuery] = useState(\"\");\n  const [cursorPosition, setCursorPosition] = useState(0);\n  const [embeddedImages, setEmbeddedImages] = useState<string[]>([]);\n  const [dragActive, setDragActive] = useState(false);\n\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const expandedTextareaRef = useRef<HTMLTextAreaElement>(null);\n  const unlistenDragDropRef = useRef<(() => void) | null>(null);\n  const [textareaHeight, setTextareaHeight] = useState<number>(48);\n  const isIMEComposingRef = useRef(false);\n\n  // Expose a method to add images programmatically\n  React.useImperativeHandle(\n    ref,\n    () => ({\n      addImage: (imagePath: string) => {\n        setPrompt(currentPrompt => {\n          const existingPaths = extractImagePaths(currentPrompt);\n          if (existingPaths.includes(imagePath)) {\n            return currentPrompt; // Image already added\n          }\n\n          // Wrap path in quotes if it contains spaces\n          const mention = imagePath.includes(' ') ? `@\"${imagePath}\"` : `@${imagePath}`;\n          const newPrompt = currentPrompt + (currentPrompt.endsWith(' ') || currentPrompt === '' ? '' : ' ') + mention + ' ';\n\n          // Focus the textarea\n          setTimeout(() => {\n            const target = isExpanded ? expandedTextareaRef.current : textareaRef.current;\n            target?.focus();\n            target?.setSelectionRange(newPrompt.length, newPrompt.length);\n          }, 0);\n\n          return newPrompt;\n        });\n      }\n    }),\n    [isExpanded]\n  );\n\n  // Helper function to check if a file is an image\n  const isImageFile = (path: string): boolean => {\n    // Check if it's a data URL\n    if (path.startsWith('data:image/')) {\n      return true;\n    }\n    // Otherwise check file extension\n    const ext = path.split('.').pop()?.toLowerCase();\n    return ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'].includes(ext || '');\n  };\n\n  // Extract image paths from prompt text\n  const extractImagePaths = (text: string): string[] => {\n    console.log('[extractImagePaths] Input text length:', text.length);\n    \n    // Updated regex to handle both quoted and unquoted paths\n    // Pattern 1: @\"path with spaces or data URLs\" - quoted paths\n    // Pattern 2: @path - unquoted paths (continues until @ or end)\n    const quotedRegex = /@\"([^\"]+)\"/g;\n    const unquotedRegex = /@([^@\\n\\s]+)/g;\n    \n    const pathsSet = new Set<string>(); // Use Set to ensure uniqueness\n    \n    // First, extract quoted paths (including data URLs)\n    let matches = Array.from(text.matchAll(quotedRegex));\n    console.log('[extractImagePaths] Quoted matches:', matches.length);\n    \n    for (const match of matches) {\n      const path = match[1]; // No need to trim, quotes preserve exact path\n      console.log('[extractImagePaths] Processing quoted path:', path.startsWith('data:') ? 'data URL' : path);\n      \n      // For data URLs, use as-is; for file paths, convert to absolute\n      const fullPath = path.startsWith('data:') \n        ? path \n        : (path.startsWith('/') ? path : (projectPath ? `${projectPath}/${path}` : path));\n      \n      if (isImageFile(fullPath)) {\n        pathsSet.add(fullPath);\n      }\n    }\n    \n    // Remove quoted mentions from text to avoid double-matching\n    let textWithoutQuoted = text.replace(quotedRegex, '');\n    \n    // Then extract unquoted paths (typically file paths)\n    matches = Array.from(textWithoutQuoted.matchAll(unquotedRegex));\n    console.log('[extractImagePaths] Unquoted matches:', matches.length);\n    \n    for (const match of matches) {\n      const path = match[1].trim();\n      // Skip if it looks like a data URL fragment (shouldn't happen with proper quoting)\n      if (path.includes('data:')) continue;\n      \n      console.log('[extractImagePaths] Processing unquoted path:', path);\n      \n      // Convert relative path to absolute if needed\n      const fullPath = path.startsWith('/') ? path : (projectPath ? `${projectPath}/${path}` : path);\n      \n      if (isImageFile(fullPath)) {\n        pathsSet.add(fullPath);\n      }\n    }\n\n    const uniquePaths = Array.from(pathsSet);\n    console.log('[extractImagePaths] Final extracted paths (unique):', uniquePaths.length);\n    return uniquePaths;\n  };\n\n  // Update embedded images when prompt changes\n  useEffect(() => {\n    console.log('[useEffect] Prompt changed:', prompt);\n    const imagePaths = extractImagePaths(prompt);\n    console.log('[useEffect] Setting embeddedImages to:', imagePaths);\n    setEmbeddedImages(imagePaths);\n    \n    // Auto-resize on prompt change (handles paste, programmatic changes, etc.)\n    if (textareaRef.current && !isExpanded) {\n      textareaRef.current.style.height = 'auto';\n      const scrollHeight = textareaRef.current.scrollHeight;\n      const newHeight = Math.min(Math.max(scrollHeight, 48), 240);\n      setTextareaHeight(newHeight);\n      textareaRef.current.style.height = `${newHeight}px`;\n    }\n  }, [prompt, projectPath, isExpanded]);\n\n  // Set up Tauri drag-drop event listener\n  useEffect(() => {\n    // This effect runs only once on component mount to set up the listener.\n    let lastDropTime = 0;\n\n    const setupListener = async () => {\n      try {\n        // If a listener from a previous mount/render is still around, clean it up.\n        if (unlistenDragDropRef.current) {\n          unlistenDragDropRef.current();\n        }\n\n        const webview = getCurrentWebviewWindow();\n        unlistenDragDropRef.current = await webview.onDragDropEvent((event: any) => {\n          if (event.payload.type === 'enter' || event.payload.type === 'over') {\n            setDragActive(true);\n          } else if (event.payload.type === 'leave') {\n            setDragActive(false);\n          } else if (event.payload.type === 'drop' && event.payload.paths) {\n            setDragActive(false);\n\n            const currentTime = Date.now();\n            if (currentTime - lastDropTime < 200) {\n              // This debounce is crucial to handle the storm of drop events\n              // that Tauri/OS can fire for a single user action.\n              return;\n            }\n            lastDropTime = currentTime;\n\n            const droppedPaths = event.payload.paths as string[];\n            const imagePaths = droppedPaths.filter(isImageFile);\n\n            if (imagePaths.length > 0) {\n              setPrompt(currentPrompt => {\n                const existingPaths = extractImagePaths(currentPrompt);\n                const newPaths = imagePaths.filter(p => !existingPaths.includes(p));\n\n                if (newPaths.length === 0) {\n                  return currentPrompt; // All dropped images are already in the prompt\n                }\n\n                // Wrap paths with spaces in quotes for clarity\n                const mentionsToAdd = newPaths.map(p => {\n                  // If path contains spaces, wrap in quotes\n                  if (p.includes(' ')) {\n                    return `@\"${p}\"`;\n                  }\n                  return `@${p}`;\n                }).join(' ');\n                const newPrompt = currentPrompt + (currentPrompt.endsWith(' ') || currentPrompt === '' ? '' : ' ') + mentionsToAdd + ' ';\n\n                setTimeout(() => {\n                  const target = isExpanded ? expandedTextareaRef.current : textareaRef.current;\n                  target?.focus();\n                  target?.setSelectionRange(newPrompt.length, newPrompt.length);\n                }, 0);\n\n                return newPrompt;\n              });\n            }\n          }\n        });\n      } catch (error) {\n        console.error('Failed to set up Tauri drag-drop listener:', error);\n      }\n    };\n\n    setupListener();\n\n    return () => {\n      // On unmount, ensure we clean up the listener.\n      if (unlistenDragDropRef.current) {\n        unlistenDragDropRef.current();\n        unlistenDragDropRef.current = null;\n      }\n    };\n  }, []); // Empty dependency array ensures this runs only on mount/unmount.\n\n  useEffect(() => {\n    // Focus the appropriate textarea when expanded state changes\n    if (isExpanded && expandedTextareaRef.current) {\n      expandedTextareaRef.current.focus();\n    } else if (!isExpanded && textareaRef.current) {\n      textareaRef.current.focus();\n    }\n  }, [isExpanded]);\n\n  const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    const newValue = e.target.value;\n    const newCursorPosition = e.target.selectionStart || 0;\n    \n    // Auto-resize textarea based on content\n    if (textareaRef.current && !isExpanded) {\n      // Reset height to auto to get the actual scrollHeight\n      textareaRef.current.style.height = 'auto';\n      const scrollHeight = textareaRef.current.scrollHeight;\n      // Set min height to 48px and max to 240px (about 10 lines)\n      const newHeight = Math.min(Math.max(scrollHeight, 48), 240);\n      setTextareaHeight(newHeight);\n      textareaRef.current.style.height = `${newHeight}px`;\n    }\n\n    // Check if / was just typed at the beginning of input or after whitespace\n    if (newValue.length > prompt.length && newValue[newCursorPosition - 1] === '/') {\n      // Check if it's at the start or after whitespace\n      const isStartOfCommand = newCursorPosition === 1 || \n        (newCursorPosition > 1 && /\\s/.test(newValue[newCursorPosition - 2]));\n      \n      if (isStartOfCommand) {\n        console.log('[FloatingPromptInput] / detected for slash command');\n        setShowSlashCommandPicker(true);\n        setSlashCommandQuery(\"\");\n        setCursorPosition(newCursorPosition);\n      }\n    }\n\n    // Check if @ was just typed\n    if (projectPath?.trim() && newValue.length > prompt.length && newValue[newCursorPosition - 1] === '@') {\n      console.log('[FloatingPromptInput] @ detected, projectPath:', projectPath);\n      setShowFilePicker(true);\n      setFilePickerQuery(\"\");\n      setCursorPosition(newCursorPosition);\n    }\n\n    // Check if we're typing after / (for slash command search)\n    if (showSlashCommandPicker && newCursorPosition >= cursorPosition) {\n      // Find the / position before cursor\n      let slashPosition = -1;\n      for (let i = newCursorPosition - 1; i >= 0; i--) {\n        if (newValue[i] === '/') {\n          slashPosition = i;\n          break;\n        }\n        // Stop if we hit whitespace (new word)\n        if (newValue[i] === ' ' || newValue[i] === '\\n') {\n          break;\n        }\n      }\n\n      if (slashPosition !== -1) {\n        const query = newValue.substring(slashPosition + 1, newCursorPosition);\n        setSlashCommandQuery(query);\n      } else {\n        // / was removed or cursor moved away\n        setShowSlashCommandPicker(false);\n        setSlashCommandQuery(\"\");\n      }\n    }\n\n    // Check if we're typing after @ (for search query)\n    if (showFilePicker && newCursorPosition >= cursorPosition) {\n      // Find the @ position before cursor\n      let atPosition = -1;\n      for (let i = newCursorPosition - 1; i >= 0; i--) {\n        if (newValue[i] === '@') {\n          atPosition = i;\n          break;\n        }\n        // Stop if we hit whitespace (new word)\n        if (newValue[i] === ' ' || newValue[i] === '\\n') {\n          break;\n        }\n      }\n\n      if (atPosition !== -1) {\n        const query = newValue.substring(atPosition + 1, newCursorPosition);\n        setFilePickerQuery(query);\n      } else {\n        // @ was removed or cursor moved away\n        setShowFilePicker(false);\n        setFilePickerQuery(\"\");\n      }\n    }\n\n    setPrompt(newValue);\n    setCursorPosition(newCursorPosition);\n  };\n\n  const handleFileSelect = (entry: FileEntry) => {\n    if (textareaRef.current) {\n      // Find the @ position before cursor\n      let atPosition = -1;\n      for (let i = cursorPosition - 1; i >= 0; i--) {\n        if (prompt[i] === '@') {\n          atPosition = i;\n          break;\n        }\n        // Stop if we hit whitespace (new word)\n        if (prompt[i] === ' ' || prompt[i] === '\\n') {\n          break;\n        }\n      }\n\n      if (atPosition === -1) {\n        // @ not found, this shouldn't happen but handle gracefully\n        console.error('[FloatingPromptInput] @ position not found');\n        return;\n      }\n\n      // Replace the @ and partial query with the selected path (file or directory)\n      const textarea = textareaRef.current;\n      const beforeAt = prompt.substring(0, atPosition);\n      const afterCursor = prompt.substring(cursorPosition);\n      const relativePath = entry.path.startsWith(projectPath || '')\n        ? entry.path.slice((projectPath || '').length + 1)\n        : entry.path;\n\n      const newPrompt = `${beforeAt}@${relativePath} ${afterCursor}`;\n      setPrompt(newPrompt);\n      setShowFilePicker(false);\n      setFilePickerQuery(\"\");\n\n      // Focus back on textarea and set cursor position after the inserted path\n      setTimeout(() => {\n        textarea.focus();\n        const newCursorPos = beforeAt.length + relativePath.length + 2; // +2 for @ and space\n        textarea.setSelectionRange(newCursorPos, newCursorPos);\n      }, 0);\n    }\n  };\n\n  const handleFilePickerClose = () => {\n    setShowFilePicker(false);\n    setFilePickerQuery(\"\");\n    // Return focus to textarea\n    setTimeout(() => {\n      textareaRef.current?.focus();\n    }, 0);\n  };\n\n  const handleSlashCommandSelect = (command: SlashCommand) => {\n    const textarea = isExpanded ? expandedTextareaRef.current : textareaRef.current;\n    if (!textarea) return;\n\n    // Find the / position before cursor\n    let slashPosition = -1;\n    for (let i = cursorPosition - 1; i >= 0; i--) {\n      if (prompt[i] === '/') {\n        slashPosition = i;\n        break;\n      }\n      // Stop if we hit whitespace (new word)\n      if (prompt[i] === ' ' || prompt[i] === '\\n') {\n        break;\n      }\n    }\n\n    if (slashPosition === -1) {\n      console.error('[FloatingPromptInput] / position not found');\n      return;\n    }\n\n    // Simply insert the command syntax\n    const beforeSlash = prompt.substring(0, slashPosition);\n    const afterCursor = prompt.substring(cursorPosition);\n    \n    if (command.accepts_arguments) {\n      // Insert command with placeholder for arguments\n      const newPrompt = `${beforeSlash}${command.full_command} `;\n      setPrompt(newPrompt);\n      setShowSlashCommandPicker(false);\n      setSlashCommandQuery(\"\");\n\n      // Focus and position cursor after the command\n      setTimeout(() => {\n        textarea.focus();\n        const newCursorPos = beforeSlash.length + command.full_command.length + 1;\n        textarea.setSelectionRange(newCursorPos, newCursorPos);\n      }, 0);\n    } else {\n      // Insert command and close picker\n      const newPrompt = `${beforeSlash}${command.full_command} ${afterCursor}`;\n      setPrompt(newPrompt);\n      setShowSlashCommandPicker(false);\n      setSlashCommandQuery(\"\");\n\n      // Focus and position cursor after the command\n      setTimeout(() => {\n        textarea.focus();\n        const newCursorPos = beforeSlash.length + command.full_command.length + 1;\n        textarea.setSelectionRange(newCursorPos, newCursorPos);\n      }, 0);\n    }\n  };\n\n  const handleSlashCommandPickerClose = () => {\n    setShowSlashCommandPicker(false);\n    setSlashCommandQuery(\"\");\n    // Return focus to textarea\n    setTimeout(() => {\n      const textarea = isExpanded ? expandedTextareaRef.current : textareaRef.current;\n      textarea?.focus();\n    }, 0);\n  };\n\n  const handleCompositionStart = () => {\n    isIMEComposingRef.current = true;\n  };\n\n  const handleCompositionEnd = () => {\n    setTimeout(() => {\n      isIMEComposingRef.current = false;\n    }, 0);\n  };\n\n  const isIMEInteraction = (event?: React.KeyboardEvent) => {\n    if (isIMEComposingRef.current) {\n      return true;\n    }\n\n    if (!event) {\n      return false;\n    }\n\n    const nativeEvent = event.nativeEvent;\n\n    if (nativeEvent.isComposing) {\n      return true;\n    }\n\n    const key = nativeEvent.key;\n    if (key === 'Process' || key === 'Unidentified') {\n      return true;\n    }\n\n    const keyboardEvent = nativeEvent as unknown as KeyboardEvent;\n    const keyCode = keyboardEvent.keyCode ?? (keyboardEvent as unknown as { which?: number }).which;\n    if (keyCode === 229) {\n      return true;\n    }\n\n    return false;\n  };\n\n  const handleSend = () => {\n    if (isIMEInteraction()) {\n      return;\n    }\n\n    if (prompt.trim() && !disabled) {\n      let finalPrompt = prompt.trim();\n\n      // Append thinking phrase if not auto mode\n      const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode);\n      if (thinkingMode && thinkingMode.phrase) {\n        finalPrompt = `${finalPrompt}.\\n\\n${thinkingMode.phrase}.`;\n      }\n\n      onSend(finalPrompt, selectedModel);\n      setPrompt(\"\");\n      setEmbeddedImages([]);\n      setTextareaHeight(48); // Reset height after sending\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (showFilePicker && e.key === 'Escape') {\n      e.preventDefault();\n      setShowFilePicker(false);\n      setFilePickerQuery(\"\");\n      return;\n    }\n\n    if (showSlashCommandPicker && e.key === 'Escape') {\n      e.preventDefault();\n      setShowSlashCommandPicker(false);\n      setSlashCommandQuery(\"\");\n      return;\n    }\n\n    // Add keyboard shortcut for expanding\n    if (e.key === 'e' && (e.ctrlKey || e.metaKey) && e.shiftKey) {\n      e.preventDefault();\n      setIsExpanded(true);\n      return;\n    }\n\n    if (\n      e.key === \"Enter\" &&\n      !e.shiftKey &&\n      !isExpanded &&\n      !showFilePicker &&\n      !showSlashCommandPicker\n    ) {\n      if (isIMEInteraction(e)) {\n        return;\n      }\n      e.preventDefault();\n      handleSend();\n    }\n  };\n\n  const handlePaste = async (e: React.ClipboardEvent) => {\n    const items = e.clipboardData?.items;\n    if (!items) return;\n\n    for (const item of items) {\n      if (item.type.startsWith('image/')) {\n        e.preventDefault();\n        \n        // Get the image blob\n        const blob = item.getAsFile();\n        if (!blob) continue;\n\n        try {\n          // Convert blob to base64\n          const reader = new FileReader();\n          reader.onload = () => {\n            const base64Data = reader.result as string;\n            \n            // Add the base64 data URL directly to the prompt\n            setPrompt(currentPrompt => {\n              // Use the data URL directly as the image reference\n              const mention = `@\"${base64Data}\"`;\n              const newPrompt = currentPrompt + (currentPrompt.endsWith(' ') || currentPrompt === '' ? '' : ' ') + mention + ' ';\n              \n              // Focus the textarea and move cursor to end\n              setTimeout(() => {\n                const target = isExpanded ? expandedTextareaRef.current : textareaRef.current;\n                target?.focus();\n                target?.setSelectionRange(newPrompt.length, newPrompt.length);\n              }, 0);\n\n              return newPrompt;\n            });\n          };\n          \n          reader.readAsDataURL(blob);\n        } catch (error) {\n          console.error('Failed to paste image:', error);\n        }\n      }\n    }\n  };\n\n  // Browser drag and drop handlers - just prevent default behavior\n  // Actual file handling is done via Tauri's window-level drag-drop events\n  const handleDrag = (e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    // Visual feedback is handled by Tauri events\n  };\n\n  const handleDrop = (e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    // File processing is handled by Tauri's onDragDropEvent\n  };\n\n  const handleRemoveImage = (index: number) => {\n    // Remove the corresponding @mention from the prompt\n    const imagePath = embeddedImages[index];\n    \n    // For data URLs, we need to handle them specially since they're always quoted\n    if (imagePath.startsWith('data:')) {\n      // Simply remove the exact quoted data URL\n      const quotedPath = `@\"${imagePath}\"`;\n      const newPrompt = prompt.replace(quotedPath, '').trim();\n      setPrompt(newPrompt);\n      return;\n    }\n    \n    // For file paths, use the original logic\n    const escapedPath = imagePath.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n    const escapedRelativePath = imagePath.replace(projectPath + '/', '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n    \n    // Create patterns for both quoted and unquoted mentions\n    const patterns = [\n      // Quoted full path\n      new RegExp(`@\"${escapedPath}\"\\\\s?`, 'g'),\n      // Unquoted full path\n      new RegExp(`@${escapedPath}\\\\s?`, 'g'),\n      // Quoted relative path\n      new RegExp(`@\"${escapedRelativePath}\"\\\\s?`, 'g'),\n      // Unquoted relative path\n      new RegExp(`@${escapedRelativePath}\\\\s?`, 'g')\n    ];\n\n    let newPrompt = prompt;\n    for (const pattern of patterns) {\n      newPrompt = newPrompt.replace(pattern, '');\n    }\n\n    setPrompt(newPrompt.trim());\n  };\n\n  const selectedModelData = MODELS.find(m => m.id === selectedModel) || MODELS[0];\n\n  return (\n    <TooltipProvider>\n    <>\n      {/* Expanded Modal */}\n      <AnimatePresence>\n        {isExpanded && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm\"\n            onClick={() => setIsExpanded(false)}\n          >\n            <motion.div\n              initial={{ opacity: 0, y: 8 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -8 }}\n              transition={{ duration: 0.15 }}\n              className=\"bg-background border border-border rounded-lg shadow-lg w-full max-w-2xl p-4 space-y-4\"\n              onClick={(e) => e.stopPropagation()}\n            >\n              <div className=\"flex items-center justify-between\">\n                <h3 className=\"text-sm font-medium\">Compose your prompt</h3>\n                <TooltipSimple content=\"Minimize\" side=\"bottom\">\n                  <motion.div\n                    whileTap={{ scale: 0.97 }}\n                    transition={{ duration: 0.15 }}\n                  >\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      onClick={() => setIsExpanded(false)}\n                      className=\"h-8 w-8\"\n                    >\n                      <Minimize2 className=\"h-4 w-4\" />\n                    </Button>\n                  </motion.div>\n                </TooltipSimple>\n              </div>\n\n              {/* Image previews in expanded mode */}\n              {embeddedImages.length > 0 && (\n                <ImagePreview\n                  images={embeddedImages}\n                  onRemove={handleRemoveImage}\n                  className=\"border-t border-border pt-2\"\n                />\n              )}\n\n              <Textarea\n                ref={expandedTextareaRef}\n                value={prompt}\n                onChange={handleTextChange}\n                onCompositionStart={handleCompositionStart}\n                onCompositionEnd={handleCompositionEnd}\n                onPaste={handlePaste}\n                placeholder=\"Type your message...\"\n                className=\"min-h-[200px] resize-none\"\n                disabled={disabled}\n                onDragEnter={handleDrag}\n                onDragLeave={handleDrag}\n                onDragOver={handleDrag}\n                onDrop={handleDrop}\n              />\n\n              <div className=\"flex items-center justify-between\">\n                <div className=\"flex items-center gap-4\">\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"text-xs text-muted-foreground\">Model:</span>\n                    <Popover\n                      trigger={\n                        <Button\n                          variant=\"outline\"\n                          size=\"sm\"\n                          onClick={() => setModelPickerOpen(!modelPickerOpen)}\n                          className=\"gap-2\"\n                        >\n                          <span className={selectedModelData.color}>\n                            {selectedModelData.icon}\n                          </span>\n                          {selectedModelData.name}\n                        </Button>\n                      }\n                      content={\n                        <div className=\"w-[300px] p-1\">\n                          {MODELS.map((model) => (\n                            <button\n                              key={model.id}\n                              onClick={() => {\n                                setSelectedModel(model.id);\n                                setModelPickerOpen(false);\n                              }}\n                              className={cn(\n                                \"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left\",\n                                \"hover:bg-accent\",\n                                selectedModel === model.id && \"bg-accent\"\n                              )}\n                            >\n                              <div className=\"mt-0.5\">\n                                <span className={model.color}>\n                                  {model.icon}\n                                </span>\n                              </div>\n                              <div className=\"flex-1 space-y-1\">\n                                <div className=\"font-medium text-sm\">{model.name}</div>\n                                <div className=\"text-xs text-muted-foreground\">\n                                  {model.description}\n                                </div>\n                              </div>\n                            </button>\n                          ))}\n                        </div>\n                      }\n                      open={modelPickerOpen}\n                      onOpenChange={setModelPickerOpen}\n                      align=\"start\"\n                      side=\"top\"\n                    />\n                  </div>\n\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"text-xs text-muted-foreground\">Thinking:</span>\n                    <Popover\n                      trigger={\n                        <Tooltip>\n                          <TooltipTrigger asChild>\n                            <Button\n                              variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => setThinkingModePickerOpen(!thinkingModePickerOpen)}\n                                className=\"gap-2\"\n                              >\n                                <span className={THINKING_MODES.find(m => m.id === selectedThinkingMode)?.color}>\n                                  {THINKING_MODES.find(m => m.id === selectedThinkingMode)?.icon}\n                                </span>\n                                <ThinkingModeIndicator \n                                  level={THINKING_MODES.find(m => m.id === selectedThinkingMode)?.level || 0} \n                                />\n                              </Button>\n                            </TooltipTrigger>\n                            <TooltipContent>\n                              <p className=\"font-medium\">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || \"Auto\"}</p>\n                              <p className=\"text-xs text-muted-foreground\">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}</p>\n                            </TooltipContent>\n                          </Tooltip>\n                      }\n                      content={\n                        <div className=\"w-[280px] p-1\">\n                          {THINKING_MODES.map((mode) => (\n                            <button\n                              key={mode.id}\n                              onClick={() => {\n                                setSelectedThinkingMode(mode.id);\n                                setThinkingModePickerOpen(false);\n                              }}\n                              className={cn(\n                                \"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left\",\n                                \"hover:bg-accent\",\n                                selectedThinkingMode === mode.id && \"bg-accent\"\n                              )}\n                            >\n                              <span className={cn(\"mt-0.5\", mode.color)}>\n                                {mode.icon}\n                              </span>\n                              <div className=\"flex-1 space-y-1\">\n                                <div className=\"font-medium text-sm\">\n                                  {mode.name}\n                                </div>\n                                <div className=\"text-xs text-muted-foreground\">\n                                  {mode.description}\n                                </div>\n                              </div>\n                              <ThinkingModeIndicator level={mode.level} />\n                            </button>\n                          ))}\n                        </div>\n                      }\n                      open={thinkingModePickerOpen}\n                      onOpenChange={setThinkingModePickerOpen}\n                      align=\"start\"\n                      side=\"top\"\n                    />\n                  </div>\n                </div>\n\n                <TooltipSimple content=\"Send message\" side=\"top\">\n                  <motion.div\n                    whileTap={{ scale: 0.97 }}\n                    transition={{ duration: 0.15 }}\n                  >\n                    <Button\n                      onClick={handleSend}\n                      disabled={!prompt.trim() || disabled}\n                      size=\"default\"\n                      className=\"min-w-[60px]\"\n                    >\n                      {isLoading ? (\n                        <div className=\"rotating-symbol text-primary-foreground\" />\n                      ) : (\n                        <Send className=\"h-4 w-4\" />\n                      )}\n                    </Button>\n                  </motion.div>\n                </TooltipSimple>\n              </div>\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      {/* Fixed Position Input Bar */}\n      <div\n        className={cn(\n          \"fixed bottom-0 left-0 right-0 z-40 bg-background/95 backdrop-blur-sm border-t border-border shadow-lg\",\n          dragActive && \"ring-2 ring-primary ring-offset-2\",\n          className\n        )}\n        onDragEnter={handleDrag}\n        onDragLeave={handleDrag}\n        onDragOver={handleDrag}\n        onDrop={handleDrop}\n      >\n        <div className=\"container mx-auto\">\n          {/* Image previews */}\n          {embeddedImages.length > 0 && (\n            <ImagePreview\n              images={embeddedImages}\n              onRemove={handleRemoveImage}\n              className=\"border-b border-border\"\n            />\n          )}\n\n          <div className=\"p-3\">\n            <div className=\"flex items-end gap-2\">\n              {/* Model & Thinking Mode Selectors - Left side, fixed at bottom */}\n              <div className=\"flex items-center gap-1 shrink-0 mb-1\">\n                <Popover\n                  trigger={\n                    <Tooltip>\n                      <TooltipTrigger asChild>\n                        <motion.div\n                          whileTap={{ scale: 0.97 }}\n                            transition={{ duration: 0.15 }}\n                          >\n                            <Button\n                              variant=\"ghost\"\n                              size=\"sm\"\n                              disabled={disabled}\n                              className=\"h-9 px-2 hover:bg-accent/50 gap-1\"\n                            >\n                              <span className={selectedModelData.color}>\n                                {selectedModelData.icon}\n                              </span>\n                              <span className=\"text-[10px] font-bold opacity-70\">\n                                {selectedModelData.shortName}\n                              </span>\n                              <ChevronUp className=\"h-3 w-3 ml-0.5 opacity-50\" />\n                            </Button>\n                          </motion.div>\n                        </TooltipTrigger>\n                        <TooltipContent side=\"top\">\n                          <p className=\"text-xs font-medium\">{selectedModelData.name}</p>\n                          <p className=\"text-xs text-muted-foreground\">{selectedModelData.description}</p>\n                        </TooltipContent>\n                      </Tooltip>\n                  }\n                content={\n                  <div className=\"w-[300px] p-1\">\n                    {MODELS.map((model) => (\n                      <button\n                        key={model.id}\n                        onClick={() => {\n                          setSelectedModel(model.id);\n                          setModelPickerOpen(false);\n                        }}\n                        className={cn(\n                          \"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left\",\n                          \"hover:bg-accent\",\n                          selectedModel === model.id && \"bg-accent\"\n                        )}\n                      >\n                        <div className=\"mt-0.5\">\n                          <span className={model.color}>\n                            {model.icon}\n                          </span>\n                        </div>\n                        <div className=\"flex-1 space-y-1\">\n                          <div className=\"font-medium text-sm\">{model.name}</div>\n                          <div className=\"text-xs text-muted-foreground\">\n                            {model.description}\n                          </div>\n                        </div>\n                      </button>\n                    ))}\n                  </div>\n                }\n                open={modelPickerOpen}\n                onOpenChange={setModelPickerOpen}\n                align=\"start\"\n                side=\"top\"\n              />\n\n                <Popover\n                  trigger={\n                    <Tooltip>\n                      <TooltipTrigger asChild>\n                        <motion.div\n                          whileTap={{ scale: 0.97 }}\n                            transition={{ duration: 0.15 }}\n                          >\n                            <Button\n                              variant=\"ghost\"\n                              size=\"sm\"\n                              disabled={disabled}\n                              className=\"h-9 px-2 hover:bg-accent/50 gap-1\"\n                            >\n                              <span className={THINKING_MODES.find(m => m.id === selectedThinkingMode)?.color}>\n                                {THINKING_MODES.find(m => m.id === selectedThinkingMode)?.icon}\n                              </span>\n                              <span className=\"text-[10px] font-semibold opacity-70\">\n                                {THINKING_MODES.find(m => m.id === selectedThinkingMode)?.shortName}\n                              </span>\n                              <ChevronUp className=\"h-3 w-3 ml-0.5 opacity-50\" />\n                            </Button>\n                          </motion.div>\n                        </TooltipTrigger>\n                        <TooltipContent side=\"top\">\n                          <p className=\"text-xs font-medium\">Thinking: {THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || \"Auto\"}</p>\n                          <p className=\"text-xs text-muted-foreground\">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}</p>\n                        </TooltipContent>\n                      </Tooltip>\n                  }\n                content={\n                  <div className=\"w-[280px] p-1\">\n                    {THINKING_MODES.map((mode) => (\n                      <button\n                        key={mode.id}\n                        onClick={() => {\n                          setSelectedThinkingMode(mode.id);\n                          setThinkingModePickerOpen(false);\n                        }}\n                        className={cn(\n                          \"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left\",\n                          \"hover:bg-accent\",\n                          selectedThinkingMode === mode.id && \"bg-accent\"\n                        )}\n                      >\n                        <span className={cn(\"mt-0.5\", mode.color)}>\n                          {mode.icon}\n                        </span>\n                        <div className=\"flex-1 space-y-1\">\n                          <div className=\"font-medium text-sm\">\n                            {mode.name}\n                          </div>\n                          <div className=\"text-xs text-muted-foreground\">\n                            {mode.description}\n                          </div>\n                        </div>\n                        <ThinkingModeIndicator level={mode.level} />\n                      </button>\n                    ))}\n                  </div>\n                }\n                open={thinkingModePickerOpen}\n                onOpenChange={setThinkingModePickerOpen}\n                align=\"start\"\n                side=\"top\"\n              />\n\n              </div>\n\n              {/* Prompt Input - Center */}\n              <div className=\"flex-1 relative\">\n                <Textarea\n                  ref={textareaRef}\n                  value={prompt}\n                  onChange={handleTextChange}\n                  onKeyDown={handleKeyDown}\n                  onCompositionStart={handleCompositionStart}\n                  onCompositionEnd={handleCompositionEnd}\n                  onPaste={handlePaste}\n                  placeholder={\n                    dragActive\n                      ? \"Drop images here...\"\n                      : \"Message Claude (@ for files, / for commands)...\"\n                  }\n                  disabled={disabled}\n                  className={cn(\n                    \"resize-none pr-20 pl-3 py-2.5 transition-all duration-150\",\n                    dragActive && \"border-primary\",\n                    textareaHeight >= 240 && \"overflow-y-auto scrollbar-thin\"\n                  )}\n                  style={{\n                    height: `${textareaHeight}px`,\n                    overflowY: textareaHeight >= 240 ? 'auto' : 'hidden'\n                  }}\n                />\n\n                {/* Action buttons inside input - fixed at bottom right */}\n                <div className=\"absolute right-1.5 bottom-1.5 flex items-center gap-0.5\">\n                  <TooltipSimple content=\"Expand (Ctrl+Shift+E)\" side=\"top\">\n                    <motion.div\n                      whileTap={{ scale: 0.97 }}\n                      transition={{ duration: 0.15 }}\n                    >\n                      <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        onClick={() => setIsExpanded(true)}\n                        disabled={disabled}\n                        className=\"h-8 w-8 hover:bg-accent/50 transition-colors\"\n                      >\n                        <Maximize2 className=\"h-3.5 w-3.5\" />\n                      </Button>\n                    </motion.div>\n                  </TooltipSimple>\n\n                  <TooltipSimple content={isLoading ? \"Stop generation\" : \"Send message (Enter)\"} side=\"top\">\n                    <motion.div\n                      whileTap={{ scale: 0.97 }}\n                      transition={{ duration: 0.15 }}\n                    >\n                      <Button\n                        onClick={isLoading ? onCancel : handleSend}\n                        disabled={isLoading ? false : (!prompt.trim() || disabled)}\n                        variant={isLoading ? \"destructive\" : prompt.trim() ? \"default\" : \"ghost\"}\n                        size=\"icon\"\n                        className={cn(\n                          \"h-8 w-8 transition-all\",\n                          prompt.trim() && !isLoading && \"shadow-sm\"\n                        )}\n                      >\n                        {isLoading ? (\n                          <Square className=\"h-4 w-4\" />\n                        ) : (\n                          <Send className=\"h-4 w-4\" />\n                        )}\n                      </Button>\n                    </motion.div>\n                  </TooltipSimple>\n                </div>\n\n                {/* File Picker */}\n                <AnimatePresence>\n                  {showFilePicker && projectPath && projectPath.trim() && (\n                    <FilePicker\n                      basePath={projectPath.trim()}\n                      onSelect={handleFileSelect}\n                      onClose={handleFilePickerClose}\n                      initialQuery={filePickerQuery}\n                    />\n                  )}\n                </AnimatePresence>\n\n                {/* Slash Command Picker */}\n                <AnimatePresence>\n                  {showSlashCommandPicker && (\n                    <SlashCommandPicker\n                      projectPath={projectPath}\n                      onSelect={handleSlashCommandSelect}\n                      onClose={handleSlashCommandPickerClose}\n                      initialQuery={slashCommandQuery}\n                    />\n                  )}\n                </AnimatePresence>\n              </div>\n\n              {/* Extra menu items - Right side, fixed at bottom */}\n              {extraMenuItems && (\n                <div className=\"flex items-center gap-0.5 shrink-0 mb-1\">\n                  {extraMenuItems}\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n    </TooltipProvider>\n  );\n};\n\nexport const FloatingPromptInput = React.forwardRef<\n  FloatingPromptInputRef,\n  FloatingPromptInputProps\n>(FloatingPromptInputInner);\n\nFloatingPromptInput.displayName = 'FloatingPromptInput';\n"
  },
  {
    "path": "src/components/GitHubAgentBrowser.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport {\n  Search,\n  Download,\n  Loader2,\n  AlertCircle,\n  Eye,\n  Check,\n  Globe,\n  FileJson,\n} from \"lucide-react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { api, type GitHubAgentFile, type AgentExport, type Agent } from \"@/lib/api\";\nimport { type AgentIconName } from \"./CCAgents\";\nimport { ICON_MAP } from \"./IconPicker\";\nimport { open } from \"@tauri-apps/plugin-shell\";\n\ninterface GitHubAgentBrowserProps {\n  isOpen: boolean;\n  onClose: () => void;\n  onImportSuccess: () => void;\n}\n\ninterface AgentPreview {\n  file: GitHubAgentFile;\n  data: AgentExport | null;\n  loading: boolean;\n  error: string | null;\n}\n\nexport const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({\n  isOpen,\n  onClose,\n  onImportSuccess,\n}) => {\n  const [agents, setAgents] = useState<GitHubAgentFile[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [selectedAgent, setSelectedAgent] = useState<AgentPreview | null>(null);\n  const [importing, setImporting] = useState(false);\n  const [existingAgents, setExistingAgents] = useState<Agent[]>([]);\n\n  useEffect(() => {\n    if (isOpen) {\n      fetchAgents();\n      fetchExistingAgents();\n    }\n  }, [isOpen]);\n\n  const fetchExistingAgents = async () => {\n    try {\n      const agents = await api.listAgents();\n      setExistingAgents(agents);\n    } catch (err) {\n      console.error(\"Failed to fetch existing agents:\", err);\n    }\n  };\n\n  const fetchAgents = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const agentFiles = await api.fetchGitHubAgents();\n      setAgents(agentFiles);\n    } catch (err) {\n      console.error(\"Failed to fetch GitHub agents:\", err);\n      setError(\"Failed to fetch agents from GitHub. Please check your internet connection.\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handlePreviewAgent = async (file: GitHubAgentFile) => {\n    setSelectedAgent({\n      file,\n      data: null,\n      loading: true,\n      error: null,\n    });\n\n    try {\n      const agentData = await api.fetchGitHubAgentContent(file.download_url);\n      setSelectedAgent({\n        file,\n        data: agentData,\n        loading: false,\n        error: null,\n      });\n    } catch (err) {\n      console.error(\"Failed to fetch agent content:\", err);\n      setSelectedAgent({\n        file,\n        data: null,\n        loading: false,\n        error: \"Failed to load agent details\",\n      });\n    }\n  };\n\n  const isAgentImported = (fileName: string) => {\n    const agentName = getAgentDisplayName(fileName);\n    return existingAgents.some(agent => \n      agent.name.toLowerCase() === agentName.toLowerCase()\n    );\n  };\n\n  const handleImportAgent = async () => {\n    if (!selectedAgent?.file) return;\n\n    try {\n      setImporting(true);\n      await api.importAgentFromGitHub(selectedAgent.file.download_url);\n      \n      // Refresh existing agents list\n      await fetchExistingAgents();\n      \n      // Close preview\n      setSelectedAgent(null);\n      \n      // Notify parent\n      onImportSuccess();\n    } catch (err) {\n      console.error(\"Failed to import agent:\", err);\n      alert(`Failed to import agent: ${err instanceof Error ? err.message : \"Unknown error\"}`);\n    } finally {\n      setImporting(false);\n    }\n  };\n\n  const filteredAgents = agents.filter(agent =>\n    agent.name.toLowerCase().includes(searchQuery.toLowerCase())\n  );\n\n  const getAgentDisplayName = (fileName: string) => {\n    return fileName.replace(\".opcode.json\", \"\").replace(/-/g, \" \")\n      .split(\" \")\n      .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n      .join(\" \");\n  };\n\n  const renderIcon = (iconName: string) => {\n    const Icon = ICON_MAP[iconName as AgentIconName] || ICON_MAP.bot;\n    return <Icon className=\"h-8 w-8\" />;\n  };\n\n  const handleGitHubLinkClick = async (e: React.MouseEvent) => {\n    e.preventDefault();\n    try {\n      await open(\"https://github.com/getAsterisk/opcode/tree/main/cc_agents\");\n    } catch (error) {\n      console.error('Failed to open GitHub link:', error);\n    }\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent className=\"max-w-4xl max-h-[80vh] overflow-hidden flex flex-col\">\n        <DialogHeader>\n          <DialogTitle className=\"flex items-center gap-2\">\n            <Globe className=\"h-5 w-5\" />\n            Import Agent from GitHub\n          </DialogTitle>\n        </DialogHeader>\n\n        <div className=\"flex-1 overflow-hidden flex flex-col\">\n          {/* Repository Info */}\n          <div className=\"px-4 py-3 bg-muted/50 rounded-lg mb-4\">\n            <p className=\"text-sm text-muted-foreground\">\n              Agents are fetched from{\" \"}\n              <button\n                onClick={handleGitHubLinkClick}\n                className=\"text-primary hover:underline inline-flex items-center gap-1\"\n              >\n                github.com/getAsterisk/opcode/cc_agents\n                <Globe className=\"h-3 w-3\" />\n              </button>\n            </p>\n            <p className=\"text-sm text-muted-foreground mt-1\">\n              You can contribute your custom agents to the repository!\n            </p>\n          </div>\n\n          {/* Search Bar */}\n          <div className=\"mb-4\">\n            <div className=\"relative\">\n              <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n              <Input\n                placeholder=\"Search agents...\"\n                value={searchQuery}\n                onChange={(e) => setSearchQuery(e.target.value)}\n                className=\"pl-10\"\n              />\n            </div>\n          </div>\n\n          {/* Content */}\n          <div className=\"flex-1 overflow-y-auto\">\n            {loading ? (\n              <div className=\"flex items-center justify-center h-64\">\n                <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n              </div>\n            ) : error ? (\n              <div className=\"flex flex-col items-center justify-center h-64 text-center\">\n                <AlertCircle className=\"h-12 w-12 text-destructive mb-4\" />\n                <p className=\"text-sm text-muted-foreground mb-4\">{error}</p>\n                <Button onClick={fetchAgents} variant=\"outline\" size=\"sm\">\n                  Try Again\n                </Button>\n              </div>\n            ) : filteredAgents.length === 0 ? (\n              <div className=\"flex flex-col items-center justify-center h-64 text-center\">\n                <FileJson className=\"h-12 w-12 text-muted-foreground mb-4\" />\n                <p className=\"text-sm text-muted-foreground\">\n                  {searchQuery ? \"No agents found matching your search\" : \"No agents available\"}\n                </p>\n              </div>\n            ) : (\n              <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pb-4\">\n                <AnimatePresence mode=\"popLayout\">\n                  {filteredAgents.map((agent, index) => (\n                    <motion.div\n                      key={agent.sha}\n                      initial={{ opacity: 0, scale: 0.9 }}\n                      animate={{ opacity: 1, scale: 1 }}\n                      exit={{ opacity: 0, scale: 0.9 }}\n                      transition={{ duration: 0.2, delay: index * 0.05 }}\n                    >\n                      <Card className=\"h-full hover:shadow-lg transition-shadow cursor-pointer\"\n                            onClick={() => handlePreviewAgent(agent)}>\n                        <CardContent className=\"p-4\">\n                          <div className=\"flex items-start justify-between mb-3\">\n                            <div className=\"flex items-center gap-3 flex-1\">\n                              <div className=\"p-2 rounded-lg bg-primary/10 text-primary flex-shrink-0\">\n                                {/* Default to bot icon for now, will be loaded from preview */}\n                                {(() => {\n                                  const Icon = ICON_MAP.bot;\n                                  return <Icon className=\"h-6 w-6\" />;\n                                })()}\n                              </div>\n                              <h3 className=\"text-sm font-semibold line-clamp-2\">\n                                {getAgentDisplayName(agent.name)}\n                              </h3>\n                            </div>\n                            {isAgentImported(agent.name) && (\n                              <Badge variant=\"secondary\" className=\"ml-2 flex-shrink-0\">\n                                <Check className=\"h-3 w-3 mr-1\" />\n                                Imported\n                              </Badge>\n                            )}\n                          </div>\n                          <p className=\"text-xs text-muted-foreground\">\n                            {(agent.size / 1024).toFixed(1)} KB\n                          </p>\n                        </CardContent>\n                        <CardFooter className=\"p-4 pt-0\">\n                          <Button\n                            size=\"sm\"\n                            variant=\"outline\"\n                            className=\"w-full\"\n                            onClick={(e) => {\n                              e.stopPropagation();\n                              handlePreviewAgent(agent);\n                            }}\n                          >\n                            <Eye className=\"h-3 w-3 mr-2\" />\n                            Preview\n                          </Button>\n                        </CardFooter>\n                      </Card>\n                    </motion.div>\n                  ))}\n                </AnimatePresence>\n              </div>\n            )}\n          </div>\n        </div>\n      </DialogContent>\n\n      {/* Agent Preview Dialog */}\n      <AnimatePresence>\n        {selectedAgent && (\n          <Dialog open={!!selectedAgent} onOpenChange={() => setSelectedAgent(null)}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-hidden flex flex-col\">\n              <DialogHeader>\n                <DialogTitle>Agent Preview</DialogTitle>\n              </DialogHeader>\n\n              <div className=\"flex-1 overflow-y-auto\">\n                {selectedAgent.loading ? (\n                  <div className=\"flex items-center justify-center h-64\">\n                    <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n                  </div>\n                ) : selectedAgent.error ? (\n                  <div className=\"flex flex-col items-center justify-center h-64 text-center\">\n                    <AlertCircle className=\"h-12 w-12 text-destructive mb-4\" />\n                    <p className=\"text-sm text-muted-foreground\">{selectedAgent.error}</p>\n                  </div>\n                ) : selectedAgent.data ? (\n                  <div className=\"space-y-4\">\n                    {/* Agent Info */}\n                    <div className=\"flex items-start gap-4\">\n                      <div className=\"p-3 rounded-lg bg-primary/10 text-primary\">\n                        {renderIcon(selectedAgent.data.agent.icon)}\n                      </div>\n                      <div className=\"flex-1\">\n                        <h3 className=\"text-lg font-semibold\">\n                          {selectedAgent.data.agent.name}\n                        </h3>\n                        <div className=\"flex items-center gap-2 mt-1\">\n                          <Badge variant=\"outline\">{selectedAgent.data.agent.model}</Badge>\n                        </div>\n                      </div>\n                    </div>\n\n                    {/* System Prompt */}\n                    <div>\n                      <h4 className=\"text-sm font-medium mb-2\">System Prompt</h4>\n                      <div className=\"bg-muted rounded-lg p-3 max-h-48 overflow-y-auto\">\n                        <pre className=\"text-xs whitespace-pre-wrap font-mono\">\n                          {selectedAgent.data.agent.system_prompt}\n                        </pre>\n                      </div>\n                    </div>\n\n                    {/* Default Task */}\n                    {selectedAgent.data.agent.default_task && (\n                      <div>\n                        <h4 className=\"text-sm font-medium mb-2\">Default Task</h4>\n                        <div className=\"bg-muted rounded-lg p-3\">\n                          <p className=\"text-sm\">{selectedAgent.data.agent.default_task}</p>\n                        </div>\n                      </div>\n                    )}\n\n\n\n                    {/* Metadata */}\n                    <div className=\"text-xs text-muted-foreground\">\n                      <p>Version: {selectedAgent.data.version}</p>\n                      <p>Exported: {new Date(selectedAgent.data.exported_at).toLocaleDateString()}</p>\n                    </div>\n                  </div>\n                ) : null}\n              </div>\n\n              {/* Actions */}\n              {selectedAgent.data && (\n                <div className=\"flex justify-end gap-2 mt-4 pt-4 border-t\">\n                  <Button\n                    variant=\"outline\"\n                    onClick={() => setSelectedAgent(null)}\n                  >\n                    Cancel\n                  </Button>\n                  <Button\n                    onClick={handleImportAgent}\n                    disabled={importing || isAgentImported(selectedAgent.file.name)}\n                  >\n                    {importing ? (\n                      <>\n                        <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                        Importing...\n                      </>\n                    ) : isAgentImported(selectedAgent.file.name) ? (\n                      <>\n                        <Check className=\"h-4 w-4 mr-2\" />\n                        Already Imported\n                      </>\n                    ) : (\n                      <>\n                        <Download className=\"h-4 w-4 mr-2\" />\n                        Import Agent\n                      </>\n                    )}\n                  </Button>\n                </div>\n              )}\n            </DialogContent>\n          </Dialog>\n        )}\n      </AnimatePresence>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "src/components/HooksEditor.tsx",
    "content": "/**\n * HooksEditor component for managing Claude Code hooks configuration\n */\n\nimport React, { useState, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { \n  Plus, \n  Trash2, \n  AlertTriangle, \n  Code2,\n  Terminal,\n  FileText,\n  ChevronRight,\n  ChevronDown,\n  Clock,\n  Zap,\n  Shield,\n  PlayCircle,\n  Info,\n  Save,\n  Loader2\n} from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Card } from '@/components/ui/card';\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Badge } from '@/components/ui/badge';\nimport { \n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport { cn } from '@/lib/utils';\nimport { HooksManager } from '@/lib/hooksManager';\nimport { api } from '@/lib/api';\nimport {\n  HooksConfiguration,\n  HookEvent,\n  HookMatcher,\n  HookCommand,\n  HookTemplate,\n  COMMON_TOOL_MATCHERS,\n  HOOK_TEMPLATES,\n} from '@/types/hooks';\n\ninterface HooksEditorProps {\n  projectPath?: string;\n  scope: 'project' | 'local' | 'user';\n  readOnly?: boolean;\n  className?: string;\n  onChange?: (hasChanges: boolean, getHooks: () => HooksConfiguration) => void;\n  hideActions?: boolean;\n}\n\ninterface EditableHookCommand extends HookCommand {\n  id: string;\n}\n\ninterface EditableHookMatcher extends Omit<HookMatcher, 'hooks'> {\n  id: string;\n  hooks: EditableHookCommand[];\n  expanded?: boolean;\n}\n\nconst EVENT_INFO: Record<HookEvent, { label: string; description: string; icon: React.ReactNode }> = {\n  PreToolUse: {\n    label: 'Pre Tool Use',\n    description: 'Runs before tool calls, can block and provide feedback',\n    icon: <Shield className=\"h-4 w-4\" />\n  },\n  PostToolUse: {\n    label: 'Post Tool Use',\n    description: 'Runs after successful tool completion',\n    icon: <PlayCircle className=\"h-4 w-4\" />\n  },\n  Notification: {\n    label: 'Notification',\n    description: 'Customizes notifications when Claude needs attention',\n    icon: <Zap className=\"h-4 w-4\" />\n  },\n  Stop: {\n    label: 'Stop',\n    description: 'Runs when Claude finishes responding',\n    icon: <Code2 className=\"h-4 w-4\" />\n  },\n  SubagentStop: {\n    label: 'Subagent Stop',\n    description: 'Runs when a Claude subagent (Task) finishes',\n    icon: <Terminal className=\"h-4 w-4\" />\n  }\n};\n\nexport const HooksEditor: React.FC<HooksEditorProps> = ({\n  projectPath,\n  scope,\n  readOnly = false,\n  className,\n  onChange,\n  hideActions = false\n}) => {\n  const [selectedEvent, setSelectedEvent] = useState<HookEvent>('PreToolUse');\n  const [showTemplateDialog, setShowTemplateDialog] = useState(false);\n  const [validationErrors, setValidationErrors] = useState<string[]>([]);\n  const [validationWarnings, setValidationWarnings] = useState<string[]>([]);\n  const isInitialMount = React.useRef(true);\n  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n  const [isSaving, setIsSaving] = useState(false);\n  const [isLoading, setIsLoading] = useState(false);\n  const [loadError, setLoadError] = useState<string | null>(null);\n  const [hooks, setHooks] = useState<HooksConfiguration>({});\n  \n  // Events with matchers (tool-related)\n  const matcherEvents = ['PreToolUse', 'PostToolUse'] as const;\n  // Events without matchers (non-tool-related)\n  const directEvents = ['Notification', 'Stop', 'SubagentStop'] as const;\n  \n  // Convert hooks to editable format with IDs\n  const [editableHooks, setEditableHooks] = useState<{\n    PreToolUse: EditableHookMatcher[];\n    PostToolUse: EditableHookMatcher[];\n    Notification: EditableHookCommand[];\n    Stop: EditableHookCommand[];\n    SubagentStop: EditableHookCommand[];\n  }>(() => {\n    const result = {\n      PreToolUse: [],\n      PostToolUse: [],\n      Notification: [],\n      Stop: [],\n      SubagentStop: []\n    } as any;\n    \n    // Initialize matcher events\n    matcherEvents.forEach(event => {\n      const matchers = hooks?.[event] as HookMatcher[] | undefined;\n      if (matchers && Array.isArray(matchers)) {\n        result[event] = matchers.map(matcher => ({\n          ...matcher,\n          id: HooksManager.generateId(),\n          expanded: false,\n          hooks: (matcher.hooks || []).map(hook => ({\n            ...hook,\n            id: HooksManager.generateId()\n          }))\n        }));\n      }\n    });\n    \n    // Initialize direct events\n    directEvents.forEach(event => {\n      const commands = hooks?.[event] as HookCommand[] | undefined;\n      if (commands && Array.isArray(commands)) {\n        result[event] = commands.map(hook => ({\n          ...hook,\n          id: HooksManager.generateId()\n        }));\n      }\n    });\n    \n    return result;\n  });\n\n  // Load hooks when projectPath or scope changes\n  useEffect(() => {\n    // For user scope, we don't need a projectPath\n    if (scope === 'user' || projectPath) {\n      setIsLoading(true);\n      setLoadError(null);\n      \n      api.getHooksConfig(scope, projectPath)\n        .then((config) => {\n          setHooks(config || {});\n          setHasUnsavedChanges(false);\n        })\n        .catch((err) => {\n          console.error(\"Failed to load hooks configuration:\", err);\n          setLoadError(err instanceof Error ? err.message : \"Failed to load hooks configuration\");\n          setHooks({});\n        })\n        .finally(() => {\n          setIsLoading(false);\n        });\n    } else {\n      // No projectPath for project/local scopes\n      setHooks({});\n    }\n  }, [projectPath, scope]);\n\n  // Reset initial mount flag when hooks prop changes\n  useEffect(() => {\n    isInitialMount.current = true;\n    setHasUnsavedChanges(false); // Reset unsaved changes when hooks prop changes\n    \n    // Reinitialize editable hooks when hooks prop changes\n    const result = {\n      PreToolUse: [],\n      PostToolUse: [],\n      Notification: [],\n      Stop: [],\n      SubagentStop: []\n    } as any;\n    \n    // Initialize matcher events\n    matcherEvents.forEach(event => {\n      const matchers = hooks?.[event] as HookMatcher[] | undefined;\n      if (matchers && Array.isArray(matchers)) {\n        result[event] = matchers.map(matcher => ({\n          ...matcher,\n          id: HooksManager.generateId(),\n          expanded: false,\n          hooks: (matcher.hooks || []).map(hook => ({\n            ...hook,\n            id: HooksManager.generateId()\n          }))\n        }));\n      }\n    });\n    \n    // Initialize direct events\n    directEvents.forEach(event => {\n      const commands = hooks?.[event] as HookCommand[] | undefined;\n      if (commands && Array.isArray(commands)) {\n        result[event] = commands.map(hook => ({\n          ...hook,\n          id: HooksManager.generateId()\n        }));\n      }\n    });\n    \n    setEditableHooks(result);\n  }, [hooks]);\n\n  // Track changes when editable hooks change (but don't save automatically)\n  useEffect(() => {\n    if (isInitialMount.current) {\n      isInitialMount.current = false;\n      return;\n    }\n    \n    setHasUnsavedChanges(true);\n  }, [editableHooks]);\n\n  // Notify parent of changes\n  useEffect(() => {\n    if (onChange) {\n      const getHooks = () => {\n        const newHooks: HooksConfiguration = {};\n        \n        // Handle matcher events\n        matcherEvents.forEach(event => {\n          const matchers = editableHooks[event];\n          if (matchers.length > 0) {\n            newHooks[event] = matchers.map(({ id, expanded, ...matcher }) => ({\n              ...matcher,\n              hooks: matcher.hooks.map(({ id, ...hook }) => hook)\n            }));\n          }\n        });\n        \n        // Handle direct events\n        directEvents.forEach(event => {\n          const commands = editableHooks[event];\n          if (commands.length > 0) {\n            newHooks[event] = commands.map(({ id, ...hook }) => hook);\n          }\n        });\n        \n        return newHooks;\n      };\n      \n      onChange(hasUnsavedChanges, getHooks);\n    }\n  }, [hasUnsavedChanges, editableHooks, onChange]);\n\n  // Save function to be called explicitly\n  const handleSave = async () => {\n    if (scope !== 'user' && !projectPath) return;\n    \n    setIsSaving(true);\n    \n    const newHooks: HooksConfiguration = {};\n    \n    // Handle matcher events\n    matcherEvents.forEach(event => {\n      const matchers = editableHooks[event];\n      if (matchers.length > 0) {\n        newHooks[event] = matchers.map(({ id, expanded, ...matcher }) => ({\n          ...matcher,\n          hooks: matcher.hooks.map(({ id, ...hook }) => hook)\n        }));\n      }\n    });\n    \n    // Handle direct events\n    directEvents.forEach(event => {\n      const commands = editableHooks[event];\n      if (commands.length > 0) {\n        newHooks[event] = commands.map(({ id, ...hook }) => hook);\n      }\n    });\n    \n    try {\n      await api.updateHooksConfig(scope, newHooks, projectPath);\n      setHooks(newHooks);\n      setHasUnsavedChanges(false);\n    } catch (error) {\n      console.error('Failed to save hooks:', error);\n      setLoadError(error instanceof Error ? error.message : 'Failed to save hooks');\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const addMatcher = (event: HookEvent) => {\n    // Only for events with matchers\n    if (!matcherEvents.includes(event as any)) return;\n    \n    const newMatcher: EditableHookMatcher = {\n      id: HooksManager.generateId(),\n      matcher: '',\n      hooks: [],\n      expanded: true\n    };\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: [...(prev[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]), newMatcher]\n    }));\n  };\n  \n  const addDirectCommand = (event: HookEvent) => {\n    // Only for events without matchers\n    if (!directEvents.includes(event as any)) return;\n    \n    const newCommand: EditableHookCommand = {\n      id: HooksManager.generateId(),\n      type: 'command',\n      command: ''\n    };\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: [...(prev[event as 'Notification' | 'Stop' | 'SubagentStop'] as EditableHookCommand[]), newCommand]\n    }));\n  };\n\n  const updateMatcher = (event: HookEvent, matcherId: string, updates: Partial<EditableHookMatcher>) => {\n    if (!matcherEvents.includes(event as any)) return;\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: (prev[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]).map(matcher =>\n        matcher.id === matcherId ? { ...matcher, ...updates } : matcher\n      )\n    }));\n  };\n\n  const removeMatcher = (event: HookEvent, matcherId: string) => {\n    if (!matcherEvents.includes(event as any)) return;\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: (prev[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]).filter(matcher => matcher.id !== matcherId)\n    }));\n  };\n  \n  const updateDirectCommand = (event: HookEvent, commandId: string, updates: Partial<EditableHookCommand>) => {\n    if (!directEvents.includes(event as any)) return;\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: (prev[event as 'Notification' | 'Stop' | 'SubagentStop'] as EditableHookCommand[]).map(cmd =>\n        cmd.id === commandId ? { ...cmd, ...updates } : cmd\n      )\n    }));\n  };\n  \n  const removeDirectCommand = (event: HookEvent, commandId: string) => {\n    if (!directEvents.includes(event as any)) return;\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: (prev[event as 'Notification' | 'Stop' | 'SubagentStop'] as EditableHookCommand[]).filter(cmd => cmd.id !== commandId)\n    }));\n  };\n\n  const applyTemplate = (template: HookTemplate) => {\n    if (matcherEvents.includes(template.event as any)) {\n      // For events with matchers\n      const newMatcher: EditableHookMatcher = {\n        id: HooksManager.generateId(),\n        matcher: template.matcher,\n        hooks: template.commands.map(cmd => ({\n          id: HooksManager.generateId(),\n          type: 'command' as const,\n          command: cmd\n        })),\n        expanded: true\n      };\n      \n      setEditableHooks(prev => ({\n        ...prev,\n        [template.event]: [...(prev[template.event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]), newMatcher]\n      }));\n    } else {\n      // For direct events\n      const newCommands: EditableHookCommand[] = template.commands.map(cmd => ({\n        id: HooksManager.generateId(),\n        type: 'command' as const,\n        command: cmd\n      }));\n      \n      setEditableHooks(prev => ({\n        ...prev,\n        [template.event]: [...(prev[template.event as 'Notification' | 'Stop' | 'SubagentStop'] as EditableHookCommand[]), ...newCommands]\n      }));\n    }\n    \n    setSelectedEvent(template.event);\n    setShowTemplateDialog(false);\n  };\n\n  const validateHooks = async () => {\n    if (!hooks) {\n      setValidationErrors([]);\n      setValidationWarnings([]);\n      return;\n    }\n    \n    const result = await HooksManager.validateConfig(hooks);\n    setValidationErrors(result.errors.map(e => e.message));\n    setValidationWarnings(result.warnings.map(w => `${w.message} in command: ${(w.command || '').substring(0, 50)}...`));\n  };\n\n  useEffect(() => {\n    validateHooks();\n  }, [hooks]);\n\n  const addCommand = (event: HookEvent, matcherId: string) => {\n    if (!matcherEvents.includes(event as any)) return;\n    \n    const newCommand: EditableHookCommand = {\n      id: HooksManager.generateId(),\n      type: 'command',\n      command: ''\n    };\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: (prev[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]).map(matcher =>\n        matcher.id === matcherId\n          ? { ...matcher, hooks: [...matcher.hooks, newCommand] }\n          : matcher\n      )\n    }));\n  };\n\n  const updateCommand = (\n    event: HookEvent,\n    matcherId: string,\n    commandId: string,\n    updates: Partial<EditableHookCommand>\n  ) => {\n    if (!matcherEvents.includes(event as any)) return;\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: (prev[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]).map(matcher =>\n        matcher.id === matcherId\n          ? {\n              ...matcher,\n              hooks: matcher.hooks.map(cmd =>\n                cmd.id === commandId ? { ...cmd, ...updates } : cmd\n              )\n            }\n          : matcher\n      )\n    }));\n  };\n\n  const removeCommand = (event: HookEvent, matcherId: string, commandId: string) => {\n    if (!matcherEvents.includes(event as any)) return;\n    \n    setEditableHooks(prev => ({\n      ...prev,\n      [event]: (prev[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]).map(matcher =>\n        matcher.id === matcherId\n          ? { ...matcher, hooks: matcher.hooks.filter(cmd => cmd.id !== commandId) }\n          : matcher\n      )\n    }));\n  };\n\n  const renderMatcher = (event: HookEvent, matcher: EditableHookMatcher) => (\n    <Card key={matcher.id} className=\"p-4 space-y-4\">\n      <div className=\"flex items-start gap-4\">\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"p-0 h-6 w-6\"\n          onClick={() => updateMatcher(event, matcher.id, { expanded: !matcher.expanded })}\n        >\n          {matcher.expanded ? <ChevronDown className=\"h-4 w-4\" /> : <ChevronRight className=\"h-4 w-4\" />}\n        </Button>\n        \n        <div className=\"flex-1 space-y-2\">\n          <div className=\"flex items-center gap-2\">\n            <Label htmlFor={`matcher-${matcher.id}`}>Pattern</Label>\n            <TooltipProvider>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <Info className=\"h-3 w-3 text-muted-foreground\" />\n                </TooltipTrigger>\n                <TooltipContent>\n                  <p>Tool name pattern (regex supported). Leave empty to match all tools.</p>\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          </div>\n          \n          <div className=\"flex items-center gap-2\">\n            <Input\n              id={`matcher-${matcher.id}`}\n              placeholder=\"e.g., Bash, Edit|Write, mcp__.*\"\n              value={matcher.matcher || ''}\n              onChange={(e) => updateMatcher(event, matcher.id, { matcher: e.target.value })}\n              disabled={readOnly}\n              className=\"flex-1\"\n            />\n            \n            <Select\n              value={matcher.matcher || 'custom'}\n              onValueChange={(value) => {\n                if (value !== 'custom') {\n                  updateMatcher(event, matcher.id, { matcher: value });\n                }\n              }}\n              disabled={readOnly}\n            >\n              <SelectTrigger className=\"w-40\">\n                <SelectValue placeholder=\"Common patterns\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"custom\">Custom</SelectItem>\n                {COMMON_TOOL_MATCHERS.map(pattern => (\n                  <SelectItem key={pattern} value={pattern}>{pattern}</SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n            \n            {!readOnly && (\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => removeMatcher(event, matcher.id)}\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n            )}\n          </div>\n        </div>\n      </div>\n      \n      <AnimatePresence>\n        {matcher.expanded && (\n          <motion.div\n            initial={{ opacity: 0, height: 0 }}\n            animate={{ opacity: 1, height: 'auto' }}\n            exit={{ opacity: 0, height: 0 }}\n            className=\"space-y-4 pl-10\"\n          >\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <Label>Commands</Label>\n                {!readOnly && (\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={() => addCommand(event, matcher.id)}\n                  >\n                    <Plus className=\"h-3 w-3 mr-1\" />\n                    Add Command\n                  </Button>\n                )}\n              </div>\n              \n              {matcher.hooks.length === 0 ? (\n                <p className=\"text-sm text-muted-foreground\">No commands added yet</p>\n              ) : (\n                <div className=\"space-y-2\">\n                  {matcher.hooks.map((hook) => (\n                    <div key={hook.id} className=\"space-y-2\">\n                      <div className=\"flex items-start gap-2\">\n                        <div className=\"flex-1 space-y-2\">\n                          <Textarea\n                            placeholder=\"Enter shell command...\"\n                            value={hook.command || ''}\n                            onChange={(e) => updateCommand(event, matcher.id, hook.id, { command: e.target.value })}\n                            disabled={readOnly}\n                            className=\"font-mono text-sm min-h-[80px]\"\n                          />\n                          \n                          <div className=\"flex items-center gap-4\">\n                            <div className=\"flex items-center gap-2\">\n                              <Clock className=\"h-3 w-3 text-muted-foreground\" />\n                              <Input\n                                type=\"number\"\n                                placeholder=\"60\"\n                                value={hook.timeout || ''}\n                                onChange={(e) => updateCommand(event, matcher.id, hook.id, { \n                                  timeout: e.target.value ? parseInt(e.target.value) : undefined \n                                })}\n                                disabled={readOnly}\n                                className=\"w-20 h-8\"\n                              />\n                              <span className=\"text-sm text-muted-foreground\">seconds</span>\n                            </div>\n                            \n                            {!readOnly && (\n                              <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => removeCommand(event, matcher.id, hook.id)}\n                              >\n                                <Trash2 className=\"h-4 w-4\" />\n                              </Button>\n                            )}\n                          </div>\n                        </div>\n                      </div>\n                      \n                      {/* Show warnings for this command */}\n                      {(() => {\n                        const warnings = HooksManager.checkDangerousPatterns(hook.command || '');\n                        return warnings.length > 0 && (\n                          <div className=\"flex items-start gap-2 p-2 bg-yellow-500/10 rounded-md\">\n                            <AlertTriangle className=\"h-4 w-4 text-yellow-600 mt-0.5\" />\n                            <div className=\"space-y-1\">\n                              {warnings.map((warning, i) => (\n                                <p key={i} className=\"text-xs text-yellow-600\">{warning}</p>\n                              ))}\n                            </div>\n                          </div>\n                        );\n                      })()}\n                    </div>\n                  ))}\n                </div>\n              )}\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </Card>\n  );\n  \n  const renderDirectCommand = (event: HookEvent, command: EditableHookCommand) => (\n    <Card key={command.id} className=\"p-4 space-y-2\">\n      <div className=\"flex items-start gap-2\">\n        <div className=\"flex-1 space-y-2\">\n          <Textarea\n            placeholder=\"Enter shell command...\"\n            value={command.command || ''}\n            onChange={(e) => updateDirectCommand(event, command.id, { command: e.target.value })}\n            disabled={readOnly}\n            className=\"font-mono text-sm min-h-[80px]\"\n          />\n          \n          <div className=\"flex items-center gap-4\">\n            <div className=\"flex items-center gap-2\">\n              <Clock className=\"h-3 w-3 text-muted-foreground\" />\n              <Input\n                type=\"number\"\n                placeholder=\"60\"\n                value={command.timeout || ''}\n                onChange={(e) => updateDirectCommand(event, command.id, { \n                  timeout: e.target.value ? parseInt(e.target.value) : undefined \n                })}\n                disabled={readOnly}\n                className=\"w-20 h-8\"\n              />\n              <span className=\"text-sm text-muted-foreground\">seconds</span>\n            </div>\n            \n            {!readOnly && (\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => removeDirectCommand(event, command.id)}\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n            )}\n          </div>\n        </div>\n      </div>\n      \n      {/* Show warnings for this command */}\n      {(() => {\n        const warnings = HooksManager.checkDangerousPatterns(command.command || '');\n        return warnings.length > 0 && (\n          <div className=\"flex items-start gap-2 p-2 bg-yellow-500/10 rounded-md\">\n            <AlertTriangle className=\"h-4 w-4 text-yellow-600 mt-0.5\" />\n            <div className=\"space-y-1\">\n              {warnings.map((warning, i) => (\n                <p key={i} className=\"text-xs text-yellow-600\">{warning}</p>\n              ))}\n            </div>\n          </div>\n        );\n      })()}\n    </Card>\n  );\n\n  return (\n    <div className={cn(\"space-y-6\", className)}>\n      {/* Loading State */}\n      {isLoading && (\n        <div className=\"flex items-center justify-center p-8\">\n          <Loader2 className=\"h-6 w-6 animate-spin mr-2\" />\n          <span className=\"text-sm text-muted-foreground\">Loading hooks configuration...</span>\n        </div>\n      )}\n      \n      {/* Error State */}\n      {loadError && !isLoading && (\n        <div className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-xs text-destructive flex items-center gap-2\">\n          <AlertTriangle className=\"h-4 w-4 flex-shrink-0\" />\n          {loadError}\n        </div>\n      )}\n      \n      {/* Main Content */}\n      {!isLoading && (\n        <>\n          {/* Header */}\n          <div className=\"space-y-2\">\n            <div className=\"flex items-center justify-between\">\n              <h3 className=\"text-lg font-semibold\">Hooks Configuration</h3>\n              <div className=\"flex items-center gap-2\">\n                <Badge variant={scope === 'project' ? 'secondary' : scope === 'local' ? 'outline' : 'default'}>\n                  {scope === 'project' ? 'Project' : scope === 'local' ? 'Local' : 'User'} Scope\n                </Badge>\n                {!readOnly && (\n                  <>\n                    <Button\n                      variant=\"outline\"\n                      size=\"sm\"\n                      onClick={() => setShowTemplateDialog(true)}\n                    >\n                      <FileText className=\"h-4 w-4 mr-2\" />\n                      Templates\n                    </Button>\n                    {!hideActions && (\n                      <Button\n                        variant={hasUnsavedChanges ? \"default\" : \"outline\"}\n                        size=\"sm\"\n                        onClick={handleSave}\n                        disabled={!hasUnsavedChanges || isSaving || !projectPath}\n                      >\n                        {isSaving ? (\n                          <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                        ) : (\n                          <Save className=\"h-4 w-4 mr-2\" />\n                        )}\n                        {isSaving ? \"Saving...\" : \"Save\"}\n                      </Button>\n                    )}\n                  </>\n                )}\n              </div>\n            </div>\n            <p className=\"text-sm text-muted-foreground\">\n              Configure shell commands to execute at various points in Claude Code's lifecycle.\n              {scope === 'local' && ' These settings are not committed to version control.'}\n            </p>\n            {hasUnsavedChanges && !readOnly && (\n              <p className=\"text-sm text-amber-600\">\n                You have unsaved changes. Click Save to persist them.\n              </p>\n            )}\n          </div>\n\n          {/* Validation Messages */}\n          {validationErrors.length > 0 && (\n            <div className=\"p-3 bg-red-500/10 rounded-md space-y-1\">\n              <p className=\"text-sm font-medium text-red-600\">Validation Errors:</p>\n              {validationErrors.map((error, i) => (\n                <p key={i} className=\"text-xs text-red-600\">• {error}</p>\n              ))}\n            </div>\n          )}\n\n          {validationWarnings.length > 0 && (\n            <div className=\"p-3 bg-yellow-500/10 rounded-md space-y-1\">\n              <p className=\"text-sm font-medium text-yellow-600\">Security Warnings:</p>\n              {validationWarnings.map((warning, i) => (\n                <p key={i} className=\"text-xs text-yellow-600\">• {warning}</p>\n              ))}\n            </div>\n          )}\n\n          {/* Event Tabs */}\n          <Tabs value={selectedEvent} onValueChange={(v) => setSelectedEvent(v as HookEvent)}>\n            <TabsList className=\"w-full\">\n              {(Object.keys(EVENT_INFO) as HookEvent[]).map(event => {\n                const isMatcherEvent = matcherEvents.includes(event as any);\n                const count = isMatcherEvent \n                  ? (editableHooks[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[]).length\n                  : (editableHooks[event as 'Notification' | 'Stop' | 'SubagentStop'] as EditableHookCommand[]).length;\n                \n                return (\n                  <TabsTrigger key={event} value={event} className=\"flex items-center gap-2\">\n                    {EVENT_INFO[event].icon}\n                    <span className=\"hidden sm:inline\">{EVENT_INFO[event].label}</span>\n                    {count > 0 && (\n                      <Badge variant=\"secondary\" className=\"ml-1 h-5 px-1\">\n                        {count}\n                      </Badge>\n                    )}\n                  </TabsTrigger>\n                );\n              })}\n            </TabsList>\n\n            {(Object.keys(EVENT_INFO) as HookEvent[]).map(event => {\n              const isMatcherEvent = matcherEvents.includes(event as any);\n              const items = isMatcherEvent \n                ? (editableHooks[event as 'PreToolUse' | 'PostToolUse'] as EditableHookMatcher[])\n                : (editableHooks[event as 'Notification' | 'Stop' | 'SubagentStop'] as EditableHookCommand[]);\n              \n              return (\n                <TabsContent key={event} value={event} className=\"space-y-4\">\n                  <div className=\"space-y-2\">\n                    <p className=\"text-sm text-muted-foreground\">\n                      {EVENT_INFO[event].description}\n                    </p>\n                  </div>\n\n                  {items.length === 0 ? (\n                    <Card className=\"p-8 text-center\">\n                      <p className=\"text-muted-foreground mb-4\">No hooks configured for this event</p>\n                      {!readOnly && (\n                        <Button onClick={() => isMatcherEvent ? addMatcher(event) : addDirectCommand(event)}>\n                          <Plus className=\"h-4 w-4 mr-2\" />\n                          Add Hook\n                        </Button>\n                      )}\n                    </Card>\n                  ) : (\n                    <div className=\"space-y-4\">\n                      {isMatcherEvent \n                        ? (items as EditableHookMatcher[]).map(matcher => renderMatcher(event, matcher))\n                        : (items as EditableHookCommand[]).map(command => renderDirectCommand(event, command))\n                      }\n                      \n                      {!readOnly && (\n                        <Button\n                          variant=\"outline\"\n                          onClick={() => isMatcherEvent ? addMatcher(event) : addDirectCommand(event)}\n                          className=\"w-full\"\n                        >\n                          <Plus className=\"h-4 w-4 mr-2\" />\n                          Add Another {isMatcherEvent ? 'Matcher' : 'Command'}\n                        </Button>\n                      )}\n                    </div>\n                  )}\n                </TabsContent>\n              );\n            })}\n          </Tabs>\n\n          {/* Template Dialog */}\n          <Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto\">\n              <DialogHeader>\n                <DialogTitle>Hook Templates</DialogTitle>\n                <DialogDescription>\n                  Choose a pre-configured hook template to get started quickly\n                </DialogDescription>\n              </DialogHeader>\n              \n              <div className=\"space-y-4 py-4\">\n                {HOOK_TEMPLATES.map(template => (\n                  <Card\n                    key={template.id}\n                    className=\"p-4 cursor-pointer hover:bg-accent\"\n                    onClick={() => applyTemplate(template)}\n                  >\n                    <div className=\"space-y-2\">\n                      <div className=\"flex items-center justify-between\">\n                        <h4 className=\"font-medium\">{template.name}</h4>\n                        <Badge>{EVENT_INFO[template.event].label}</Badge>\n                      </div>\n                      <p className=\"text-sm text-muted-foreground\">{template.description}</p>\n                      {matcherEvents.includes(template.event as any) && template.matcher && (\n                        <p className=\"text-xs font-mono bg-muted px-2 py-1 rounded inline-block\">\n                          Matcher: {template.matcher}\n                        </p>\n                      )}\n                    </div>\n                  </Card>\n                ))}\n              </div>\n            </DialogContent>\n          </Dialog>\n        </>\n      )}\n    </div>\n  );\n}; \n"
  },
  {
    "path": "src/components/IconPicker.tsx",
    "content": "import React, { useState, useMemo } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport {\n  // Interface & Navigation\n  Home,\n  Menu,\n  Settings,\n  User,\n  Users,\n  LogOut,\n  Bell,\n  Bookmark,\n  Calendar,\n  Clock,\n  Eye,\n  EyeOff,\n  Hash,\n  Heart,\n  Info,\n  Link,\n  Lock,\n  Map,\n  MessageSquare,\n  Mic,\n  Music,\n  Paperclip,\n  Phone,\n  Pin,\n  Plus,\n  Save,\n  Share,\n  Star,\n  Tag,\n  Trash,\n  Upload,\n  Download,\n  Edit,\n  Copy,\n  // Development & Tech\n  Bot,\n  Brain,\n  Code,\n  Terminal,\n  Cpu,\n  Database,\n  GitBranch,\n  Github,\n  Globe,\n  HardDrive,\n  Laptop,\n  Monitor,\n  Server,\n  Wifi,\n  Cloud,\n  Command,\n  FileCode,\n  FileJson,\n  Folder,\n  FolderOpen,\n  Bug,\n  Coffee,\n  // Business & Finance\n  Briefcase,\n  Building,\n  CreditCard,\n  DollarSign,\n  TrendingUp,\n  TrendingDown,\n  BarChart,\n  PieChart,\n  Calculator,\n  Receipt,\n  Wallet,\n  // Creative & Design\n  Palette,\n  Brush,\n  Camera,\n  Film,\n  Image,\n  Layers,\n  Layout,\n  PenTool,\n  Scissors,\n  Type,\n  Zap,\n  Sparkles,\n  Wand2,\n  // Nature & Science\n  Beaker,\n  Atom,\n  Dna,\n  Flame,\n  Leaf,\n  Mountain,\n  Sun,\n  Moon,\n  CloudRain,\n  Snowflake,\n  TreePine,\n  Waves,\n  Wind,\n  // Gaming & Entertainment\n  Gamepad2,\n  Dice1,\n  Trophy,\n  Medal,\n  Crown,\n  Rocket,\n  Target,\n  Swords,\n  Shield,\n  // Communication\n  Mail,\n  Send,\n  MessageCircle,\n  Video,\n  Voicemail,\n  Radio,\n  Podcast,\n  Megaphone,\n  // Miscellaneous\n  Activity,\n  Anchor,\n  Award,\n  Battery,\n  Bluetooth,\n  Compass,\n  Crosshair,\n  Flag,\n  Flashlight,\n  Gift,\n  Headphones,\n  Key,\n  Lightbulb,\n  Package,\n  Puzzle,\n  Search as SearchIcon,\n  Smile,\n  ThumbsUp,\n  Umbrella,\n  Watch,\n  Wrench,\n  type LucideIcon,\n} from \"lucide-react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Icon categories for better organization\n */\nconst ICON_CATEGORIES = {\n  \"Interface & Navigation\": [\n    { name: \"home\", icon: Home },\n    { name: \"menu\", icon: Menu },\n    { name: \"settings\", icon: Settings },\n    { name: \"user\", icon: User },\n    { name: \"users\", icon: Users },\n    { name: \"log-out\", icon: LogOut },\n    { name: \"bell\", icon: Bell },\n    { name: \"bookmark\", icon: Bookmark },\n    { name: \"calendar\", icon: Calendar },\n    { name: \"clock\", icon: Clock },\n    { name: \"eye\", icon: Eye },\n    { name: \"eye-off\", icon: EyeOff },\n    { name: \"hash\", icon: Hash },\n    { name: \"heart\", icon: Heart },\n    { name: \"info\", icon: Info },\n    { name: \"link\", icon: Link },\n    { name: \"lock\", icon: Lock },\n    { name: \"map\", icon: Map },\n    { name: \"message-square\", icon: MessageSquare },\n    { name: \"mic\", icon: Mic },\n    { name: \"music\", icon: Music },\n    { name: \"paperclip\", icon: Paperclip },\n    { name: \"phone\", icon: Phone },\n    { name: \"pin\", icon: Pin },\n    { name: \"plus\", icon: Plus },\n    { name: \"save\", icon: Save },\n    { name: \"share\", icon: Share },\n    { name: \"star\", icon: Star },\n    { name: \"tag\", icon: Tag },\n    { name: \"trash\", icon: Trash },\n    { name: \"upload\", icon: Upload },\n    { name: \"download\", icon: Download },\n    { name: \"edit\", icon: Edit },\n    { name: \"copy\", icon: Copy },\n  ],\n  \"Development & Tech\": [\n    { name: \"bot\", icon: Bot },\n    { name: \"brain\", icon: Brain },\n    { name: \"code\", icon: Code },\n    { name: \"terminal\", icon: Terminal },\n    { name: \"cpu\", icon: Cpu },\n    { name: \"database\", icon: Database },\n    { name: \"git-branch\", icon: GitBranch },\n    { name: \"github\", icon: Github },\n    { name: \"globe\", icon: Globe },\n    { name: \"hard-drive\", icon: HardDrive },\n    { name: \"laptop\", icon: Laptop },\n    { name: \"monitor\", icon: Monitor },\n    { name: \"server\", icon: Server },\n    { name: \"wifi\", icon: Wifi },\n    { name: \"cloud\", icon: Cloud },\n    { name: \"command\", icon: Command },\n    { name: \"file-code\", icon: FileCode },\n    { name: \"file-json\", icon: FileJson },\n    { name: \"folder\", icon: Folder },\n    { name: \"folder-open\", icon: FolderOpen },\n    { name: \"bug\", icon: Bug },\n    { name: \"coffee\", icon: Coffee },\n  ],\n  \"Business & Finance\": [\n    { name: \"briefcase\", icon: Briefcase },\n    { name: \"building\", icon: Building },\n    { name: \"credit-card\", icon: CreditCard },\n    { name: \"dollar-sign\", icon: DollarSign },\n    { name: \"trending-up\", icon: TrendingUp },\n    { name: \"trending-down\", icon: TrendingDown },\n    { name: \"bar-chart\", icon: BarChart },\n    { name: \"pie-chart\", icon: PieChart },\n    { name: \"calculator\", icon: Calculator },\n    { name: \"receipt\", icon: Receipt },\n    { name: \"wallet\", icon: Wallet },\n  ],\n  \"Creative & Design\": [\n    { name: \"palette\", icon: Palette },\n    { name: \"brush\", icon: Brush },\n    { name: \"camera\", icon: Camera },\n    { name: \"film\", icon: Film },\n    { name: \"image\", icon: Image },\n    { name: \"layers\", icon: Layers },\n    { name: \"layout\", icon: Layout },\n    { name: \"pen-tool\", icon: PenTool },\n    { name: \"scissors\", icon: Scissors },\n    { name: \"type\", icon: Type },\n    { name: \"zap\", icon: Zap },\n    { name: \"sparkles\", icon: Sparkles },\n    { name: \"wand-2\", icon: Wand2 },\n  ],\n  \"Nature & Science\": [\n    { name: \"beaker\", icon: Beaker },\n    { name: \"atom\", icon: Atom },\n    { name: \"dna\", icon: Dna },\n    { name: \"flame\", icon: Flame },\n    { name: \"leaf\", icon: Leaf },\n    { name: \"mountain\", icon: Mountain },\n    { name: \"sun\", icon: Sun },\n    { name: \"moon\", icon: Moon },\n    { name: \"cloud-rain\", icon: CloudRain },\n    { name: \"snowflake\", icon: Snowflake },\n    { name: \"tree-pine\", icon: TreePine },\n    { name: \"waves\", icon: Waves },\n    { name: \"wind\", icon: Wind },\n  ],\n  \"Gaming & Entertainment\": [\n    { name: \"gamepad-2\", icon: Gamepad2 },\n    { name: \"dice-1\", icon: Dice1 },\n    { name: \"trophy\", icon: Trophy },\n    { name: \"medal\", icon: Medal },\n    { name: \"crown\", icon: Crown },\n    { name: \"rocket\", icon: Rocket },\n    { name: \"target\", icon: Target },\n    { name: \"swords\", icon: Swords },\n    { name: \"shield\", icon: Shield },\n  ],\n  \"Communication\": [\n    { name: \"mail\", icon: Mail },\n    { name: \"send\", icon: Send },\n    { name: \"message-circle\", icon: MessageCircle },\n    { name: \"video\", icon: Video },\n    { name: \"voicemail\", icon: Voicemail },\n    { name: \"radio\", icon: Radio },\n    { name: \"podcast\", icon: Podcast },\n    { name: \"megaphone\", icon: Megaphone },\n  ],\n  \"Miscellaneous\": [\n    { name: \"activity\", icon: Activity },\n    { name: \"anchor\", icon: Anchor },\n    { name: \"award\", icon: Award },\n    { name: \"battery\", icon: Battery },\n    { name: \"bluetooth\", icon: Bluetooth },\n    { name: \"compass\", icon: Compass },\n    { name: \"crosshair\", icon: Crosshair },\n    { name: \"flag\", icon: Flag },\n    { name: \"flashlight\", icon: Flashlight },\n    { name: \"gift\", icon: Gift },\n    { name: \"headphones\", icon: Headphones },\n    { name: \"key\", icon: Key },\n    { name: \"lightbulb\", icon: Lightbulb },\n    { name: \"package\", icon: Package },\n    { name: \"puzzle\", icon: Puzzle },\n    { name: \"search\", icon: SearchIcon },\n    { name: \"smile\", icon: Smile },\n    { name: \"thumbs-up\", icon: ThumbsUp },\n    { name: \"umbrella\", icon: Umbrella },\n    { name: \"watch\", icon: Watch },\n    { name: \"wrench\", icon: Wrench },\n  ],\n} as const;\n\ntype IconCategory = typeof ICON_CATEGORIES[keyof typeof ICON_CATEGORIES];\ntype IconItem = IconCategory[number];\n\ninterface IconPickerProps {\n  /**\n   * Currently selected icon name\n   */\n  value: string;\n  /**\n   * Callback when an icon is selected\n   */\n  onSelect: (iconName: string) => void;\n  /**\n   * Whether the picker is open\n   */\n  isOpen: boolean;\n  /**\n   * Callback to close the picker\n   */\n  onClose: () => void;\n}\n\n/**\n * Icon picker component with search and categories\n * Similar to Notion's icon picker interface\n */\nexport const IconPicker: React.FC<IconPickerProps> = ({\n  value,\n  onSelect,\n  isOpen,\n  onClose,\n}) => {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [hoveredIcon, setHoveredIcon] = useState<string | null>(null);\n\n  // Filter icons based on search query\n  const filteredCategories = useMemo(() => {\n    if (!searchQuery.trim()) return ICON_CATEGORIES;\n\n    const query = searchQuery.toLowerCase();\n    const filtered: Record<string, IconItem[]> = {};\n\n    Object.entries(ICON_CATEGORIES).forEach(([category, icons]) => {\n      const matchingIcons = icons.filter(({ name }) =>\n        name.toLowerCase().includes(query)\n      );\n      if (matchingIcons.length > 0) {\n        filtered[category] = matchingIcons;\n      }\n    });\n\n    return filtered;\n  }, [searchQuery]);\n\n  // Get all icons for search\n  const allIcons = useMemo(() => {\n    return Object.values(ICON_CATEGORIES).flat();\n  }, []);\n\n  const handleSelect = (iconName: string) => {\n    onSelect(iconName);\n    onClose();\n    setSearchQuery(\"\");\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent className=\"max-w-2xl max-h-[80vh] p-0\">\n        <DialogHeader className=\"px-6 py-4 border-b\">\n          <DialogTitle>Choose an icon</DialogTitle>\n        </DialogHeader>\n\n        {/* Search Bar */}\n        <div className=\"px-6 py-3 border-b\">\n          <div className=\"relative\">\n            <SearchIcon className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n            <Input\n              placeholder=\"Search icons...\"\n              value={searchQuery}\n              onChange={(e) => setSearchQuery(e.target.value)}\n              className=\"pl-10\"\n              autoFocus\n            />\n          </div>\n        </div>\n\n        {/* Icon Grid */}\n        <div className=\"h-[60vh] px-6 py-4 overflow-y-auto\">\n          {Object.keys(filteredCategories).length === 0 ? (\n            <div className=\"flex flex-col items-center justify-center h-32 text-center\">\n              <p className=\"text-sm text-muted-foreground\">\n                No icons found for \"{searchQuery}\"\n              </p>\n            </div>\n          ) : (\n            <div className=\"space-y-6\">\n              <AnimatePresence mode=\"wait\">\n                {Object.entries(filteredCategories).map(([category, icons]) => (\n                  <motion.div\n                    key={category}\n                    initial={{ opacity: 0, y: 10 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -10 }}\n                    transition={{ duration: 0.2 }}\n                  >\n                    <h3 className=\"text-sm font-medium text-muted-foreground mb-3\">\n                      {category}\n                    </h3>\n                    <div className=\"grid grid-cols-10 gap-2\">\n                      {icons.map((item: IconItem) => {\n                        const Icon = item.icon;\n                        return (\n                          <motion.button\n                            key={item.name}\n                            whileHover={{ scale: 1.1 }}\n                            whileTap={{ scale: 0.95 }}\n                            onClick={() => handleSelect(item.name)}\n                            onMouseEnter={() => setHoveredIcon(item.name)}\n                            onMouseLeave={() => setHoveredIcon(null)}\n                            className={cn(\n                              \"p-2.5 rounded-lg transition-colors relative group\",\n                              \"hover:bg-accent hover:text-accent-foreground\",\n                              value === item.name && \"bg-primary/10 text-primary\"\n                            )}\n                          >\n                            <Icon className=\"h-5 w-5\" />\n                            {hoveredIcon === item.name && (\n                              <div className=\"absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded shadow-lg whitespace-nowrap z-10\">\n                                {item.name}\n                              </div>\n                            )}\n                          </motion.button>\n                        );\n                      })}\n                    </div>\n                  </motion.div>\n                ))}\n              </AnimatePresence>\n            </div>\n          )}\n        </div>\n\n        {/* Footer */}\n        <div className=\"px-6 py-3 border-t bg-muted/50\">\n          <p className=\"text-xs text-muted-foreground text-center\">\n            Click an icon to select • {allIcons.length} icons available\n          </p>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\n// Export all available icon names for type safety\nexport const AVAILABLE_ICONS = Object.values(ICON_CATEGORIES)\n  .flat()\n  .map(({ name }) => name);\n\n// Export icon map for easy access\nexport const ICON_MAP = Object.values(ICON_CATEGORIES)\n  .flat()\n  .reduce((acc, { name, icon }) => ({ ...acc, [name]: icon }), {} as Record<string, LucideIcon>); "
  },
  {
    "path": "src/components/ImagePreview.tsx",
    "content": "import React, { useState } from \"react\";\nimport { X, Maximize2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent, DialogTitle } from \"@/components/ui/dialog\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { convertFileSrc } from \"@tauri-apps/api/core\";\n\ninterface ImagePreviewProps {\n  /**\n   * Array of image file paths to preview\n   */\n  images: string[];\n  /**\n   * Callback to remove an image from the preview\n   */\n  onRemove: (index: number) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * ImagePreview component - Shows thumbnail previews of embedded images\n * \n * Features:\n * - Shows up to 10 image thumbnails in a row\n * - Click on thumbnail to see full-size preview\n * - Hover to show remove button\n * - Smooth animations\n * \n * @example\n * <ImagePreview \n *   images={[\"/path/to/image1.png\", \"/path/to/image2.jpg\"]}\n *   onRemove={(index) => console.log('Remove image at', index)}\n * />\n */\nexport const ImagePreview: React.FC<ImagePreviewProps> = ({\n  images,\n  onRemove,\n  className,\n}) => {\n  const [selectedImageIndex, setSelectedImageIndex] = useState<number | null>(null);\n  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n  const [imageErrors, setImageErrors] = useState<Set<number>>(new Set());\n\n  // Limit to 10 images\n  const displayImages = images.slice(0, 10);\n\n  const handleImageError = (index: number) => {\n    setImageErrors(prev => new Set(prev).add(index));\n  };\n\n  const handleRemove = (e: React.MouseEvent, index: number) => {\n    e.stopPropagation();\n    onRemove(index);\n  };\n\n  // Helper to get the image source - handles both file paths and data URLs\n  const getImageSrc = (imagePath: string): string => {\n    // If it's already a data URL, return as-is\n    if (imagePath.startsWith('data:')) {\n      return imagePath;\n    }\n    // Otherwise, convert the file path\n    return convertFileSrc(imagePath);\n  };\n\n  if (displayImages.length === 0) return null;\n\n  return (\n    <>\n      <div className={cn(\"flex gap-2 p-2 overflow-x-auto\", className)}>\n        <AnimatePresence>\n          {displayImages.map((imagePath, index) => (\n            <motion.div\n              key={`${imagePath}-${index}`}\n              initial={{ opacity: 0, scale: 0.8 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: 0.8 }}\n              transition={{ duration: 0.2 }}\n              className=\"relative flex-shrink-0 group\"\n              onMouseEnter={() => setHoveredIndex(index)}\n              onMouseLeave={() => setHoveredIndex(null)}\n            >\n              <div\n                className=\"relative w-16 h-16 rounded-md overflow-hidden border border-border cursor-pointer hover:border-primary transition-colors\"\n                onClick={() => setSelectedImageIndex(index)}\n              >\n                {imageErrors.has(index) ? (\n                  <div className=\"w-full h-full bg-muted flex items-center justify-center\">\n                    <span className=\"text-xs text-muted-foreground\">Error</span>\n                  </div>\n                ) : (\n                  <img\n                    src={getImageSrc(imagePath)}\n                    alt={`Preview ${index + 1}`}\n                    className=\"w-full h-full object-cover\"\n                    onError={() => handleImageError(index)}\n                  />\n                )}\n                \n                {/* Hover overlay with maximize icon */}\n                <div className=\"absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center\">\n                  <Maximize2 className=\"h-4 w-4 text-white\" />\n                </div>\n              </div>\n\n              {/* Remove button */}\n              <AnimatePresence>\n                {hoveredIndex === index && (\n                  <motion.button\n                    initial={{ opacity: 0, scale: 0.8 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    exit={{ opacity: 0, scale: 0.8 }}\n                    className=\"absolute -top-1 -right-1 w-5 h-5 bg-destructive text-destructive-foreground rounded-full flex items-center justify-center hover:bg-destructive/90 transition-colors\"\n                    onClick={(e) => handleRemove(e, index)}\n                  >\n                    <X className=\"h-3 w-3\" />\n                  </motion.button>\n                )}\n              </AnimatePresence>\n            </motion.div>\n          ))}\n        </AnimatePresence>\n\n        {images.length > 10 && (\n          <div className=\"flex-shrink-0 w-16 h-16 rounded-md border border-border bg-muted flex items-center justify-center\">\n            <span className=\"text-xs text-muted-foreground\">+{images.length - 10}</span>\n          </div>\n        )}\n      </div>\n\n      {/* Full-size preview dialog */}\n      <Dialog \n        open={selectedImageIndex !== null} \n        onOpenChange={(open) => !open && setSelectedImageIndex(null)}\n      >\n        <DialogContent className=\"max-w-4xl max-h-[90vh] p-0\">\n          <DialogTitle className=\"sr-only\">Image Preview</DialogTitle>\n          {selectedImageIndex !== null && (\n            <div className=\"relative w-full h-full flex items-center justify-center p-4\">\n              <img\n                src={getImageSrc(displayImages[selectedImageIndex])}\n                alt={`Full preview ${selectedImageIndex + 1}`}\n                className=\"max-w-full max-h-full object-contain\"\n                onError={() => handleImageError(selectedImageIndex)}\n              />\n              \n              {/* Navigation buttons if multiple images */}\n              {displayImages.length > 1 && (\n                <>\n                  <button\n                    className=\"absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-black/50 text-white rounded-full flex items-center justify-center hover:bg-black/70 transition-colors\"\n                    onClick={() => setSelectedImageIndex((prev) => \n                      prev !== null ? (prev - 1 + displayImages.length) % displayImages.length : 0\n                    )}\n                  >\n                    ←\n                  </button>\n                  <button\n                    className=\"absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-black/50 text-white rounded-full flex items-center justify-center hover:bg-black/70 transition-colors\"\n                    onClick={() => setSelectedImageIndex((prev) => \n                      prev !== null ? (prev + 1) % displayImages.length : 0\n                    )}\n                  >\n                    →\n                  </button>\n                </>\n              )}\n            </div>\n          )}\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}; \n"
  },
  {
    "path": "src/components/MCPAddServer.tsx",
    "content": "import React, { useState } from \"react\";\nimport { Plus, Terminal, Globe, Trash2, Info, Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { SelectComponent } from \"@/components/ui/select\";\nimport { Card } from \"@/components/ui/card\";\nimport { api } from \"@/lib/api\";\nimport { useTrackEvent } from \"@/hooks\";\n\ninterface MCPAddServerProps {\n  /**\n   * Callback when a server is successfully added\n   */\n  onServerAdded: () => void;\n  /**\n   * Callback for error messages\n   */\n  onError: (message: string) => void;\n}\n\ninterface EnvironmentVariable {\n  id: string;\n  key: string;\n  value: string;\n}\n\n/**\n * Component for adding new MCP servers\n * Supports both stdio and SSE transport types\n */\nexport const MCPAddServer: React.FC<MCPAddServerProps> = ({\n  onServerAdded,\n  onError,\n}) => {\n  const [transport, setTransport] = useState<\"stdio\" | \"sse\">(\"stdio\");\n  const [saving, setSaving] = useState(false);\n  \n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n  \n  // Stdio server state\n  const [stdioName, setStdioName] = useState(\"\");\n  const [stdioCommand, setStdioCommand] = useState(\"\");\n  const [stdioArgs, setStdioArgs] = useState(\"\");\n  const [stdioScope, setStdioScope] = useState(\"local\");\n  const [stdioEnvVars, setStdioEnvVars] = useState<EnvironmentVariable[]>([]);\n  \n  // SSE server state\n  const [sseName, setSseName] = useState(\"\");\n  const [sseUrl, setSseUrl] = useState(\"\");\n  const [sseScope, setSseScope] = useState(\"local\");\n  const [sseEnvVars, setSseEnvVars] = useState<EnvironmentVariable[]>([]);\n\n  /**\n   * Adds a new environment variable\n   */\n  const addEnvVar = (type: \"stdio\" | \"sse\") => {\n    const newVar: EnvironmentVariable = {\n      id: `env-${Date.now()}`,\n      key: \"\",\n      value: \"\",\n    };\n    \n    if (type === \"stdio\") {\n      setStdioEnvVars(prev => [...prev, newVar]);\n    } else {\n      setSseEnvVars(prev => [...prev, newVar]);\n    }\n  };\n\n  /**\n   * Updates an environment variable\n   */\n  const updateEnvVar = (type: \"stdio\" | \"sse\", id: string, field: \"key\" | \"value\", value: string) => {\n    if (type === \"stdio\") {\n      setStdioEnvVars(prev => prev.map(v => \n        v.id === id ? { ...v, [field]: value } : v\n      ));\n    } else {\n      setSseEnvVars(prev => prev.map(v => \n        v.id === id ? { ...v, [field]: value } : v\n      ));\n    }\n  };\n\n  /**\n   * Removes an environment variable\n   */\n  const removeEnvVar = (type: \"stdio\" | \"sse\", id: string) => {\n    if (type === \"stdio\") {\n      setStdioEnvVars(prev => prev.filter(v => v.id !== id));\n    } else {\n      setSseEnvVars(prev => prev.filter(v => v.id !== id));\n    }\n  };\n\n  /**\n   * Validates and adds a stdio server\n   */\n  const handleAddStdioServer = async () => {\n    if (!stdioName.trim()) {\n      onError(\"Server name is required\");\n      return;\n    }\n    \n    if (!stdioCommand.trim()) {\n      onError(\"Command is required\");\n      return;\n    }\n    \n    try {\n      setSaving(true);\n      \n      // Parse arguments\n      const args = stdioArgs.trim() ? stdioArgs.split(/\\s+/) : [];\n      \n      // Convert env vars to object\n      const env = stdioEnvVars.reduce((acc, { key, value }) => {\n        if (key.trim() && value.trim()) {\n          acc[key] = value;\n        }\n        return acc;\n      }, {} as Record<string, string>);\n      \n      const result = await api.mcpAdd(\n        stdioName,\n        \"stdio\",\n        stdioCommand,\n        args,\n        env,\n        undefined,\n        stdioScope\n      );\n      \n      if (result.success) {\n        // Track server added\n        trackEvent.mcpServerAdded({\n          server_type: \"stdio\",\n          configuration_method: \"manual\"\n        });\n        \n        // Reset form\n        setStdioName(\"\");\n        setStdioCommand(\"\");\n        setStdioArgs(\"\");\n        setStdioEnvVars([]);\n        setStdioScope(\"local\");\n        onServerAdded();\n      } else {\n        onError(result.message);\n      }\n    } catch (error) {\n      onError(\"Failed to add server\");\n      console.error(\"Failed to add stdio server:\", error);\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  /**\n   * Validates and adds an SSE server\n   */\n  const handleAddSseServer = async () => {\n    if (!sseName.trim()) {\n      onError(\"Server name is required\");\n      return;\n    }\n    \n    if (!sseUrl.trim()) {\n      onError(\"URL is required\");\n      return;\n    }\n    \n    try {\n      setSaving(true);\n      \n      // Convert env vars to object\n      const env = sseEnvVars.reduce((acc, { key, value }) => {\n        if (key.trim() && value.trim()) {\n          acc[key] = value;\n        }\n        return acc;\n      }, {} as Record<string, string>);\n      \n      const result = await api.mcpAdd(\n        sseName,\n        \"sse\",\n        undefined,\n        [],\n        env,\n        sseUrl,\n        sseScope\n      );\n      \n      if (result.success) {\n        // Track server added\n        trackEvent.mcpServerAdded({\n          server_type: \"sse\",\n          configuration_method: \"manual\"\n        });\n        \n        // Reset form\n        setSseName(\"\");\n        setSseUrl(\"\");\n        setSseEnvVars([]);\n        setSseScope(\"local\");\n        onServerAdded();\n      } else {\n        onError(result.message);\n      }\n    } catch (error) {\n      onError(\"Failed to add server\");\n      console.error(\"Failed to add SSE server:\", error);\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  /**\n   * Renders environment variable inputs\n   */\n  const renderEnvVars = (type: \"stdio\" | \"sse\", envVars: EnvironmentVariable[]) => {\n    return (\n      <div className=\"space-y-3\">\n        <div className=\"flex items-center justify-between\">\n          <Label className=\"text-sm font-medium\">Environment Variables</Label>\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={() => addEnvVar(type)}\n            className=\"gap-2\"\n          >\n            <Plus className=\"h-3 w-3\" />\n            Add Variable\n          </Button>\n        </div>\n        \n        {envVars.length > 0 && (\n          <div className=\"space-y-2\">\n            {envVars.map((envVar) => (\n              <div key={envVar.id} className=\"flex items-center gap-2\">\n                <Input\n                  placeholder=\"KEY\"\n                  value={envVar.key}\n                  onChange={(e) => updateEnvVar(type, envVar.id, \"key\", e.target.value)}\n                  className=\"flex-1 font-mono text-sm\"\n                />\n                <span className=\"text-muted-foreground\">=</span>\n                <Input\n                  placeholder=\"value\"\n                  value={envVar.value}\n                  onChange={(e) => updateEnvVar(type, envVar.id, \"value\", e.target.value)}\n                  className=\"flex-1 font-mono text-sm\"\n                />\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  onClick={() => removeEnvVar(type, envVar.id)}\n                  className=\"h-8 w-8 hover:text-destructive\"\n                >\n                  <Trash2 className=\"h-4 w-4\" />\n                </Button>\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    );\n  };\n\n  return (\n    <div className=\"p-6 space-y-6\">\n      <div>\n        <h3 className=\"text-base font-semibold\">Add MCP Server</h3>\n        <p className=\"text-sm text-muted-foreground mt-1\">\n          Configure a new Model Context Protocol server\n        </p>\n      </div>\n\n      <Tabs value={transport} onValueChange={(v) => setTransport(v as \"stdio\" | \"sse\")}>\n        <TabsList className=\"grid w-full grid-cols-2 max-w-sm mb-6\">\n          <TabsTrigger value=\"stdio\" className=\"gap-2\">\n            <Terminal className=\"h-4 w-4 text-amber-500\" />\n            Stdio\n          </TabsTrigger>\n          <TabsTrigger value=\"sse\" className=\"gap-2\">\n            <Globe className=\"h-4 w-4 text-emerald-500\" />\n            SSE\n          </TabsTrigger>\n        </TabsList>\n\n        {/* Stdio Server */}\n        <TabsContent value=\"stdio\" className=\"space-y-6\">\n          <Card className=\"p-6 space-y-6\">\n            <div className=\"space-y-4\">\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"stdio-name\">Server Name</Label>\n                <Input\n                  id=\"stdio-name\"\n                  placeholder=\"my-server\"\n                  value={stdioName}\n                  onChange={(e) => setStdioName(e.target.value)}\n                />\n                <p className=\"text-xs text-muted-foreground\">\n                  A unique name to identify this server\n                </p>\n              </div>\n\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"stdio-command\">Command</Label>\n                <Input\n                  id=\"stdio-command\"\n                  placeholder=\"/path/to/server\"\n                  value={stdioCommand}\n                  onChange={(e) => setStdioCommand(e.target.value)}\n                  className=\"font-mono\"\n                />\n                <p className=\"text-xs text-muted-foreground\">\n                  The command to execute the server\n                </p>\n              </div>\n\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"stdio-args\">Arguments (optional)</Label>\n                <Input\n                  id=\"stdio-args\"\n                  placeholder=\"arg1 arg2 arg3\"\n                  value={stdioArgs}\n                  onChange={(e) => setStdioArgs(e.target.value)}\n                  className=\"font-mono\"\n                />\n                <p className=\"text-xs text-muted-foreground\">\n                  Space-separated command arguments\n                </p>\n              </div>\n\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"stdio-scope\">Scope</Label>\n                <SelectComponent\n                  value={stdioScope}\n                  onValueChange={(value: string) => setStdioScope(value)}\n                  options={[\n                    { value: \"local\", label: \"Local (this project only)\" },\n                    { value: \"project\", label: \"Project (shared via .mcp.json)\" },\n                    { value: \"user\", label: \"User (all projects)\" },\n                  ]}\n                />\n              </div>\n\n              {renderEnvVars(\"stdio\", stdioEnvVars)}\n            </div>\n\n            <div className=\"pt-2\">\n              <Button\n                onClick={handleAddStdioServer}\n                disabled={saving}\n                className=\"w-full gap-2 bg-primary hover:bg-primary/90\"\n              >\n                {saving ? (\n                  <>\n                    <Loader2 className=\"h-4 w-4 animate-spin\" />\n                    Adding Server...\n                  </>\n                ) : (\n                  <>\n                    <Plus className=\"h-4 w-4\" />\n                    Add Stdio Server\n                  </>\n                )}\n              </Button>\n            </div>\n          </Card>\n        </TabsContent>\n\n        {/* SSE Server */}\n        <TabsContent value=\"sse\" className=\"space-y-6\">\n          <Card className=\"p-6 space-y-6\">\n            <div className=\"space-y-4\">\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"sse-name\">Server Name</Label>\n                <Input\n                  id=\"sse-name\"\n                  placeholder=\"sse-server\"\n                  value={sseName}\n                  onChange={(e) => setSseName(e.target.value)}\n                />\n                <p className=\"text-xs text-muted-foreground\">\n                  A unique name to identify this server\n                </p>\n              </div>\n\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"sse-url\">URL</Label>\n                <Input\n                  id=\"sse-url\"\n                  placeholder=\"https://example.com/sse-endpoint\"\n                  value={sseUrl}\n                  onChange={(e) => setSseUrl(e.target.value)}\n                  className=\"font-mono\"\n                />\n                <p className=\"text-xs text-muted-foreground\">\n                  The SSE endpoint URL\n                </p>\n              </div>\n\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"sse-scope\">Scope</Label>\n                <SelectComponent\n                  value={sseScope}\n                  onValueChange={(value: string) => setSseScope(value)}\n                  options={[\n                    { value: \"local\", label: \"Local (this project only)\" },\n                    { value: \"project\", label: \"Project (shared via .mcp.json)\" },\n                    { value: \"user\", label: \"User (all projects)\" },\n                  ]}\n                />\n              </div>\n\n              {renderEnvVars(\"sse\", sseEnvVars)}\n            </div>\n\n            <div className=\"pt-2\">\n              <Button\n                onClick={handleAddSseServer}\n                disabled={saving}\n                className=\"w-full gap-2 bg-primary hover:bg-primary/90\"\n              >\n                {saving ? (\n                  <>\n                    <Loader2 className=\"h-4 w-4 animate-spin\" />\n                    Adding Server...\n                  </>\n                ) : (\n                  <>\n                    <Plus className=\"h-4 w-4\" />\n                    Add SSE Server\n                  </>\n                )}\n              </Button>\n            </div>\n          </Card>\n        </TabsContent>\n      </Tabs>\n\n      {/* Example */}\n      <Card className=\"p-4 bg-muted/30\">\n        <div className=\"space-y-3\">\n          <div className=\"flex items-center gap-2 text-sm font-medium\">\n            <Info className=\"h-4 w-4 text-primary\" />\n            <span>Example Commands</span>\n          </div>\n          <div className=\"space-y-2 text-xs text-muted-foreground\">\n            <div className=\"font-mono bg-background p-2 rounded\">\n              <p>• Postgres: /path/to/postgres-mcp-server --connection-string \"postgresql://...\"</p>\n              <p>• Weather API: /usr/local/bin/weather-cli --api-key ABC123</p>\n              <p>• SSE Server: https://api.example.com/mcp/stream</p>\n            </div>\n          </div>\n        </div>\n      </Card>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/MCPImportExport.tsx",
    "content": "import React, { useState } from \"react\";\nimport { Download, Upload, FileText, Loader2, Info, Network, Settings2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { Label } from \"@/components/ui/label\";\nimport { SelectComponent } from \"@/components/ui/select\";\nimport { api } from \"@/lib/api\";\n\ninterface MCPImportExportProps {\n  /**\n   * Callback when import is completed\n   */\n  onImportCompleted: (imported: number, failed: number) => void;\n  /**\n   * Callback for error messages\n   */\n  onError: (message: string) => void;\n}\n\n/**\n * Component for importing and exporting MCP server configurations\n */\nexport const MCPImportExport: React.FC<MCPImportExportProps> = ({\n  onImportCompleted,\n  onError,\n}) => {\n  const [importingDesktop, setImportingDesktop] = useState(false);\n  const [importingJson, setImportingJson] = useState(false);\n  const [importScope, setImportScope] = useState(\"local\");\n\n  /**\n   * Imports servers from Claude Desktop\n   */\n  const handleImportFromDesktop = async () => {\n    try {\n      setImportingDesktop(true);\n      // Always use \"user\" scope for Claude Desktop imports (was previously \"global\")\n      const result = await api.mcpAddFromClaudeDesktop(\"user\");\n      \n      // Show detailed results if available\n      if (result.servers && result.servers.length > 0) {\n        const successfulServers = result.servers.filter(s => s.success);\n        const failedServers = result.servers.filter(s => !s.success);\n        \n        if (successfulServers.length > 0) {\n          const successMessage = `Successfully imported: ${successfulServers.map(s => s.name).join(\", \")}`;\n          onImportCompleted(result.imported_count, result.failed_count);\n          // Show success details\n          if (failedServers.length === 0) {\n            onError(successMessage);\n          }\n        }\n        \n        if (failedServers.length > 0) {\n          const failureDetails = failedServers\n            .map(s => `${s.name}: ${s.error || \"Unknown error\"}`)\n            .join(\"\\n\");\n          onError(`Failed to import some servers:\\n${failureDetails}`);\n        }\n      } else {\n        onImportCompleted(result.imported_count, result.failed_count);\n      }\n    } catch (error: any) {\n      console.error(\"Failed to import from Claude Desktop:\", error);\n      onError(error.toString() || \"Failed to import from Claude Desktop\");\n    } finally {\n      setImportingDesktop(false);\n    }\n  };\n\n  /**\n   * Handles JSON file import\n   */\n  const handleJsonFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    if (!file) return;\n\n    try {\n      setImportingJson(true);\n      const content = await file.text();\n      \n      // Parse the JSON to validate it\n      let jsonData;\n      try {\n        jsonData = JSON.parse(content);\n      } catch (e) {\n        onError(\"Invalid JSON file. Please check the format.\");\n        return;\n      }\n\n      // Check if it's a single server or multiple servers\n      if (jsonData.mcpServers) {\n        // Multiple servers format\n        let imported = 0;\n        let failed = 0;\n\n        for (const [name, config] of Object.entries(jsonData.mcpServers)) {\n          try {\n            const serverConfig = {\n              type: \"stdio\",\n              command: (config as any).command,\n              args: (config as any).args || [],\n              env: (config as any).env || {}\n            };\n            \n            const result = await api.mcpAddJson(name, JSON.stringify(serverConfig), importScope);\n            if (result.success) {\n              imported++;\n            } else {\n              failed++;\n            }\n          } catch (e) {\n            failed++;\n          }\n        }\n        \n        onImportCompleted(imported, failed);\n      } else if (jsonData.type && jsonData.command) {\n        // Single server format\n        const name = prompt(\"Enter a name for this server:\");\n        if (!name) return;\n\n        const result = await api.mcpAddJson(name, content, importScope);\n        if (result.success) {\n          onImportCompleted(1, 0);\n        } else {\n          onError(result.message);\n        }\n      } else {\n        onError(\"Unrecognized JSON format. Expected MCP server configuration.\");\n      }\n    } catch (error) {\n      console.error(\"Failed to import JSON:\", error);\n      onError(\"Failed to import JSON file\");\n    } finally {\n      setImportingJson(false);\n      // Reset the input\n      event.target.value = \"\";\n    }\n  };\n\n  /**\n   * Handles exporting servers (placeholder)\n   */\n  const handleExport = () => {\n    // TODO: Implement export functionality\n    onError(\"Export functionality coming soon!\");\n  };\n\n  /**\n   * Starts Claude Code as MCP server\n   */\n  const handleStartMCPServer = async () => {\n    try {\n      await api.mcpServe();\n      onError(\"Claude Code MCP server started. You can now connect to it from other applications.\");\n    } catch (error) {\n      console.error(\"Failed to start MCP server:\", error);\n      onError(\"Failed to start Claude Code as MCP server\");\n    }\n  };\n\n  return (\n    <div className=\"p-6 space-y-6\">\n      <div>\n        <h3 className=\"text-base font-semibold\">Import & Export</h3>\n        <p className=\"text-sm text-muted-foreground mt-1\">\n          Import MCP servers from other sources or export your configuration\n        </p>\n      </div>\n\n      <div className=\"space-y-4\">\n        {/* Import Scope Selection */}\n        <Card className=\"p-4\">\n          <div className=\"space-y-3\">\n            <div className=\"flex items-center gap-2 mb-2\">\n              <Settings2 className=\"h-4 w-4 text-slate-500\" />\n              <Label className=\"text-sm font-medium\">Import Scope</Label>\n            </div>\n            <SelectComponent\n              value={importScope}\n              onValueChange={(value: string) => setImportScope(value)}\n              options={[\n                { value: \"local\", label: \"Local (this project only)\" },\n                { value: \"project\", label: \"Project (shared via .mcp.json)\" },\n                { value: \"user\", label: \"User (all projects)\" },\n              ]}\n            />\n            <p className=\"text-xs text-muted-foreground\">\n              Choose where to save imported servers from JSON files\n            </p>\n          </div>\n        </Card>\n\n        {/* Import from Claude Desktop */}\n        <Card className=\"p-4 hover:bg-accent/5 transition-colors\">\n          <div className=\"space-y-3\">\n            <div className=\"flex items-start gap-3\">\n              <div className=\"p-2.5 bg-blue-500/10 rounded-lg\">\n                <Download className=\"h-5 w-5 text-blue-500\" />\n              </div>\n              <div className=\"flex-1\">\n                <h4 className=\"text-sm font-medium\">Import from Claude Desktop</h4>\n                <p className=\"text-xs text-muted-foreground mt-1\">\n                  Automatically imports all MCP servers from Claude Desktop. Installs to user scope (available across all projects).\n                </p>\n              </div>\n            </div>\n            <Button\n              onClick={handleImportFromDesktop}\n              disabled={importingDesktop}\n              className=\"w-full gap-2 bg-primary hover:bg-primary/90\"\n            >\n              {importingDesktop ? (\n                <>\n                  <Loader2 className=\"h-4 w-4 animate-spin\" />\n                  Importing...\n                </>\n              ) : (\n                <>\n                  <Download className=\"h-4 w-4\" />\n                  Import from Claude Desktop\n                </>\n              )}\n            </Button>\n          </div>\n        </Card>\n\n        {/* Import from JSON */}\n        <Card className=\"p-4 hover:bg-accent/5 transition-colors\">\n          <div className=\"space-y-3\">\n            <div className=\"flex items-start gap-3\">\n              <div className=\"p-2.5 bg-purple-500/10 rounded-lg\">\n                <FileText className=\"h-5 w-5 text-purple-500\" />\n              </div>\n              <div className=\"flex-1\">\n                <h4 className=\"text-sm font-medium\">Import from JSON</h4>\n                <p className=\"text-xs text-muted-foreground mt-1\">\n                  Import server configuration from a JSON file\n                </p>\n              </div>\n            </div>\n            <div>\n              <input\n                type=\"file\"\n                accept=\".json\"\n                onChange={handleJsonFileSelect}\n                disabled={importingJson}\n                className=\"hidden\"\n                id=\"json-file-input\"\n              />\n              <Button\n                onClick={() => document.getElementById(\"json-file-input\")?.click()}\n                disabled={importingJson}\n                className=\"w-full gap-2\"\n                variant=\"outline\"\n              >\n                {importingJson ? (\n                  <>\n                    <Loader2 className=\"h-4 w-4 animate-spin\" />\n                    Importing...\n                  </>\n                ) : (\n                  <>\n                    <FileText className=\"h-4 w-4\" />\n                    Choose JSON File\n                  </>\n                )}\n              </Button>\n            </div>\n          </div>\n        </Card>\n\n        {/* Export (Coming Soon) */}\n        <Card className=\"p-4 opacity-60\">\n          <div className=\"space-y-3\">\n            <div className=\"flex items-start gap-3\">\n              <div className=\"p-2.5 bg-muted rounded-lg\">\n                <Upload className=\"h-5 w-5 text-muted-foreground\" />\n              </div>\n              <div className=\"flex-1\">\n                <h4 className=\"text-sm font-medium\">Export Configuration</h4>\n                <p className=\"text-xs text-muted-foreground mt-1\">\n                  Export your MCP server configuration\n                </p>\n              </div>\n            </div>\n            <Button\n              onClick={handleExport}\n              disabled={true}\n              variant=\"secondary\"\n              className=\"w-full gap-2\"\n            >\n              <Upload className=\"h-4 w-4\" />\n              Export (Coming Soon)\n            </Button>\n          </div>\n        </Card>\n\n        {/* Serve as MCP */}\n        <Card className=\"p-4 border-primary/20 bg-primary/5 hover:bg-primary/10 transition-colors\">\n          <div className=\"space-y-3\">\n            <div className=\"flex items-start gap-3\">\n              <div className=\"p-2.5 bg-green-500/20 rounded-lg\">\n                <Network className=\"h-5 w-5 text-green-500\" />\n              </div>\n              <div className=\"flex-1\">\n                <h4 className=\"text-sm font-medium\">Use Claude Code as MCP Server</h4>\n                <p className=\"text-xs text-muted-foreground mt-1\">\n                  Start Claude Code as an MCP server that other applications can connect to\n                </p>\n              </div>\n            </div>\n            <Button\n              onClick={handleStartMCPServer}\n              variant=\"outline\"\n              className=\"w-full gap-2 border-green-500/20 hover:bg-green-500/10 hover:text-green-600 hover:border-green-500/50\"\n            >\n              <Network className=\"h-4 w-4\" />\n              Start MCP Server\n            </Button>\n          </div>\n        </Card>\n      </div>\n\n      {/* Info Box */}\n      <Card className=\"p-4 bg-muted/30\">\n        <div className=\"space-y-3\">\n          <div className=\"flex items-center gap-2 text-sm font-medium\">\n            <Info className=\"h-4 w-4 text-primary\" />\n            <span>JSON Format Examples</span>\n          </div>\n          <div className=\"space-y-3 text-xs\">\n            <div>\n              <p className=\"font-medium text-muted-foreground mb-1\">Single server:</p>\n              <pre className=\"bg-background p-3 rounded-lg overflow-x-auto\">\n{`{\n  \"type\": \"stdio\",\n  \"command\": \"/path/to/server\",\n  \"args\": [\"--arg1\", \"value\"],\n  \"env\": { \"KEY\": \"value\" }\n}`}\n              </pre>\n            </div>\n            <div>\n              <p className=\"font-medium text-muted-foreground mb-1\">Multiple servers (.mcp.json format):</p>\n              <pre className=\"bg-background p-3 rounded-lg overflow-x-auto\">\n{`{\n  \"mcpServers\": {\n    \"server1\": {\n      \"command\": \"/path/to/server1\",\n      \"args\": [],\n      \"env\": {}\n    },\n    \"server2\": {\n      \"command\": \"/path/to/server2\",\n      \"args\": [\"--port\", \"8080\"],\n      \"env\": { \"API_KEY\": \"...\" }\n    }\n  }\n}`}\n              </pre>\n            </div>\n          </div>\n        </div>\n      </Card>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/MCPManager.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { AlertCircle, Loader2 } from \"lucide-react\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { Card } from \"@/components/ui/card\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { api, type MCPServer } from \"@/lib/api\";\nimport { MCPServerList } from \"./MCPServerList\";\nimport { MCPAddServer } from \"./MCPAddServer\";\nimport { MCPImportExport } from \"./MCPImportExport\";\n\ninterface MCPManagerProps {\n  /**\n   * Callback to go back to the main view\n   */\n  onBack: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * Main component for managing MCP (Model Context Protocol) servers\n * Provides a comprehensive UI for adding, configuring, and managing MCP servers\n */\nexport const MCPManager: React.FC<MCPManagerProps> = ({\n  className: _className,\n}) => {\n  const [activeTab, setActiveTab] = useState(\"servers\");\n  const [servers, setServers] = useState<MCPServer[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  \n\n  // Load servers on mount\n  useEffect(() => {\n    loadServers();\n  }, []);\n\n  /**\n   * Loads all MCP servers\n   */\n  const loadServers = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      console.log(\"MCPManager: Loading servers...\");\n      const serverList = await api.mcpList();\n      console.log(\"MCPManager: Received server list:\", serverList);\n      console.log(\"MCPManager: Server count:\", serverList.length);\n      setServers(serverList);\n    } catch (err) {\n      console.error(\"MCPManager: Failed to load MCP servers:\", err);\n      setError(\"Failed to load MCP servers. Make sure Claude Code is installed.\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Handles server added event\n   */\n  const handleServerAdded = () => {\n    loadServers();\n    setToast({ message: \"MCP server added successfully!\", type: \"success\" });\n    setActiveTab(\"servers\");\n  };\n\n  /**\n   * Handles server removed event\n   */\n  const handleServerRemoved = (name: string) => {\n    setServers(prev => prev.filter(s => s.name !== name));\n    setToast({ message: `Server \"${name}\" removed successfully!`, type: \"success\" });\n  };\n\n  /**\n   * Handles import completed event\n   */\n  const handleImportCompleted = (imported: number, failed: number) => {\n    loadServers();\n    if (failed === 0) {\n      setToast({ \n        message: `Successfully imported ${imported} server${imported > 1 ? 's' : ''}!`, \n        type: \"success\" \n      });\n    } else {\n      setToast({ \n        message: `Imported ${imported} server${imported > 1 ? 's' : ''}, ${failed} failed`, \n        type: \"error\" \n      });\n    }\n  };\n\n  return (\n    <div className=\"h-full overflow-y-auto\">\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <h1 className=\"text-heading-1\">MCP Servers</h1>\n              <p className=\"mt-1 text-body-small text-muted-foreground\">\n                Manage Model Context Protocol servers\n              </p>\n            </div>\n          </div>\n        </div>\n\n        {/* Error Display */}\n        <AnimatePresence>\n          {error && (\n            <motion.div\n              initial={{ opacity: 0, y: -10 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -10 }}\n              className=\"mx-6 mb-4 p-3 rounded-lg bg-destructive/10 border border-destructive/50 flex items-center gap-2 text-body-small text-destructive\"\n            >\n              <AlertCircle className=\"h-4 w-4\" />\n              {error}\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        {/* Content */}\n        {loading ? (\n          <div className=\"flex-1 flex items-center justify-center\">\n            <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n          </div>\n        ) : (\n          <div className=\"flex-1 overflow-y-auto p-6\">\n            <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n              <TabsList className=\"grid grid-cols-3 w-full max-w-md mb-6 h-auto p-1\">\n                <TabsTrigger value=\"servers\" className=\"py-2.5 px-3\">\n                  Servers\n                </TabsTrigger>\n                <TabsTrigger value=\"add\" className=\"py-2.5 px-3\">\n                  Add Server\n                </TabsTrigger>\n                <TabsTrigger value=\"import\" className=\"py-2.5 px-3\">\n                  Import/Export\n                </TabsTrigger>\n              </TabsList>\n\n              {/* Servers Tab */}\n              <TabsContent value=\"servers\" className=\"space-y-6 mt-6\">\n                <Card>\n                  <MCPServerList\n                    servers={servers}\n                    loading={false}\n                    onServerRemoved={handleServerRemoved}\n                    onRefresh={loadServers}\n                  />\n                </Card>\n              </TabsContent>\n\n              {/* Add Server Tab */}\n              <TabsContent value=\"add\" className=\"space-y-6 mt-6\">\n                <Card>\n                  <MCPAddServer\n                    onServerAdded={handleServerAdded}\n                    onError={(message: string) => setToast({ message, type: \"error\" })}\n                  />\n                </Card>\n              </TabsContent>\n\n              {/* Import/Export Tab */}\n              <TabsContent value=\"import\" className=\"space-y-6 mt-6\">\n                <Card className=\"overflow-hidden\">\n                  <MCPImportExport\n                    onImportCompleted={handleImportCompleted}\n                    onError={(message: string) => setToast({ message, type: \"error\" })}\n                  />\n                </Card>\n              </TabsContent>\n            </Tabs>\n          </div>\n        )}\n      </div>\n\n      {/* Toast Notifications */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/MCPServerList.tsx",
    "content": "import React, { useState } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { \n  Network, \n  Globe, \n  Terminal, \n  Trash2, \n  Play, \n  CheckCircle,\n  Loader2,\n  RefreshCw,\n  FolderOpen,\n  User,\n  FileText,\n  ChevronDown,\n  ChevronUp,\n  Copy\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { api, type MCPServer } from \"@/lib/api\";\nimport { useTrackEvent } from \"@/hooks\";\n\ninterface MCPServerListProps {\n  /**\n   * List of MCP servers to display\n   */\n  servers: MCPServer[];\n  /**\n   * Whether the list is loading\n   */\n  loading: boolean;\n  /**\n   * Callback when a server is removed\n   */\n  onServerRemoved: (name: string) => void;\n  /**\n   * Callback to refresh the server list\n   */\n  onRefresh: () => void;\n}\n\n/**\n * Component for displaying a list of MCP servers\n * Shows servers grouped by scope with status indicators\n */\nexport const MCPServerList: React.FC<MCPServerListProps> = ({\n  servers,\n  loading,\n  onServerRemoved,\n  onRefresh,\n}) => {\n  const [removingServer, setRemovingServer] = useState<string | null>(null);\n  const [testingServer, setTestingServer] = useState<string | null>(null);\n  const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());\n  const [copiedServer, setCopiedServer] = useState<string | null>(null);\n  const [connectedServers] = useState<string[]>([]);\n  \n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n\n  // Group servers by scope\n  const serversByScope = servers.reduce((acc, server) => {\n    const scope = server.scope || \"local\";\n    if (!acc[scope]) acc[scope] = [];\n    acc[scope].push(server);\n    return acc;\n  }, {} as Record<string, MCPServer[]>);\n\n  /**\n   * Toggles expanded state for a server\n   */\n  const toggleExpanded = (serverName: string) => {\n    setExpandedServers(prev => {\n      const next = new Set(prev);\n      if (next.has(serverName)) {\n        next.delete(serverName);\n      } else {\n        next.add(serverName);\n      }\n      return next;\n    });\n  };\n\n  /**\n   * Copies command to clipboard\n   */\n  const copyCommand = async (command: string, serverName: string) => {\n    try {\n      await navigator.clipboard.writeText(command);\n      setCopiedServer(serverName);\n      setTimeout(() => setCopiedServer(null), 2000);\n    } catch (error) {\n      console.error(\"Failed to copy command:\", error);\n    }\n  };\n\n  /**\n   * Removes a server\n   */\n  const handleRemoveServer = async (name: string) => {\n    try {\n      setRemovingServer(name);\n      \n      // Check if server was connected\n      const wasConnected = connectedServers.includes(name);\n      \n      await api.mcpRemove(name);\n      \n      // Track server removal\n      trackEvent.mcpServerRemoved({\n        server_name: name,\n        was_connected: wasConnected\n      });\n      \n      onServerRemoved(name);\n    } catch (error) {\n      console.error(\"Failed to remove server:\", error);\n    } finally {\n      setRemovingServer(null);\n    }\n  };\n\n  /**\n   * Tests connection to a server\n   */\n  const handleTestConnection = async (name: string) => {\n    try {\n      setTestingServer(name);\n      const result = await api.mcpTestConnection(name);\n      const server = servers.find(s => s.name === name);\n      \n      // Track connection result - result is a string message\n      trackEvent.mcpServerConnected(name, true, server?.transport || 'unknown');\n      \n      // TODO: Show result in a toast or modal\n      console.log(\"Test result:\", result);\n    } catch (error) {\n      console.error(\"Failed to test connection:\", error);\n      \n      trackEvent.mcpConnectionError({\n        server_name: name,\n        error_type: 'test_failed',\n        retry_attempt: 0\n      });\n    } finally {\n      setTestingServer(null);\n    }\n  };\n\n  /**\n   * Gets icon for transport type\n   */\n  const getTransportIcon = (transport: string) => {\n    switch (transport) {\n      case \"stdio\":\n        return <Terminal className=\"h-4 w-4 text-amber-500\" />;\n      case \"sse\":\n        return <Globe className=\"h-4 w-4 text-emerald-500\" />;\n      default:\n        return <Network className=\"h-4 w-4 text-blue-500\" />;\n    }\n  };\n\n  /**\n   * Gets icon for scope\n   */\n  const getScopeIcon = (scope: string) => {\n    switch (scope) {\n      case \"local\":\n        return <User className=\"h-3 w-3 text-slate-500\" />;\n      case \"project\":\n        return <FolderOpen className=\"h-3 w-3 text-orange-500\" />;\n      case \"user\":\n        return <FileText className=\"h-3 w-3 text-purple-500\" />;\n      default:\n        return null;\n    }\n  };\n\n  /**\n   * Gets scope display name\n   */\n  const getScopeDisplayName = (scope: string) => {\n    switch (scope) {\n      case \"local\":\n        return \"Local (Project-specific)\";\n      case \"project\":\n        return \"Project (Shared via .mcp.json)\";\n      case \"user\":\n        return \"User (All projects)\";\n      default:\n        return scope;\n    }\n  };\n\n  /**\n   * Renders a single server item\n   */\n  const renderServerItem = (server: MCPServer) => {\n    const isExpanded = expandedServers.has(server.name);\n    const isCopied = copiedServer === server.name;\n    \n    return (\n      <motion.div\n        key={server.name}\n        initial={{ opacity: 0, x: -20 }}\n        animate={{ opacity: 1, x: 0 }}\n        exit={{ opacity: 0, x: -20 }}\n        className=\"group p-4 rounded-lg border border-border bg-card hover:bg-accent/5 hover:border-primary/20 transition-all overflow-hidden\"\n      >\n        <div className=\"space-y-2\">\n          <div className=\"flex items-start justify-between gap-4\">\n            <div className=\"flex-1 min-w-0 space-y-1\">\n              <div className=\"flex items-center gap-2\">\n                <div className=\"p-1.5 bg-primary/10 rounded\">\n                  {getTransportIcon(server.transport)}\n                </div>\n                <h4 className=\"font-medium truncate\">{server.name}</h4>\n                {server.status?.running && (\n                  <Badge variant=\"outline\" className=\"gap-1 flex-shrink-0 border-green-500/50 text-green-600 bg-green-500/10\">\n                    <CheckCircle className=\"h-3 w-3\" />\n                    Running\n                  </Badge>\n                )}\n              </div>\n              \n              {server.command && !isExpanded && (\n                <div className=\"flex items-center gap-2\">\n                  <p className=\"text-xs text-muted-foreground font-mono truncate pl-9 flex-1\" title={server.command}>\n                    {server.command}\n                  </p>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={() => toggleExpanded(server.name)}\n                    className=\"h-6 px-2 text-xs hover:bg-primary/10\"\n                  >\n                    <ChevronDown className=\"h-3 w-3 mr-1\" />\n                    Show full\n                  </Button>\n                </div>\n              )}\n              \n              {server.transport === \"sse\" && server.url && !isExpanded && (\n                <div className=\"overflow-hidden\">\n                  <p className=\"text-xs text-muted-foreground font-mono truncate pl-9\" title={server.url}>\n                    {server.url}\n                  </p>\n                </div>\n              )}\n              \n              {Object.keys(server.env).length > 0 && !isExpanded && (\n                <div className=\"flex items-center gap-1 text-xs text-muted-foreground pl-9\">\n                  <span>Environment variables: {Object.keys(server.env).length}</span>\n                </div>\n              )}\n            </div>\n            \n            <div className=\"flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0\">\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => handleTestConnection(server.name)}\n                disabled={testingServer === server.name}\n                className=\"hover:bg-green-500/10 hover:text-green-600\"\n              >\n                {testingServer === server.name ? (\n                  <Loader2 className=\"h-4 w-4 animate-spin\" />\n                ) : (\n                  <Play className=\"h-4 w-4\" />\n                )}\n              </Button>\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => handleRemoveServer(server.name)}\n                disabled={removingServer === server.name}\n                className=\"hover:bg-destructive/10 hover:text-destructive\"\n              >\n                {removingServer === server.name ? (\n                  <Loader2 className=\"h-4 w-4 animate-spin\" />\n                ) : (\n                  <Trash2 className=\"h-4 w-4\" />\n                )}\n              </Button>\n            </div>\n          </div>\n          \n          {/* Expanded Details */}\n          {isExpanded && (\n            <motion.div\n              initial={{ height: 0, opacity: 0 }}\n              animate={{ height: \"auto\", opacity: 1 }}\n              exit={{ height: 0, opacity: 0 }}\n              transition={{ duration: 0.2 }}\n              className=\"pl-9 space-y-3 pt-2 border-t border-border/50\"\n            >\n              {server.command && (\n                <div className=\"space-y-1\">\n                  <div className=\"flex items-center justify-between\">\n                    <p className=\"text-xs font-medium text-muted-foreground\">Command</p>\n                    <div className=\"flex items-center gap-1\">\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() => copyCommand(server.command!, server.name)}\n                        className=\"h-6 px-2 text-xs hover:bg-primary/10\"\n                      >\n                        <Copy className=\"h-3 w-3 mr-1\" />\n                        {isCopied ? \"Copied!\" : \"Copy\"}\n                      </Button>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() => toggleExpanded(server.name)}\n                        className=\"h-6 px-2 text-xs hover:bg-primary/10\"\n                      >\n                        <ChevronUp className=\"h-3 w-3 mr-1\" />\n                        Hide\n                      </Button>\n                    </div>\n                  </div>\n                  <p className=\"text-xs font-mono bg-muted/50 p-2 rounded break-all\">\n                    {server.command}\n                  </p>\n                </div>\n              )}\n              \n              {server.args && server.args.length > 0 && (\n                <div className=\"space-y-1\">\n                  <p className=\"text-xs font-medium text-muted-foreground\">Arguments</p>\n                  <div className=\"text-xs font-mono bg-muted/50 p-2 rounded space-y-1\">\n                    {server.args.map((arg, idx) => (\n                      <div key={idx} className=\"break-all\">\n                        <span className=\"text-muted-foreground mr-2\">[{idx}]</span>\n                        {arg}\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n              \n              {server.transport === \"sse\" && server.url && (\n                <div className=\"space-y-1\">\n                  <p className=\"text-xs font-medium text-muted-foreground\">URL</p>\n                  <p className=\"text-xs font-mono bg-muted/50 p-2 rounded break-all\">\n                    {server.url}\n                  </p>\n                </div>\n              )}\n              \n              {Object.keys(server.env).length > 0 && (\n                <div className=\"space-y-1\">\n                  <p className=\"text-xs font-medium text-muted-foreground\">Environment Variables</p>\n                  <div className=\"text-xs font-mono bg-muted/50 p-2 rounded space-y-1\">\n                    {Object.entries(server.env).map(([key, value]) => (\n                      <div key={key} className=\"break-all\">\n                        <span className=\"text-primary\">{key}</span>\n                        <span className=\"text-muted-foreground mx-1\">=</span>\n                        <span>{value}</span>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n            </motion.div>\n          )}\n        </div>\n      </motion.div>\n    );\n  };\n\n  if (loading) {\n    return (\n      <div className=\"flex items-center justify-center h-64\">\n        <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"p-6\">\n      {/* Header */}\n      <div className=\"flex items-center justify-between mb-6\">\n        <div>\n          <h3 className=\"text-base font-semibold\">Configured Servers</h3>\n          <p className=\"text-sm text-muted-foreground\">\n            {servers.length} server{servers.length !== 1 ? \"s\" : \"\"} configured\n          </p>\n        </div>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={onRefresh}\n          className=\"gap-2 hover:bg-primary/10 hover:text-primary hover:border-primary/50\"\n        >\n          <RefreshCw className=\"h-4 w-4\" />\n          Refresh\n        </Button>\n      </div>\n\n      {/* Server List */}\n      {servers.length === 0 ? (\n        <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n          <div className=\"p-4 bg-primary/10 rounded-full mb-4\">\n            <Network className=\"h-12 w-12 text-primary\" />\n          </div>\n          <p className=\"text-muted-foreground mb-2 font-medium\">No MCP servers configured</p>\n          <p className=\"text-sm text-muted-foreground\">\n            Add a server to get started with Model Context Protocol\n          </p>\n        </div>\n      ) : (\n        <div className=\"space-y-6\">\n          {Object.entries(serversByScope).map(([scope, scopeServers]) => (\n            <div key={scope} className=\"space-y-3\">\n              <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n                {getScopeIcon(scope)}\n                <span className=\"font-medium\">{getScopeDisplayName(scope)}</span>\n                <span className=\"text-muted-foreground/60\">({scopeServers.length})</span>\n              </div>\n              <AnimatePresence>\n                <div className=\"space-y-2\">\n                  {scopeServers.map(renderServerItem)}\n                </div>\n              </AnimatePresence>\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/MarkdownEditor.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport MDEditor from \"@uiw/react-md-editor\";\nimport { motion } from \"framer-motion\";\nimport { Save, Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { api } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\ninterface MarkdownEditorProps {\n  /**\n   * Callback to go back to the main view\n   */\n  onBack: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * MarkdownEditor component for editing the CLAUDE.md system prompt\n * \n * @example\n * <MarkdownEditor onBack={() => setView('main')} />\n */\nexport const MarkdownEditor: React.FC<MarkdownEditorProps> = ({\n  className,\n}) => {\n  const [content, setContent] = useState<string>(\"\");\n  const [originalContent, setOriginalContent] = useState<string>(\"\");\n  const [loading, setLoading] = useState(true);\n  const [saving, setSaving] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  \n  const hasChanges = content !== originalContent;\n  \n  // Load the system prompt on mount\n  useEffect(() => {\n    loadSystemPrompt();\n  }, []);\n  \n  const loadSystemPrompt = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const prompt = await api.getSystemPrompt();\n      setContent(prompt);\n      setOriginalContent(prompt);\n    } catch (err) {\n      console.error(\"Failed to load system prompt:\", err);\n      setError(\"Failed to load CLAUDE.md file\");\n    } finally {\n      setLoading(false);\n    }\n  };\n  \n  const handleSave = async () => {\n    try {\n      setSaving(true);\n      setError(null);\n      setToast(null);\n      await api.saveSystemPrompt(content);\n      setOriginalContent(content);\n      setToast({ message: \"CLAUDE.md saved successfully\", type: \"success\" });\n    } catch (err) {\n      console.error(\"Failed to save system prompt:\", err);\n      setError(\"Failed to save CLAUDE.md file\");\n      setToast({ message: \"Failed to save CLAUDE.md\", type: \"error\" });\n    } finally {\n      setSaving(false);\n    }\n  };\n  \n  \n  return (\n    <div className={cn(\"h-full overflow-y-auto\", className)}>\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <h1 className=\"text-3xl font-bold tracking-tight\">CLAUDE.md</h1>\n              <p className=\"mt-1 text-sm text-muted-foreground\">\n                Edit your Claude Code system prompt\n              </p>\n            </div>\n            <Button\n              onClick={handleSave}\n              disabled={!hasChanges || saving}\n              size=\"default\"\n            >\n              {saving ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  Saving...\n                </>\n              ) : (\n                <>\n                  <Save className=\"mr-2 h-4 w-4\" />\n                  Save\n                </>\n              )}\n            </Button>\n          </div>\n        </div>\n        \n        {/* Error display */}\n        {error && (\n          <motion.div\n            initial={{ opacity: 0, y: -10 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -10 }}\n            className=\"mx-6 mb-4 p-3 rounded-lg bg-destructive/10 border border-destructive/50 text-sm text-destructive\"\n          >\n            {error}\n          </motion.div>\n        )}\n        \n        {/* Content */}\n        <div className=\"flex-1 overflow-hidden p-6\">\n          {loading ? (\n            <div className=\"flex items-center justify-center h-64\">\n              <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n            </div>\n          ) : (\n            <div className=\"h-full rounded-lg border border-border overflow-hidden shadow-sm\" data-color-mode=\"dark\">\n              <MDEditor\n                value={content}\n                onChange={(val) => setContent(val || \"\")}\n                preview=\"edit\"\n                height=\"100%\"\n                visibleDragbar={false}\n              />\n            </div>\n          )}\n        </div>\n      </div>\n      \n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/NFOCredits.tsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { X, Volume2, VolumeX, Github } from \"lucide-react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { openUrl } from \"@tauri-apps/plugin-opener\";\nimport asteriskLogo from \"@/assets/nfo/asterisk-logo.png\";\nimport keygennMusic from \"@/assets/nfo/opcode-nfo.ogg\";\n\ninterface NFOCreditsProps {\n  /**\n   * Callback when the NFO window is closed\n   */\n  onClose: () => void;\n}\n\n/**\n * NFO Credits component - Displays a keygen/crack style credits window\n * with auto-scrolling text, retro fonts, and background music\n * \n * @example\n * <NFOCredits onClose={() => setShowNFO(false)} />\n */\nexport const NFOCredits: React.FC<NFOCreditsProps> = ({ onClose }) => {\n  const audioRef = useRef<HTMLAudioElement | null>(null);\n  const scrollRef = useRef<HTMLDivElement | null>(null);\n  const [isMuted, setIsMuted] = useState(false);\n  const [scrollPosition, setScrollPosition] = useState(0);\n  \n  // Initialize and autoplay audio muted then unmute\n  useEffect(() => {\n    const audio = new Audio(keygennMusic);\n    audio.loop = true;\n    audio.volume = 0.7;\n    // Start muted to satisfy autoplay policy\n    audio.muted = true;\n    audioRef.current = audio;\n    // Attempt to play\n    audio.play().then(() => {\n      // Unmute after autoplay\n      audio.muted = false;\n    }).catch(err => {\n      console.error(\"Audio autoplay failed:\", err);\n    });\n    return () => {\n      if (audioRef.current) {\n        audioRef.current.pause();\n        audioRef.current.src = '';\n        audioRef.current = null;\n      }\n    };\n  }, []);\n  \n  // Handle mute toggle\n  const toggleMute = () => {\n    if (audioRef.current) {\n      audioRef.current.muted = !isMuted;\n      setIsMuted(!isMuted);\n    }\n  };\n  \n  // Start auto-scrolling\n  useEffect(() => {\n    const scrollInterval = setInterval(() => {\n      setScrollPosition(prev => prev + 1);\n    }, 30); // Smooth scrolling speed\n    \n    return () => clearInterval(scrollInterval);\n  }, []);\n  \n  // Apply scroll position\n  useEffect(() => {\n    if (scrollRef.current) {\n      const maxScroll = scrollRef.current.scrollHeight - scrollRef.current.clientHeight;\n      if (scrollPosition >= maxScroll) {\n        // Reset to beginning when reaching the end\n        setScrollPosition(0);\n        scrollRef.current.scrollTop = 0;\n      } else {\n        scrollRef.current.scrollTop = scrollPosition;\n      }\n    }\n  }, [scrollPosition]);\n  \n  // Credits content\n  const creditsContent = [\n    { type: \"header\", text: \"opcode v0.2.1\" },\n    { type: \"subheader\", text: \"[ A STRATEGIC PROJECT BY ASTERISK ]\" },\n    { type: \"spacer\" },\n    { type: \"section\", title: \"━━━ CREDITS ━━━\" },\n    { type: \"credit\", role: \"POWERED BY\", name: \"Anthropic Claude 4\" },\n    { type: \"credit\", role: \"CLAUDE CODE\", name: \"The Ultimate Coding Assistant\" },\n    { type: \"credit\", role: \"MCP PROTOCOL\", name: \"Model Context Protocol\" },\n    { type: \"spacer\" },\n    { type: \"section\", title: \"━━━ DEPENDENCIES ━━━\" },\n    { type: \"credit\", role: \"RUNTIME\", name: \"Tauri Framework\" },\n    { type: \"credit\", role: \"UI FRAMEWORK\", name: \"React + TypeScript\" },\n    { type: \"credit\", role: \"STYLING\", name: \"Tailwind CSS + shadcn/ui\" },\n    { type: \"credit\", role: \"ANIMATIONS\", name: \"Framer Motion\" },\n    { type: \"credit\", role: \"BUILD TOOL\", name: \"Vite\" },\n    { type: \"credit\", role: \"PACKAGE MANAGER\", name: \"Bun\" },\n    { type: \"spacer\" },\n    { type: \"section\", title: \"━━━ SPECIAL THANKS ━━━\" },\n    { type: \"text\", content: \"To the open source community\" },\n    { type: \"text\", content: \"To all the beta testers\" },\n    { type: \"text\", content: \"To everyone who believed in this project\" },\n    { type: \"spacer\" },\n    { type: \"ascii\", content: `\n     ▄▄▄· .▄▄ · ▄▄▄▄▄▄▄▄ .▄▄▄  ▪  .▄▄ · ▄ •▄ \n    ▐█ ▀█ ▐█ ▀. •██  ▀▄.▀·▀▄ █·██ ▐█ ▀. █▌▄▌▪\n    ▄█▀▀█ ▄▀▀▀█▄ ▐█.▪▐▀▀▪▄▐▀▀▄ ▐█·▄▀▀▀█▄▐▀▀▄·\n    ▐█ ▪▐▌▐█▄▪▐█ ▐█▌·▐█▄▄▌▐█•█▌▐█▌▐█▄▪▐█▐█.█▌\n     ▀  ▀  ▀▀▀▀  ▀▀▀  ▀▀▀ .▀  ▀▀▀▀ ▀▀▀▀ ·▀  ▀\n    ` },\n    { type: \"spacer\" },\n    { type: \"text\", content: \"Remember: Sharing is caring!\" },\n    { type: \"text\", content: \"Support the developers!\" },\n    { type: \"spacer\" },\n    { type: \"spacer\" },\n    { type: \"spacer\" },\n  ];\n  \n  return (\n    <AnimatePresence>\n      <motion.div\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        exit={{ opacity: 0 }}\n        className=\"fixed inset-0 z-50 flex items-center justify-center\"\n      >\n        {/* Backdrop with blur */}\n        <div \n          className=\"absolute inset-0 bg-black/80 backdrop-blur-md\"\n          onClick={onClose}\n        />\n        \n        {/* NFO Window */}\n        <motion.div\n          initial={{ scale: 0.8, opacity: 0 }}\n          animate={{ scale: 1, opacity: 1 }}\n          exit={{ scale: 0.8, opacity: 0 }}\n          transition={{ type: \"spring\", damping: 25, stiffness: 300 }}\n          className=\"relative z-10\"\n        >\n          <Card className=\"w-[600px] h-[500px] bg-background border-border shadow-2xl overflow-hidden\">\n            {/* Window Header */}\n            <div className=\"flex items-center justify-between px-4 py-2 bg-card border-b border-border\">\n              <div className=\"flex items-center space-x-2\">\n                <div className=\"text-sm font-bold tracking-wider font-mono text-foreground\">\n                  opcode.NFO\n                </div>\n              </div>\n              <div className=\"flex items-center space-x-2\">\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={async (e) => {\n                    e.stopPropagation();\n                    await openUrl(\"https://github.com/getAsterisk/opcode/issues/new\");\n                  }}\n                  className=\"flex items-center gap-1 h-auto px-2 py-1\"\n                  title=\"File a bug\"\n                >\n                  <Github className=\"h-3 w-3\" />\n                  <span className=\"text-xs\">File a bug</span>\n                </Button>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    toggleMute();\n                  }}\n                  className=\"h-6 w-6 p-0\"\n                >\n                  {isMuted ? <VolumeX className=\"h-4 w-4\" /> : <Volume2 className=\"h-4 w-4\" />}\n                </Button>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    onClose();\n                  }}\n                  className=\"h-6 w-6 p-0\"\n                >\n                  <X className=\"h-4 w-4\" />\n                </Button>\n              </div>\n            </div>\n            \n            {/* NFO Content */}\n            <div className=\"relative h-[calc(100%-40px)] bg-background overflow-hidden\">\n              {/* Asterisk Logo Section (Fixed at top) */}\n              <div className=\"absolute top-0 left-0 right-0 bg-background z-10 pb-4 text-center\">\n                <button\n                  className=\"inline-block mt-4 hover:scale-110 transition-transform cursor-pointer\"\n                  onClick={async (e) => {\n                    e.stopPropagation();\n                    await openUrl(\"https://asterisk.so\");\n                  }}\n                >\n                  <img \n                    src={asteriskLogo} \n                    alt=\"Asterisk\" \n                    className=\"h-20 w-auto mx-auto filter brightness-0 invert opacity-90\"\n                  />\n                </button>\n                <div className=\"text-muted-foreground text-sm font-mono mt-2 tracking-wider\">\n                  A strategic project by Asterisk\n                </div>\n              </div>\n              \n              {/* Scrolling Credits */}\n              <div \n                ref={scrollRef}\n                className=\"absolute inset-0 top-32 overflow-hidden\"\n                style={{ fontFamily: \"'Courier New', monospace\" }}\n              >\n                <div className=\"px-8 pb-32\">\n                  {creditsContent.map((item, index) => {\n                    switch (item.type) {\n                      case \"header\":\n                        return (\n                          <div \n                            key={index} \n                            className=\"text-foreground text-3xl font-bold text-center mb-2 tracking-widest\"\n                          >\n                            {item.text}\n                          </div>\n                        );\n                      case \"subheader\":\n                        return (\n                          <div \n                            key={index} \n                            className=\"text-muted-foreground text-lg text-center mb-8 tracking-wide\"\n                          >\n                            {item.text}\n                          </div>\n                        );\n                      case \"section\":\n                        return (\n                          <div \n                            key={index} \n                            className=\"text-foreground text-xl font-bold text-center my-6 tracking-wider\"\n                          >\n                            {item.title}\n                          </div>\n                        );\n                      case \"credit\":\n                        return (\n                          <div \n                            key={index} \n                            className=\"flex justify-between items-center mb-2 text-foreground\"\n                          >\n                            <span className=\"text-sm text-muted-foreground\">{item.role}:</span>\n                            <span className=\"text-base tracking-wide\">{item.name}</span>\n                          </div>\n                        );\n                      case \"text\":\n                        return (\n                          <div \n                            key={index} \n                            className=\"text-muted-foreground text-center text-sm mb-2\"\n                          >\n                            {item.content}\n                          </div>\n                        );\n                      case \"ascii\":\n                        return (\n                          <pre \n                            key={index} \n                            className=\"text-foreground text-xs text-center my-6 leading-tight opacity-80\"\n                          >\n                            {item.content}\n                          </pre>\n                        );\n                      case \"spacer\":\n                        return <div key={index} className=\"h-8\" />;\n                      default:\n                        return null;\n                    }\n                  })}\n                </div>\n              </div>\n              \n              {/* Subtle Scanlines Effect */}\n              <div className=\"absolute inset-0 pointer-events-none\">\n                <div className=\"absolute inset-0 bg-gradient-to-b from-transparent via-foreground/[0.02] to-transparent animate-scanlines\" />\n              </div>\n            </div>\n          </Card>\n        </motion.div>\n      </motion.div>\n    </AnimatePresence>\n  );\n}; \n"
  },
  {
    "path": "src/components/PreviewPromptDialog.tsx",
    "content": "import React from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Globe, ExternalLink } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\n\ninterface PreviewPromptDialogProps {\n  /**\n   * Whether the dialog is open\n   */\n  isOpen: boolean;\n  /**\n   * The detected URL to preview\n   */\n  url: string;\n  /**\n   * Callback when user confirms opening preview\n   */\n  onConfirm: () => void;\n  /**\n   * Callback when user cancels\n   */\n  onCancel: () => void;\n}\n\n/**\n * Dialog component that prompts the user to open a detected URL in the preview pane\n * \n * @example\n * <PreviewPromptDialog\n *   isOpen={showPrompt}\n *   url=\"http://localhost:3000\"\n *   onConfirm={() => openPreview(url)}\n *   onCancel={() => setShowPrompt(false)}\n * />\n */\nexport const PreviewPromptDialog: React.FC<PreviewPromptDialogProps> = ({\n  isOpen,\n  url,\n  onConfirm,\n  onCancel,\n}) => {\n  // Extract domain for display\n  const getDomain = (urlString: string) => {\n    try {\n      const urlObj = new URL(urlString);\n      return urlObj.hostname;\n    } catch {\n      return urlString;\n    }\n  };\n\n  const domain = getDomain(url);\n  const isLocalhost = domain.includes('localhost') || domain.includes('127.0.0.1');\n\n  return (\n    <Dialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle className=\"flex items-center gap-2\">\n            <Globe className=\"h-5 w-5 text-primary\" />\n            Open Preview?\n          </DialogTitle>\n          <DialogDescription>\n            A URL was detected in the terminal output. Would you like to open it in the preview pane?\n          </DialogDescription>\n        </DialogHeader>\n        \n        <div className=\"py-4\">\n          <div className=\"rounded-lg border bg-muted/50 p-4\">\n            <div className=\"flex items-start gap-3\">\n              <ExternalLink className={`h-4 w-4 mt-0.5 ${isLocalhost ? 'text-green-500' : 'text-blue-500'}`} />\n              <div className=\"flex-1 min-w-0\">\n                <p className=\"text-sm font-medium\">\n                  {isLocalhost ? 'Local Development Server' : 'External URL'}\n                </p>\n                <p className=\"text-xs text-muted-foreground mt-1 break-all\">\n                  {url}\n                </p>\n              </div>\n            </div>\n          </div>\n          \n          <motion.div\n            initial={{ opacity: 0, y: 10 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ delay: 0.1 }}\n            className=\"mt-3 text-xs text-muted-foreground\"\n          >\n            The preview will open in a split view on the right side of the screen.\n          </motion.div>\n        </div>\n        \n        <DialogFooter>\n          <Button variant=\"outline\" onClick={onCancel}>\n            Cancel\n          </Button>\n          <Button onClick={onConfirm} className=\"gap-2\">\n            <ExternalLink className=\"h-4 w-4\" />\n            Open Preview\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}; "
  },
  {
    "path": "src/components/ProjectList.tsx",
    "content": "import React, { useState } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { \n  FolderOpen,\n  ChevronLeft,\n  ChevronRight\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport type { Project } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ProjectListProps {\n  /**\n   * Array of projects to display\n   */\n  projects: Project[];\n  /**\n   * Callback when a project is clicked\n   */\n  onProjectClick: (project: Project) => void;\n  /**\n   * Callback when open project is clicked\n   */\n  onOpenProject?: () => void | Promise<void>;\n  /**\n   * Whether the list is currently loading\n   */\n  loading?: boolean;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * Extracts the project name from the full path\n */\nconst getProjectName = (path: string): string => {\n  const parts = path.split('/').filter(Boolean);\n  return parts[parts.length - 1] || path;\n};\n\n/**\n * Formats path to be more readable - shows full path relative to home\n * Truncates long paths with ellipsis in the middle\n */\nconst getDisplayPath = (path: string, maxLength: number = 30): string => {\n  // Try to make path home-relative\n  let displayPath = path;\n  const homeIndicators = ['/Users/', '/home/'];\n  for (const indicator of homeIndicators) {\n    if (path.includes(indicator)) {\n      const parts = path.split('/');\n      const userIndex = parts.findIndex((_part, i) => \n        i > 0 && parts[i - 1] === indicator.split('/')[1]\n      );\n      if (userIndex > 0) {\n        const relativePath = parts.slice(userIndex + 1).join('/');\n        displayPath = `~/${relativePath}`;\n        break;\n      }\n    }\n  }\n  \n  // Truncate if too long\n  if (displayPath.length > maxLength) {\n    const start = displayPath.substring(0, Math.floor(maxLength / 2) - 2);\n    const end = displayPath.substring(displayPath.length - Math.floor(maxLength / 2) + 2);\n    return `${start}...${end}`;\n  }\n  \n  return displayPath;\n};\n\n/**\n * ProjectList component - Displays recent projects in a Cursor-like interface\n * \n * @example\n * <ProjectList\n *   projects={projects}\n *   onProjectClick={(project) => console.log('Selected:', project)}\n *   onOpenProject={() => console.log('Open project')}\n * />\n */\nexport const ProjectList: React.FC<ProjectListProps> = ({\n  projects,\n  onProjectClick,\n  onOpenProject,\n  className,\n}) => {\n  const [showAll, setShowAll] = useState(false);\n  const [currentPage, setCurrentPage] = useState(1);\n  \n  // Determine how many projects to show\n  const projectsPerPage = showAll ? 10 : 5;\n  const totalPages = Math.ceil(projects.length / projectsPerPage);\n  \n  // Calculate which projects to display\n  const startIndex = showAll ? (currentPage - 1) * projectsPerPage : 0;\n  const endIndex = startIndex + projectsPerPage;\n  const displayedProjects = projects.slice(startIndex, endIndex);\n  \n  const handleViewAll = () => {\n    setShowAll(true);\n    setCurrentPage(1);\n  };\n  \n  const handleViewLess = () => {\n    setShowAll(false);\n    setCurrentPage(1);\n  };\n\n  return (\n    <div className={cn(\"h-full overflow-y-auto\", className)}>\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <h1 className=\"text-3xl font-bold\">Projects</h1>\n              <p className=\"mt-1 text-body-small text-muted-foreground\">\n                Select a project to start working with Claude Code\n              </p>\n            </div>\n            <motion.div\n              whileTap={{ scale: 0.97 }}\n              transition={{ duration: 0.15 }}\n            >\n              <Button\n                onClick={onOpenProject}\n                size=\"default\"\n                className=\"flex items-center gap-2\"\n              >\n                <FolderOpen className=\"h-4 w-4\" />\n                Open Project\n              </Button>\n            </motion.div>\n          </div>\n        </div>\n\n        {/* Content */}\n        <div className=\"flex-1 overflow-y-auto p-6\">\n          {/* Recent projects section */}\n          {displayedProjects.length > 0 ? (\n            <Card className=\"p-6\">\n              <div className=\"flex items-center justify-between mb-4\">\n                <h2 className=\"text-heading-4\">Recent Projects</h2>\n            {!showAll ? (\n              <button \n                onClick={handleViewAll}\n                className=\"text-caption text-muted-foreground hover:text-foreground transition-colors\"\n              >\n                View all ({projects.length})\n              </button>\n            ) : (\n              <button \n                onClick={handleViewLess}\n                className=\"text-caption text-muted-foreground hover:text-foreground transition-colors\"\n              >\n                View less\n              </button>\n            )}\n          </div>\n          \n          <div className=\"space-y-1\">\n            {displayedProjects.map((project, index) => (\n              <motion.div\n                key={project.id}\n                initial={{ opacity: 0, y: 4 }}\n                animate={{ opacity: 1, y: 0 }}\n                transition={{\n                  duration: 0.15,\n                  delay: index * 0.02,\n                }}\n                className=\"group\"\n              >\n                <motion.button\n                  onClick={() => onProjectClick(project)}\n                  whileTap={{ scale: 0.97 }}\n                  transition={{ duration: 0.15 }}\n                  className=\"w-full text-left px-3 py-2 rounded-md hover:bg-accent/50 transition-colors flex items-center justify-between\"\n                >\n                  <span className=\"text-body-small font-medium\">\n                    {getProjectName(project.path)}\n                  </span>\n                  <span className=\"text-caption text-muted-foreground font-mono text-right\" style={{ minWidth: '200px' }}>\n                    {getDisplayPath(project.path, 35)}\n                  </span>\n                </motion.button>\n              </motion.div>\n            ))}\n          </div>\n          \n          {/* Pagination controls */}\n          {showAll && totalPages > 1 && (\n            <div className=\"flex items-center justify-center gap-2 mt-6\">\n              <motion.div\n                whileTap={{ scale: 0.97 }}\n                transition={{ duration: 0.15 }}\n              >\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}\n                  disabled={currentPage === 1}\n                >\n                  <ChevronLeft className=\"h-4 w-4\" />\n                </Button>\n              </motion.div>\n              \n              <div className=\"flex items-center gap-1\">\n                {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (\n                  <Button\n                    key={page}\n                    variant={currentPage === page ? \"default\" : \"ghost\"}\n                    size=\"sm\"\n                    onClick={() => setCurrentPage(page)}\n                    className=\"w-8 h-8 p-0\"\n                  >\n                    {page}\n                  </Button>\n                ))}\n              </div>\n              \n              <motion.div\n                whileTap={{ scale: 0.97 }}\n                transition={{ duration: 0.15 }}\n              >\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}\n                  disabled={currentPage === totalPages}\n                >\n                  <ChevronRight className=\"h-4 w-4\" />\n                </Button>\n              </motion.div>\n            </div>\n          )}\n            </Card>\n          ) : (\n            <Card className=\"p-12\">\n              <div className=\"flex flex-col items-center justify-center text-center\">\n                <div className=\"w-16 h-16 bg-primary/10 rounded-lg flex items-center justify-center mb-4\">\n                  <FolderOpen className=\"h-8 w-8 text-primary\" />\n                </div>\n                <h3 className=\"text-heading-3 mb-2\">No recent projects</h3>\n                <p className=\"text-body-small text-muted-foreground mb-6\">\n                  Open a project to get started with Claude Code\n                </p>\n                <motion.div\n                  whileTap={{ scale: 0.97 }}\n                  transition={{ duration: 0.15 }}\n                >\n                  <Button\n                    onClick={onOpenProject}\n                    size=\"default\"\n                    className=\"flex items-center gap-2\"\n                  >\n                    <FolderOpen className=\"h-4 w-4\" />\n                    Open Your First Project\n                  </Button>\n                </motion.div>\n              </div>\n            </Card>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}; \n"
  },
  {
    "path": "src/components/ProjectSettings.tsx",
    "content": "/**\n * ProjectSettings component for managing project-specific hooks configuration\n */\n\nimport React, { useState, useEffect } from 'react';\nimport { HooksEditor } from '@/components/HooksEditor';\nimport { SlashCommandsManager } from '@/components/SlashCommandsManager';\nimport { api } from '@/lib/api';\nimport { \n  AlertTriangle, \n  ArrowLeft, \n  Settings,\n  FolderOpen,\n  GitBranch,\n  Shield,\n  Command\n} from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Card } from '@/components/ui/card';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport { cn } from '@/lib/utils';\nimport { Toast, ToastContainer } from '@/components/ui/toast';\nimport type { Project } from '@/lib/api';\n\ninterface ProjectSettingsProps {\n  project: Project;\n  onBack: () => void;\n  className?: string;\n}\n\nexport const ProjectSettings: React.FC<ProjectSettingsProps> = ({\n  project,\n  onBack,\n  className\n}) => {\n  const [activeTab, setActiveTab] = useState('commands');\n  const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);\n  \n  // Other hooks settings\n  const [gitIgnoreLocal, setGitIgnoreLocal] = useState(true);\n\n  useEffect(() => {\n    checkGitIgnore();\n  }, [project]);\n\n  const checkGitIgnore = async () => {\n    try {\n      // Check if .claude/settings.local.json is in .gitignore\n      const gitignorePath = `${project.path}/.gitignore`;\n      const gitignoreContent = await api.readClaudeMdFile(gitignorePath);\n      setGitIgnoreLocal(gitignoreContent.includes('.claude/settings.local.json'));\n    } catch {\n      // .gitignore might not exist\n      setGitIgnoreLocal(false);\n    }\n  };\n\n  const addToGitIgnore = async () => {\n    try {\n      const gitignorePath = `${project.path}/.gitignore`;\n      let content = '';\n      \n      try {\n        content = await api.readClaudeMdFile(gitignorePath);\n      } catch {\n        // File doesn't exist, create it\n      }\n      \n      if (!content.includes('.claude/settings.local.json')) {\n        content += '\\n# Claude local settings (machine-specific)\\n.claude/settings.local.json\\n';\n        await api.saveClaudeMdFile(gitignorePath, content);\n        setGitIgnoreLocal(true);\n        setToast({ message: 'Added to .gitignore', type: 'success' });\n      }\n    } catch (err) {\n      console.error('Failed to update .gitignore:', err);\n      setToast({ message: 'Failed to update .gitignore', type: 'error' });\n    }\n  };\n\n  return (\n    <div className={cn(\"flex flex-col h-full\", className)}>\n      {/* Header */}\n      <div className=\"border-b px-6 py-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-4\">\n            <Button variant=\"ghost\" size=\"sm\" onClick={onBack}>\n              <ArrowLeft className=\"h-4 w-4 mr-2\" />\n              Back\n            </Button>\n            <div className=\"flex items-center gap-2\">\n              <Settings className=\"h-5 w-5 text-muted-foreground\" />\n              <h2 className=\"text-xl font-semibold\">Project Settings</h2>\n            </div>\n          </div>\n        </div>\n        \n        <div className=\"mt-4 flex items-center gap-4 text-sm text-muted-foreground\">\n          <div className=\"flex items-center gap-2\">\n            <FolderOpen className=\"h-4 w-4\" />\n            <span className=\"font-mono\">{project.path}</span>\n          </div>\n        </div>\n      </div>\n\n      {/* Content */}\n      <div className=\"flex-1 overflow-y-auto\">\n        <div className=\"p-6\">\n          <Tabs value={activeTab} onValueChange={setActiveTab}>\n            <TabsList className=\"mb-6\">\n              <TabsTrigger value=\"commands\" className=\"gap-2\">\n                <Command className=\"h-4 w-4\" />\n                Slash Commands\n              </TabsTrigger>\n              <TabsTrigger value=\"project\" className=\"gap-2\">\n                <GitBranch className=\"h-4 w-4\" />\n                Project Hooks\n              </TabsTrigger>\n              <TabsTrigger value=\"local\" className=\"gap-2\">\n                <Shield className=\"h-4 w-4\" />\n                Local Hooks\n              </TabsTrigger>\n            </TabsList>\n\n            <TabsContent value=\"commands\" className=\"space-y-6\">\n              <Card className=\"p-6\">\n                <div className=\"space-y-4\">\n                  <div>\n                    <h3 className=\"text-lg font-semibold mb-2\">Project Slash Commands</h3>\n                    <p className=\"text-sm text-muted-foreground mb-4\">\n                      Custom commands that are specific to this project. These commands are stored in\n                      <code className=\"mx-1 px-2 py-1 bg-muted rounded text-xs\">.claude/slash-commands/</code>\n                      and can be committed to version control.\n                    </p>\n                  </div>\n                  \n                  <SlashCommandsManager\n                    projectPath={project.path}\n                    scopeFilter=\"project\"\n                  />\n                </div>\n              </Card>\n            </TabsContent>\n\n            <TabsContent value=\"project\" className=\"space-y-6\">\n              <Card className=\"p-6\">\n                <div className=\"space-y-4\">\n                  <div>\n                    <h3 className=\"text-lg font-semibold mb-2\">Project Hooks</h3>\n                    <p className=\"text-sm text-muted-foreground mb-4\">\n                      These hooks apply to all users working on this project. They are stored in\n                      <code className=\"mx-1 px-2 py-1 bg-muted rounded text-xs\">.claude/settings.json</code>\n                      and should be committed to version control.\n                    </p>\n                  </div>\n                  \n                  <HooksEditor\n                    projectPath={project.path}\n                    scope=\"project\"\n                  />\n                </div>\n              </Card>\n            </TabsContent>\n\n            <TabsContent value=\"local\" className=\"space-y-6\">\n              <Card className=\"p-6\">\n                <div className=\"space-y-4\">\n                  <div>\n                    <h3 className=\"text-lg font-semibold mb-2\">Local Hooks</h3>\n                    <p className=\"text-sm text-muted-foreground mb-4\">\n                      These hooks only apply to your machine. They are stored in\n                      <code className=\"mx-1 px-2 py-1 bg-muted rounded text-xs\">.claude/settings.local.json</code>\n                      and should NOT be committed to version control.\n                    </p>\n                    \n                    {!gitIgnoreLocal && (\n                      <div className=\"flex items-center gap-4 p-3 bg-yellow-500/10 rounded-md\">\n                        <AlertTriangle className=\"h-5 w-5 text-yellow-600\" />\n                        <div className=\"flex-1\">\n                          <p className=\"text-sm text-yellow-600\">\n                            Local settings file is not in .gitignore\n                          </p>\n                        </div>\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          onClick={addToGitIgnore}\n                        >\n                          Add to .gitignore\n                        </Button>\n                      </div>\n                    )}\n                  </div>\n                  \n                  <HooksEditor\n                    projectPath={project.path}\n                    scope=\"local\"\n                  />\n                </div>\n              </Card>\n            </TabsContent>\n          </Tabs>\n        </div>\n      </div>\n\n      {/* Toast Container */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n    </div>\n  );\n}; \n"
  },
  {
    "path": "src/components/ProxySettings.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { invoke } from '@tauri-apps/api/core';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Switch } from '@/components/ui/switch';\n\nexport interface ProxySettings {\n  http_proxy: string | null;\n  https_proxy: string | null;\n  no_proxy: string | null;\n  all_proxy: string | null;\n  enabled: boolean;\n}\n\ninterface ProxySettingsProps {\n  setToast: (toast: { message: string; type: 'success' | 'error' } | null) => void;\n  onChange?: (hasChanges: boolean, getSettings: () => ProxySettings, saveSettings: () => Promise<void>) => void;\n}\n\nexport function ProxySettings({ setToast, onChange }: ProxySettingsProps) {\n  const [settings, setSettings] = useState<ProxySettings>({\n    http_proxy: null,\n    https_proxy: null,\n    no_proxy: null,\n    all_proxy: null,\n    enabled: false,\n  });\n  const [originalSettings, setOriginalSettings] = useState<ProxySettings>({\n    http_proxy: null,\n    https_proxy: null,\n    no_proxy: null,\n    all_proxy: null,\n    enabled: false,\n  });\n\n  useEffect(() => {\n    loadSettings();\n  }, []);\n\n  // Save settings function\n  const saveSettings = async () => {\n    try {\n      await invoke('save_proxy_settings', { settings });\n      setOriginalSettings(settings);\n      setToast({\n        message: 'Proxy settings saved and applied successfully.',\n        type: 'success',\n      });\n    } catch (error) {\n      console.error('Failed to save proxy settings:', error);\n      setToast({\n        message: 'Failed to save proxy settings',\n        type: 'error',\n      });\n      throw error; // Re-throw to let parent handle the error\n    }\n  };\n\n  // Notify parent component of changes\n  useEffect(() => {\n    if (onChange) {\n      const hasChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings);\n      onChange(hasChanges, () => settings, saveSettings);\n    }\n  }, [settings, originalSettings, onChange]);\n\n  const loadSettings = async () => {\n    try {\n      const loadedSettings = await invoke<ProxySettings>('get_proxy_settings');\n      setSettings(loadedSettings);\n      setOriginalSettings(loadedSettings);\n    } catch (error) {\n      console.error('Failed to load proxy settings:', error);\n      setToast({\n        message: 'Failed to load proxy settings',\n        type: 'error',\n      });\n    }\n  };\n\n\n  const handleInputChange = (field: keyof ProxySettings, value: string) => {\n    setSettings(prev => ({\n      ...prev,\n      [field]: value || null,\n    }));\n  };\n\n  return (\n    <div className=\"space-y-6\">\n      <div>\n        <h3 className=\"text-lg font-medium\">Proxy Settings</h3>\n        <p className=\"text-sm text-muted-foreground\">\n          Configure proxy settings for Claude API requests\n        </p>\n      </div>\n\n      <div className=\"space-y-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"space-y-0.5\">\n            <Label htmlFor=\"proxy-enabled\">Enable Proxy</Label>\n            <p className=\"text-sm text-muted-foreground\">\n              Use proxy for all Claude API requests\n            </p>\n          </div>\n          <Switch\n            id=\"proxy-enabled\"\n            checked={settings.enabled}\n            onCheckedChange={(checked) => setSettings(prev => ({ ...prev, enabled: checked }))}\n          />\n        </div>\n\n        <div className=\"space-y-4\" style={{ opacity: settings.enabled ? 1 : 0.5 }}>\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"http-proxy\">HTTP Proxy</Label>\n            <Input\n              id=\"http-proxy\"\n              placeholder=\"http://proxy.example.com:8080\"\n              value={settings.http_proxy || ''}\n              onChange={(e) => handleInputChange('http_proxy', e.target.value)}\n              disabled={!settings.enabled}\n            />\n          </div>\n\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"https-proxy\">HTTPS Proxy</Label>\n            <Input\n              id=\"https-proxy\"\n              placeholder=\"http://proxy.example.com:8080\"\n              value={settings.https_proxy || ''}\n              onChange={(e) => handleInputChange('https_proxy', e.target.value)}\n              disabled={!settings.enabled}\n            />\n          </div>\n\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"no-proxy\">No Proxy</Label>\n            <Input\n              id=\"no-proxy\"\n              placeholder=\"localhost,127.0.0.1,.example.com\"\n              value={settings.no_proxy || ''}\n              onChange={(e) => handleInputChange('no_proxy', e.target.value)}\n              disabled={!settings.enabled}\n            />\n            <p className=\"text-xs text-muted-foreground\">\n              Comma-separated list of hosts that should bypass the proxy\n            </p>\n          </div>\n\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"all-proxy\">All Proxy (Optional)</Label>\n            <Input\n              id=\"all-proxy\"\n              placeholder=\"socks5://proxy.example.com:1080\"\n              value={settings.all_proxy || ''}\n              onChange={(e) => handleInputChange('all_proxy', e.target.value)}\n              disabled={!settings.enabled}\n            />\n            <p className=\"text-xs text-muted-foreground\">\n              Proxy URL to use for all protocols if protocol-specific proxies are not set\n            </p>\n          </div>\n        </div>\n\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "src/components/RunningClaudeSessions.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Play, Loader2, Terminal, AlertCircle } from \"lucide-react\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { api, type ProcessInfo, type Session } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { formatISOTimestamp } from \"@/lib/date-utils\";\n\ninterface RunningClaudeSessionsProps {\n  /**\n   * Callback when a running session is clicked to resume\n   */\n  onSessionClick?: (session: Session) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * Component to display currently running Claude sessions\n */\nexport const RunningClaudeSessions: React.FC<RunningClaudeSessionsProps> = ({\n  onSessionClick,\n  className,\n}) => {\n  const [runningSessions, setRunningSessions] = useState<ProcessInfo[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n\n  useEffect(() => {\n    loadRunningSessions();\n    \n    // Poll for updates every 5 seconds\n    const interval = setInterval(loadRunningSessions, 5000);\n    return () => clearInterval(interval);\n  }, []);\n\n  const loadRunningSessions = async () => {\n    try {\n      const sessions = await api.listRunningClaudeSessions();\n      setRunningSessions(sessions);\n      setError(null);\n    } catch (err) {\n      console.error(\"Failed to load running sessions:\", err);\n      setError(\"Failed to load running sessions\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleResumeSession = (processInfo: ProcessInfo) => {\n    // Extract session ID from process type\n    if ('ClaudeSession' in processInfo.process_type) {\n      const sessionId = processInfo.process_type.ClaudeSession.session_id;\n      \n      // Create a minimal session object for resumption\n      const session: Session = {\n        id: sessionId,\n        project_id: processInfo.project_path.replace(/[^a-zA-Z0-9]/g, '-'),\n        project_path: processInfo.project_path,\n        created_at: new Date(processInfo.started_at).getTime() / 1000,\n      };\n      \n      // Emit event to navigate to the session\n      const event = new CustomEvent('claude-session-selected', { \n        detail: { session, projectPath: processInfo.project_path } \n      });\n      window.dispatchEvent(event);\n      \n      onSessionClick?.(session);\n    }\n  };\n\n  if (loading && runningSessions.length === 0) {\n    return (\n      <div className={cn(\"flex items-center justify-center py-4\", className)}>\n        <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n      </div>\n    );\n  }\n\n  if (error) {\n    return (\n      <div className={cn(\"flex items-center gap-2 text-destructive text-sm\", className)}>\n        <AlertCircle className=\"h-4 w-4\" />\n        <span>{error}</span>\n      </div>\n    );\n  }\n\n  if (runningSessions.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className={cn(\"space-y-3\", className)}>\n      <div className=\"flex items-center gap-2\">\n        <div className=\"flex items-center gap-1\">\n          <div className=\"w-2 h-2 bg-green-500 rounded-full animate-pulse\" />\n          <h3 className=\"text-sm font-medium\">Active Claude Sessions</h3>\n        </div>\n        <span className=\"text-xs text-muted-foreground\">\n          ({runningSessions.length} running)\n        </span>\n      </div>\n\n      <div className=\"space-y-2\">\n        {runningSessions.map((session) => {\n          const sessionId = 'ClaudeSession' in session.process_type \n            ? session.process_type.ClaudeSession.session_id \n            : null;\n          \n          if (!sessionId) return null;\n\n          return (\n            <motion.div\n              key={session.run_id}\n              initial={{ opacity: 0, y: 8 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ duration: 0.15 }}\n            >\n              <Card className=\"transition-all hover:shadow-md hover:scale-[1.01] cursor-pointer\">\n                <CardContent \n                  className=\"p-3\"\n                  onClick={() => handleResumeSession(session)}\n                >\n                  <div className=\"flex items-start justify-between gap-3\">\n                    <div className=\"flex items-start gap-3 flex-1 min-w-0\">\n                      <Terminal className=\"h-4 w-4 text-green-600 mt-0.5 flex-shrink-0\" />\n                      <div className=\"space-y-1 flex-1 min-w-0\">\n                        <div className=\"flex items-center gap-2\">\n                          <p className=\"font-mono text-xs text-muted-foreground truncate\">\n                            {sessionId.substring(0, 20)}...\n                          </p>\n                          <span className=\"text-xs text-green-600 font-medium\">\n                            Running\n                          </span>\n                        </div>\n                        \n                        <p className=\"text-xs text-muted-foreground truncate\">\n                          {session.project_path}\n                        </p>\n                        \n                        <div className=\"flex items-center gap-3 text-xs text-muted-foreground\">\n                          <span>Started: {formatISOTimestamp(session.started_at)}</span>\n                          <span>Model: {session.model}</span>\n                          {session.task && (\n                            <span className=\"truncate max-w-[200px]\" title={session.task}>\n                              Task: {session.task}\n                            </span>\n                          )}\n                        </div>\n                      </div>\n                    </div>\n                    \n                    <Button\n                      size=\"sm\"\n                      variant=\"ghost\"\n                      className=\"flex-shrink-0\"\n                    >\n                      <Play className=\"h-3 w-3 mr-1\" />\n                      Resume\n                    </Button>\n                  </div>\n                </CardContent>\n              </Card>\n            </motion.div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/SessionList.optimized.tsx",
    "content": "import React, { useMemo, useCallback } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { FileText, ArrowLeft, Calendar, Clock } from \"lucide-react\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport { ClaudeMemoriesDropdown } from \"@/components/ClaudeMemoriesDropdown\";\nimport { cn } from \"@/lib/utils\";\nimport { formatUnixTimestamp, formatISOTimestamp } from \"@/lib/date-utils\";\nimport { usePagination } from \"@/hooks/usePagination\";\nimport type { Session, ClaudeMdFile } from \"@/lib/api\";\n\ninterface SessionListProps {\n  sessions: Session[];\n  projectPath: string;\n  onBack: () => void;\n  onSessionClick?: (session: Session) => void;\n  onEditClaudeFile?: (file: ClaudeMdFile) => void;\n  className?: string;\n}\n\n// Memoized session card component to prevent unnecessary re-renders\nconst SessionCard = React.memo<{\n  session: Session;\n  projectPath: string;\n  onClick?: () => void;\n  onEditClaudeFile?: (file: ClaudeMdFile) => void;\n}>(({ session, projectPath, onClick, onEditClaudeFile }) => {\n  const formatTime = useCallback((timestamp: string | number | undefined) => {\n    if (!timestamp) return \"Unknown time\";\n    \n    if (typeof timestamp === \"string\") {\n      return formatISOTimestamp(timestamp);\n    } else {\n      return formatUnixTimestamp(timestamp);\n    }\n  }, []);\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, y: 20 }}\n      animate={{ opacity: 1, y: 0 }}\n      exit={{ opacity: 0, y: -20 }}\n      whileHover={{ scale: 1.01 }}\n      whileTap={{ scale: 0.99 }}\n    >\n      <Card \n        className={cn(\n          \"cursor-pointer transition-all\",\n          \"hover:shadow-lg hover:border-primary/20\",\n          \"bg-card/50 backdrop-blur-sm\"\n        )}\n        onClick={onClick}\n      >\n        <CardContent className=\"p-6\">\n          <div className=\"flex items-start justify-between\">\n            <div className=\"flex-1 space-y-3\">\n              {/* Session title */}\n              <div className=\"flex items-start gap-3\">\n                <FileText className=\"h-5 w-5 text-primary mt-0.5 flex-shrink-0\" />\n                <div className=\"flex-1 min-w-0\">\n                  <h3 className=\"font-semibold text-lg truncate\">\n                    {`Session ${session.id.slice(0, 8)}`}\n                  </h3>\n                </div>\n              </div>\n\n              {/* Session metadata */}\n              <div className=\"flex flex-wrap gap-4 text-sm text-muted-foreground\">\n                <div className=\"flex items-center gap-1\">\n                  <Calendar className=\"h-3.5 w-3.5\" />\n                  <span>{formatTime(session.created_at)}</span>\n                </div>\n                {session.message_timestamp && (\n                  <div className=\"flex items-center gap-1\">\n                    <Clock className=\"h-3.5 w-3.5\" />\n                    <span>{formatTime(session.message_timestamp)}</span>\n                  </div>\n                )}\n              </div>\n\n              {/* Session ID */}\n              <div className=\"text-xs text-muted-foreground/60 font-mono\">\n                ID: {session.id}\n              </div>\n            </div>\n\n            {/* Claude memories dropdown */}\n            <div className=\"ml-4\">\n              <ClaudeMemoriesDropdown\n                projectPath={projectPath}\n                onEditFile={onEditClaudeFile || (() => {})}\n              />\n            </div>\n          </div>\n        </CardContent>\n      </Card>\n    </motion.div>\n  );\n});\n\nSessionCard.displayName = 'SessionCard';\n\nexport const SessionList: React.FC<SessionListProps> = React.memo(({\n  sessions,\n  projectPath,\n  onBack,\n  onSessionClick,\n  onEditClaudeFile,\n  className\n}) => {\n  // Sort sessions by created_at in descending order\n  const sortedSessions = useMemo(() => {\n    return [...sessions].sort((a, b) => {\n      const timeA = a.created_at || 0;\n      const timeB = b.created_at || 0;\n      return timeB > timeA ? 1 : -1;\n    });\n  }, [sessions]);\n\n  // Use custom pagination hook\n  const {\n    currentPage,\n    totalPages,\n    paginatedData,\n    goToPage,\n    canGoNext: _canGoNext,\n    canGoPrevious: _canGoPrevious\n  } = usePagination(sortedSessions, {\n    initialPage: 1,\n    initialPageSize: 5\n  });\n\n  const handleSessionClick = useCallback((session: Session) => {\n    onSessionClick?.(session);\n  }, [onSessionClick]);\n\n  return (\n    <div className={cn(\"space-y-6\", className)}>\n      {/* Header */}\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-4\">\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={onBack}\n            className=\"h-8 w-8\"\n          >\n            <ArrowLeft className=\"h-4 w-4\" />\n          </Button>\n          <div>\n            <h2 className=\"text-2xl font-bold\">Sessions</h2>\n            <p className=\"text-sm text-muted-foreground\">\n              {projectPath}\n            </p>\n          </div>\n        </div>\n        <div className=\"text-sm text-muted-foreground\">\n          {sessions.length} {sessions.length === 1 ? 'session' : 'sessions'}\n        </div>\n      </div>\n\n      {/* Sessions list */}\n      {sessions.length === 0 ? (\n        <Card className=\"bg-muted/20\">\n          <CardContent className=\"p-12 text-center\">\n            <FileText className=\"h-12 w-12 text-muted-foreground mx-auto mb-4\" />\n            <p className=\"text-muted-foreground\">\n              No sessions found for this project\n            </p>\n          </CardContent>\n        </Card>\n      ) : (\n        <>\n          <AnimatePresence mode=\"wait\">\n            <motion.div\n              key={currentPage}\n              initial={{ opacity: 0, x: -20 }}\n              animate={{ opacity: 1, x: 0 }}\n              exit={{ opacity: 0, x: 20 }}\n              className=\"space-y-4\"\n            >\n              {paginatedData.map((session) => (\n                <SessionCard\n                  key={session.id}\n                  session={session}\n                  projectPath={projectPath}\n                  onClick={() => handleSessionClick(session)}\n                  onEditClaudeFile={onEditClaudeFile}\n                />\n              ))}\n            </motion.div>\n          </AnimatePresence>\n\n          {/* Pagination */}\n          {totalPages > 1 && (\n            <div className=\"flex justify-center\">\n              <Pagination\n                currentPage={currentPage}\n                totalPages={totalPages}\n                onPageChange={goToPage}\n              />\n            </div>\n          )}\n        </>\n      )}\n    </div>\n  );\n});"
  },
  {
    "path": "src/components/SessionList.tsx",
    "content": "import React, { useState } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { Clock, MessageSquare } from \"lucide-react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport { ClaudeMemoriesDropdown } from \"@/components/ClaudeMemoriesDropdown\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { truncateText, getFirstLine } from \"@/lib/date-utils\";\nimport type { Session, ClaudeMdFile } from \"@/lib/api\";\n\ninterface SessionListProps {\n  /**\n   * Array of sessions to display\n   */\n  sessions: Session[];\n  /**\n   * The current project path being viewed\n   */\n  projectPath: string;\n  /**\n   * Optional callback to go back to project list (deprecated - use tabs instead)\n   */\n  onBack?: () => void;\n  /**\n   * Callback when a session is clicked\n   */\n  onSessionClick?: (session: Session) => void;\n  /**\n   * Callback when a CLAUDE.md file should be edited\n   */\n  onEditClaudeFile?: (file: ClaudeMdFile) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\nconst ITEMS_PER_PAGE = 12;\n\n/**\n * SessionList component - Displays paginated sessions for a specific project\n * \n * @example\n * <SessionList\n *   sessions={sessions}\n *   projectPath=\"/Users/example/project\"\n *   onBack={() => setSelectedProject(null)}\n *   onSessionClick={(session) => console.log('Selected session:', session)}\n * />\n */\nexport const SessionList: React.FC<SessionListProps> = ({\n  sessions,\n  projectPath,\n  onSessionClick,\n  onEditClaudeFile,\n  className,\n}) => {\n  const [currentPage, setCurrentPage] = useState(1);\n  \n  // Calculate pagination\n  const totalPages = Math.ceil(sessions.length / ITEMS_PER_PAGE);\n  const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;\n  const endIndex = startIndex + ITEMS_PER_PAGE;\n  const currentSessions = sessions.slice(startIndex, endIndex);\n  \n  // Reset to page 1 if sessions change\n  React.useEffect(() => {\n    setCurrentPage(1);\n  }, [sessions.length]);\n  \n  return (\n    <TooltipProvider>\n      <div className={cn(\"space-y-4\", className)}>\n      {/* CLAUDE.md Memories Dropdown */}\n      {onEditClaudeFile && (\n        <motion.div\n          initial={{ opacity: 0, y: 10 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.3, delay: 0.1 }}\n        >\n          <ClaudeMemoriesDropdown\n            projectPath={projectPath}\n            onEditFile={onEditClaudeFile}\n          />\n        </motion.div>\n      )}\n\n      <AnimatePresence mode=\"popLayout\">\n        <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n          {currentSessions.map((session, index) => (\n            <motion.div\n              key={session.id}\n              initial={{ opacity: 0, y: 20 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -20 }}\n              transition={{\n                duration: 0.3,\n                delay: index * 0.05,\n                ease: [0.4, 0, 0.2, 1],\n              }}\n            >\n              <Card\n                className={cn(\n                  \"p-3 hover:bg-accent/50 transition-all duration-200 cursor-pointer group h-full\",\n                  session.todo_data && \"bg-primary/5\"\n                )}\n                onClick={() => {\n                  // Emit a special event for Claude Code session navigation\n                  const event = new CustomEvent('claude-session-selected', { \n                    detail: { session, projectPath } \n                  });\n                  window.dispatchEvent(event);\n                  onSessionClick?.(session);\n                }}\n              >\n                <div className=\"flex flex-col h-full\">\n                  <div className=\"flex-1\">\n                    {/* Session header */}\n                    <div className=\"flex items-start justify-between mb-2\">\n                      <div className=\"flex items-start gap-1.5 flex-1 min-w-0\">\n                        <Clock className=\"h-4 w-4 text-primary shrink-0 mt-0.5\" />\n                        <div className=\"flex-1 min-w-0\">\n                          <p className=\"text-body-small font-medium\">\n                            Session on {session.message_timestamp \n                              ? new Date(session.message_timestamp).toLocaleDateString('en-US', { \n                                  month: 'short', \n                                  day: 'numeric',\n                                  year: 'numeric'\n                                })\n                              : new Date(session.created_at * 1000).toLocaleDateString('en-US', { \n                                  month: 'short', \n                                  day: 'numeric',\n                                  year: 'numeric'\n                                })\n                            }\n                          </p>\n                        </div>\n                      </div>\n                      {session.todo_data && (\n                        <span className=\"inline-flex items-center px-1.5 py-0.5 rounded text-caption font-medium bg-primary/10 text-primary\">\n                          Todo\n                        </span>\n                      )}\n                    </div>\n                    \n                    {/* First message preview */}\n                    {session.first_message ? (\n                      <p className=\"text-caption text-muted-foreground line-clamp-2 mb-2\">\n                        {truncateText(getFirstLine(session.first_message), 120)}\n                      </p>\n                    ) : (\n                      <p className=\"text-caption text-muted-foreground/60 italic mb-2\">\n                        No messages yet\n                      </p>\n                    )}\n                  </div>\n                  \n                  {/* Metadata footer */}\n                  <div className=\"flex items-center justify-between pt-2 border-t\">\n                    <p className=\"text-caption text-muted-foreground font-mono\">\n                      {session.id.slice(-8)}\n                    </p>\n                    {session.todo_data && (\n                      <MessageSquare className=\"h-3 w-3 text-primary\" />\n                    )}\n                  </div>\n                </div>\n              </Card>\n            </motion.div>\n          ))}\n        </div>\n      </AnimatePresence>\n      \n        <Pagination\n          currentPage={currentPage}\n          totalPages={totalPages}\n          onPageChange={setCurrentPage}\n        />\n      </div>\n    </TooltipProvider>\n  );\n}; "
  },
  {
    "path": "src/components/SessionOutputViewer.tsx",
    "content": "import { useState, useEffect, useRef, useMemo } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { X, Maximize2, Minimize2, Copy, RefreshCw, RotateCcw, ChevronDown } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Toast, ToastContainer } from '@/components/ui/toast';\nimport { Popover } from '@/components/ui/popover';\nimport { api } from '@/lib/api';\nimport { useOutputCache } from '@/lib/outputCache';\nimport type { AgentRun } from '@/lib/api';\nimport { listen, type UnlistenFn } from '@tauri-apps/api/event';\nimport { StreamMessage } from './StreamMessage';\nimport { ErrorBoundary } from './ErrorBoundary';\n\ninterface SessionOutputViewerProps {\n  session: AgentRun;\n  onClose: () => void;\n  className?: string;\n}\n\n// Use the same message interface as AgentExecution for consistency\nexport interface ClaudeStreamMessage {\n  type: \"system\" | \"assistant\" | \"user\" | \"result\";\n  subtype?: string;\n  message?: {\n    content?: any[];\n    usage?: {\n      input_tokens: number;\n      output_tokens: number;\n    };\n  };\n  usage?: {\n    input_tokens: number;\n    output_tokens: number;\n  };\n  [key: string]: any;\n}\n\nexport function SessionOutputViewer({ session, onClose, className }: SessionOutputViewerProps) {\n  const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);\n  const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [isFullscreen, setIsFullscreen] = useState(false);\n  const [refreshing, setRefreshing] = useState(false);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n  const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);\n  const [hasUserScrolled, setHasUserScrolled] = useState(false);\n  \n  const scrollAreaRef = useRef<HTMLDivElement>(null);\n  const outputEndRef = useRef<HTMLDivElement>(null);\n  const fullscreenScrollRef = useRef<HTMLDivElement>(null);\n  const fullscreenMessagesEndRef = useRef<HTMLDivElement>(null);\n  const unlistenRefs = useRef<UnlistenFn[]>([]);\n  const { getCachedOutput, setCachedOutput } = useOutputCache();\n\n  // Auto-scroll logic similar to AgentExecution\n  const isAtBottom = () => {\n    const container = isFullscreen ? fullscreenScrollRef.current : scrollAreaRef.current;\n    if (container) {\n      const { scrollTop, scrollHeight, clientHeight } = container;\n      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;\n      return distanceFromBottom < 1;\n    }\n    return true;\n  };\n\n  const scrollToBottom = () => {\n    if (!hasUserScrolled) {\n      const endRef = isFullscreen ? fullscreenMessagesEndRef.current : outputEndRef.current;\n      if (endRef) {\n        endRef.scrollIntoView({ behavior: 'smooth' });\n      }\n    }\n  };\n\n  // Clean up listeners on unmount\n  useEffect(() => {\n    return () => {\n      unlistenRefs.current.forEach(unlisten => unlisten());\n    };\n  }, []);\n\n  // Auto-scroll when messages change\n  useEffect(() => {\n    const shouldAutoScroll = !hasUserScrolled || isAtBottom();\n    if (shouldAutoScroll) {\n      scrollToBottom();\n    }\n  }, [messages, hasUserScrolled, isFullscreen]);\n\n\n  const loadOutput = async (skipCache = false) => {\n    if (!session.id) return;\n\n    try {\n      // Check cache first if not skipping cache\n      if (!skipCache) {\n        const cached = getCachedOutput(session.id);\n        if (cached) {\n          const cachedJsonlLines = cached.output.split('\\n').filter(line => line.trim());\n          setRawJsonlOutput(cachedJsonlLines);\n          setMessages(cached.messages);\n          // If cache is recent (less than 5 seconds old) and session isn't running, use cache only\n          if (Date.now() - cached.lastUpdated < 5000 && session.status !== 'running') {\n            return;\n          }\n        }\n      }\n\n      setLoading(true);\n\n      // If we have a session_id, try to load from JSONL file first\n      if (session.session_id && session.session_id !== '') {\n        try {\n          const history = await api.loadAgentSessionHistory(session.session_id);\n          \n          // Convert history to messages format using AgentExecution style\n          const loadedMessages: ClaudeStreamMessage[] = history.map(entry => ({\n            ...entry,\n            type: entry.type || \"assistant\"\n          }));\n          \n          setMessages(loadedMessages);\n          setRawJsonlOutput(history.map(h => JSON.stringify(h)));\n          \n          // Update cache\n          setCachedOutput(session.id, {\n            output: history.map(h => JSON.stringify(h)).join('\\n'),\n            messages: loadedMessages,\n            lastUpdated: Date.now(),\n            status: session.status\n          });\n          \n          // Set up live event listeners for running sessions\n          if (session.status === 'running') {\n            setupLiveEventListeners();\n            \n            try {\n              await api.streamSessionOutput(session.id);\n            } catch (streamError) {\n              console.warn('Failed to start streaming, will poll instead:', streamError);\n            }\n          }\n          \n          return;\n        } catch (err) {\n          console.warn('Failed to load from JSONL, falling back to regular output:', err);\n        }\n      }\n\n      // Fallback to the original method if JSONL loading fails or no session_id\n      const rawOutput = await api.getSessionOutput(session.id);\n      \n      // Parse JSONL output into messages using AgentExecution style\n      const jsonlLines = rawOutput.split('\\n').filter(line => line.trim());\n      setRawJsonlOutput(jsonlLines);\n      \n      const parsedMessages: ClaudeStreamMessage[] = [];\n      for (const line of jsonlLines) {\n        try {\n          const message = JSON.parse(line) as ClaudeStreamMessage;\n          parsedMessages.push(message);\n        } catch (err) {\n          console.error(\"Failed to parse message:\", err, line);\n        }\n      }\n      setMessages(parsedMessages);\n      \n      // Update cache\n      setCachedOutput(session.id, {\n        output: rawOutput,\n        messages: parsedMessages,\n        lastUpdated: Date.now(),\n        status: session.status\n      });\n      \n      // Set up live event listeners for running sessions\n      if (session.status === 'running') {\n        setupLiveEventListeners();\n        \n        try {\n          await api.streamSessionOutput(session.id);\n        } catch (streamError) {\n          console.warn('Failed to start streaming, will poll instead:', streamError);\n        }\n      }\n    } catch (error) {\n      console.error('Failed to load session output:', error);\n      setToast({ message: 'Failed to load session output', type: 'error' });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const setupLiveEventListeners = async () => {\n    if (!session.id) return;\n    \n    try {\n      // Clean up existing listeners\n      unlistenRefs.current.forEach(unlisten => unlisten());\n      unlistenRefs.current = [];\n\n      // Set up live event listeners with run ID isolation\n      const outputUnlisten = await listen<string>(`agent-output:${session.id}`, (event) => {\n        try {\n          // Store raw JSONL\n          setRawJsonlOutput(prev => [...prev, event.payload]);\n          \n          // Parse and display\n          const message = JSON.parse(event.payload) as ClaudeStreamMessage;\n          setMessages(prev => [...prev, message]);\n        } catch (err) {\n          console.error(\"Failed to parse message:\", err, event.payload);\n        }\n      });\n\n      const errorUnlisten = await listen<string>(`agent-error:${session.id}`, (event) => {\n        console.error(\"Agent error:\", event.payload);\n        setToast({ message: event.payload, type: 'error' });\n      });\n\n      const completeUnlisten = await listen<boolean>(`agent-complete:${session.id}`, () => {\n        setToast({ message: 'Agent execution completed', type: 'success' });\n        // Don't set status here as the parent component should handle it\n      });\n\n      const cancelUnlisten = await listen<boolean>(`agent-cancelled:${session.id}`, () => {\n        setToast({ message: 'Agent execution was cancelled', type: 'error' });\n      });\n\n      unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];\n    } catch (error) {\n      console.error('Failed to set up live event listeners:', error);\n    }\n  };\n\n  // Copy functionality similar to AgentExecution\n  const handleCopyAsJsonl = async () => {\n    const jsonl = rawJsonlOutput.join('\\n');\n    await navigator.clipboard.writeText(jsonl);\n    setCopyPopoverOpen(false);\n    setToast({ message: 'Output copied as JSONL', type: 'success' });\n  };\n\n  const handleCopyAsMarkdown = async () => {\n    let markdown = `# Agent Session: ${session.agent_name}\\n\\n`;\n    markdown += `**Status:** ${session.status}\\n`;\n    if (session.task) markdown += `**Task:** ${session.task}\\n`;\n    if (session.model) markdown += `**Model:** ${session.model}\\n`;\n    markdown += `**Date:** ${new Date().toISOString()}\\n\\n`;\n    markdown += `---\\n\\n`;\n\n    for (const msg of messages) {\n      if (msg.type === \"system\" && msg.subtype === \"init\") {\n        markdown += `## System Initialization\\n\\n`;\n        markdown += `- Session ID: \\`${msg.session_id || 'N/A'}\\`\\n`;\n        markdown += `- Model: \\`${msg.model || 'default'}\\`\\n`;\n        if (msg.cwd) markdown += `- Working Directory: \\`${msg.cwd}\\`\\n`;\n        if (msg.tools?.length) markdown += `- Tools: ${msg.tools.join(', ')}\\n`;\n        markdown += `\\n`;\n      } else if (msg.type === \"assistant\" && msg.message) {\n        markdown += `## Assistant\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_use\") {\n            markdown += `### Tool: ${content.name}\\n\\n`;\n            markdown += `\\`\\`\\`json\\n${JSON.stringify(content.input, null, 2)}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n        if (msg.message.usage) {\n          markdown += `*Tokens: ${msg.message.usage.input_tokens} in, ${msg.message.usage.output_tokens} out*\\n\\n`;\n        }\n      } else if (msg.type === \"user\" && msg.message) {\n        markdown += `## User\\n\\n`;\n        for (const content of msg.message.content || []) {\n          if (content.type === \"text\") {\n            markdown += `${content.text}\\n\\n`;\n          } else if (content.type === \"tool_result\") {\n            markdown += `### Tool Result\\n\\n`;\n            markdown += `\\`\\`\\`\\n${content.content}\\n\\`\\`\\`\\n\\n`;\n          }\n        }\n      } else if (msg.type === \"result\") {\n        markdown += `## Execution Result\\n\\n`;\n        if (msg.result) {\n          markdown += `${msg.result}\\n\\n`;\n        }\n        if (msg.error) {\n          markdown += `**Error:** ${msg.error}\\n\\n`;\n        }\n      }\n    }\n\n    await navigator.clipboard.writeText(markdown);\n    setCopyPopoverOpen(false);\n    setToast({ message: 'Output copied as Markdown', type: 'success' });\n  };\n\n\n  const refreshOutput = async () => {\n    setRefreshing(true);\n    try {\n      await loadOutput(true); // Skip cache when manually refreshing\n      setToast({ message: 'Output refreshed', type: 'success' });\n    } catch (error) {\n      console.error('Failed to refresh output:', error);\n      setToast({ message: 'Failed to refresh output', type: 'error' });\n    } finally {\n      setRefreshing(false);\n    }\n  };\n\n\n  // Load output on mount and check cache first\n  useEffect(() => {\n    if (!session.id) return;\n    \n    // Check cache immediately for instant display\n    const cached = getCachedOutput(session.id);\n    if (cached) {\n      const cachedJsonlLines = cached.output.split('\\n').filter(line => line.trim());\n      setRawJsonlOutput(cachedJsonlLines);\n      setMessages(cached.messages);\n    }\n    \n    // Then load fresh data\n    loadOutput();\n  }, [session.id]);\n\n  const displayableMessages = useMemo(() => {\n    return messages.filter((message, index) => {\n      if (message.isMeta && !message.leafUuid && !message.summary) return false;\n\n      if (message.type === \"user\" && message.message) {\n        if (message.isMeta) return false;\n\n        const msg = message.message;\n        if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) return false;\n\n        if (Array.isArray(msg.content)) {\n          let hasVisibleContent = false;\n          for (const content of msg.content) {\n            if (content.type === \"text\") { hasVisibleContent = true; break; }\n            if (content.type === \"tool_result\") {\n              let willBeSkipped = false;\n              if (content.tool_use_id) {\n                for (let i = index - 1; i >= 0; i--) {\n                  const prevMsg = messages[i];\n                  if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {\n                    const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id);\n                    if (toolUse) {\n                      const toolName = toolUse.name?.toLowerCase();\n                      const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep'];\n                      if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {\n                        willBeSkipped = true;\n                      }\n                      break;\n                    }\n                  }\n                }\n              }\n              if (!willBeSkipped) { hasVisibleContent = true; break; }\n            }\n          }\n          if (!hasVisibleContent) return false;\n        }\n      }\n      return true;\n    });\n  }, [messages]);\n\n  return (\n    <>\n      <motion.div\n        initial={{ opacity: 0, scale: 0.95 }}\n        animate={{ opacity: 1, scale: 1 }}\n        exit={{ opacity: 0, scale: 0.95 }}\n        transition={{ duration: 0.2 }}\n        className={`${isFullscreen ? 'fixed inset-0 z-50 bg-background' : ''} ${className}`}\n      >\n        <Card className={`h-full ${isFullscreen ? 'rounded-none border-0' : ''}`}>\n          <CardHeader className=\"pb-3\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center space-x-3\">\n                <div className=\"text-2xl\">{session.agent_icon}</div>\n                <div>\n                  <CardTitle className=\"text-base\">{session.agent_name} - Output</CardTitle>\n                  <div className=\"flex items-center space-x-2 mt-1\">\n                    <Badge variant={session.status === 'running' ? 'default' : 'secondary'}>\n                      {session.status}\n                    </Badge>\n                    {session.status === 'running' && (\n                      <Badge variant=\"outline\" className=\"text-xs bg-green-50 text-green-700 border-green-200\">\n                        <div className=\"w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse mr-1\"></div>\n                        Live\n                      </Badge>\n                    )}\n                    <span className=\"text-xs text-muted-foreground\">\n                      {messages.length} messages\n                    </span>\n                  </div>\n                </div>\n              </div>\n              <div className=\"flex items-center space-x-2\">\n                {messages.length > 0 && (\n                  <>\n                    <Button\n                      variant=\"outline\"\n                      size=\"sm\"\n                      onClick={() => setIsFullscreen(!isFullscreen)}\n                      title=\"Fullscreen\"\n                    >\n                      {isFullscreen ? <Minimize2 className=\"h-4 w-4\" /> : <Maximize2 className=\"h-4 w-4\" />}\n                    </Button>\n                    <Popover\n                      trigger={\n                        <Button\n                          variant=\"outline\"\n                          size=\"sm\"\n                          className=\"flex items-center gap-2\"\n                        >\n                          <Copy className=\"h-4 w-4\" />\n                          Copy Output\n                          <ChevronDown className=\"h-3 w-3\" />\n                        </Button>\n                      }\n                      content={\n                        <div className=\"w-44 p-1\">\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"w-full justify-start\"\n                            onClick={handleCopyAsJsonl}\n                          >\n                            Copy as JSONL\n                          </Button>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"w-full justify-start\"\n                            onClick={handleCopyAsMarkdown}\n                          >\n                            Copy as Markdown\n                          </Button>\n                        </div>\n                      }\n                      open={copyPopoverOpen}\n                      onOpenChange={setCopyPopoverOpen}\n                      align=\"end\"\n                    />\n                  </>\n                )}\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={refreshOutput}\n                  disabled={refreshing}\n                  title=\"Refresh output\"\n                >\n                  <RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />\n                </Button>\n                <Button variant=\"outline\" size=\"sm\" onClick={onClose}>\n                  <X className=\"h-4 w-4\" />\n                </Button>\n              </div>\n            </div>\n          </CardHeader>\n          <CardContent className={`${isFullscreen ? 'h-[calc(100vh-120px)]' : 'h-96'} p-0`}>\n            {loading ? (\n              <div className=\"flex items-center justify-center h-full\">\n                <div className=\"flex items-center space-x-2\">\n                  <RefreshCw className=\"h-4 w-4 animate-spin\" />\n                  <span>Loading output...</span>\n                </div>\n              </div>\n            ) : (\n              <div \n                className=\"h-full overflow-y-auto p-6 space-y-3\" \n                ref={scrollAreaRef}\n                onScroll={() => {\n                  // Mark that user has scrolled manually\n                  if (!hasUserScrolled) {\n                    setHasUserScrolled(true);\n                  }\n                  \n                  // If user scrolls back to bottom, re-enable auto-scroll\n                  if (isAtBottom()) {\n                    setHasUserScrolled(false);\n                  }\n                }}\n              >\n                {messages.length === 0 ? (\n                  <div className=\"flex flex-col items-center justify-center h-full text-center\">\n                    {session.status === 'running' ? (\n                      <>\n                        <RefreshCw className=\"h-8 w-8 animate-spin text-muted-foreground mb-2\" />\n                        <p className=\"text-muted-foreground\">Waiting for output...</p>\n                        <p className=\"text-xs text-muted-foreground mt-1\">\n                          Agent is running but no output received yet\n                        </p>\n                      </>\n                    ) : (\n                      <>\n                        <p className=\"text-muted-foreground\">No output available</p>\n                        <Button \n                          variant=\"outline\" \n                          size=\"sm\" \n                          onClick={refreshOutput}\n                          className=\"mt-2\"\n                          disabled={refreshing}\n                        >\n                          {refreshing ? <RefreshCw className=\"h-4 w-4 animate-spin mr-2\" /> : <RotateCcw className=\"h-4 w-4 mr-2\" />}\n                          Refresh\n                        </Button>\n                      </>\n                    )}\n                  </div>\n                ) : (\n                  <>\n                    <AnimatePresence>\n                      {displayableMessages.map((message: ClaudeStreamMessage, index: number) => (\n                        <motion.div\n                          key={index}\n                          initial={{ opacity: 0, y: 10 }}\n                          animate={{ opacity: 1, y: 0 }}\n                          transition={{ duration: 0.2 }}\n                        >\n                          <ErrorBoundary>\n                            <StreamMessage message={message} streamMessages={messages} />\n                          </ErrorBoundary>\n                        </motion.div>\n                      ))}\n                    </AnimatePresence>\n                    <div ref={outputEndRef} />\n                  </>\n                )}\n              </div>\n            )}\n          </CardContent>\n        </Card>\n      </motion.div>\n\n      {/* Fullscreen Modal */}\n      {isFullscreen && (\n        <div className=\"fixed inset-0 z-50 bg-background flex flex-col\">\n          {/* Modal Header */}\n          <div className=\"flex items-center justify-between p-4 border-b border-border\">\n            <div className=\"flex items-center gap-2\">\n              <div className=\"text-2xl\">{session.agent_icon}</div>\n              <h2 className=\"text-lg font-semibold\">{session.agent_name} - Output</h2>\n              {session.status === 'running' && (\n                <div className=\"flex items-center gap-1\">\n                  <div className=\"w-2 h-2 bg-green-500 rounded-full animate-pulse\"></div>\n                  <span className=\"text-xs text-green-600 font-medium\">Running</span>\n                </div>\n              )}\n            </div>\n            <div className=\"flex items-center gap-2\">\n              {messages.length > 0 && (\n                <Popover\n                  trigger={\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"flex items-center gap-2\"\n                    >\n                      <Copy className=\"h-4 w-4\" />\n                      Copy Output\n                      <ChevronDown className=\"h-3 w-3\" />\n                    </Button>\n                  }\n                  content={\n                    <div className=\"w-44 p-1\">\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        className=\"w-full justify-start\"\n                        onClick={handleCopyAsJsonl}\n                      >\n                        Copy as JSONL\n                      </Button>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        className=\"w-full justify-start\"\n                        onClick={handleCopyAsMarkdown}\n                      >\n                        Copy as Markdown\n                      </Button>\n                    </div>\n                  }\n                  open={copyPopoverOpen}\n                  onOpenChange={setCopyPopoverOpen}\n                  align=\"end\"\n                />\n              )}\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => setIsFullscreen(false)}\n                className=\"flex items-center gap-2\"\n              >\n                <X className=\"h-4 w-4\" />\n                Close\n              </Button>\n            </div>\n          </div>\n\n          {/* Modal Content */}\n          <div className=\"flex-1 overflow-hidden p-6\">\n            <div \n              ref={fullscreenScrollRef}\n              className=\"h-full overflow-y-auto space-y-3\"\n              onScroll={() => {\n                // Mark that user has scrolled manually\n                if (!hasUserScrolled) {\n                  setHasUserScrolled(true);\n                }\n                \n                // If user scrolls back to bottom, re-enable auto-scroll\n                if (isAtBottom()) {\n                  setHasUserScrolled(false);\n                }\n              }}\n            >\n              {messages.length === 0 ? (\n                <div className=\"flex flex-col items-center justify-center h-full text-center\">\n                  {session.status === 'running' ? (\n                    <>\n                      <RefreshCw className=\"h-8 w-8 animate-spin text-muted-foreground mb-2\" />\n                      <p className=\"text-muted-foreground\">Waiting for output...</p>\n                      <p className=\"text-xs text-muted-foreground mt-1\">\n                        Agent is running but no output received yet\n                      </p>\n                    </>\n                  ) : (\n                    <>\n                      <p className=\"text-muted-foreground\">No output available</p>\n                    </>\n                  )}\n                </div>\n              ) : (\n                <>\n                  <AnimatePresence>\n                    {displayableMessages.map((message: ClaudeStreamMessage, index: number) => (\n                      <motion.div\n                        key={index}\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        transition={{ duration: 0.2 }}\n                      >\n                        <ErrorBoundary>\n                          <StreamMessage message={message} streamMessages={messages} />\n                        </ErrorBoundary>\n                      </motion.div>\n                    ))}\n                  </AnimatePresence>\n                  <div ref={fullscreenMessagesEndRef} />\n                </>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/Settings.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { \n  Plus, \n  Trash2, \n  Save, \n  AlertCircle,\n  Loader2,\n  Shield,\n  Check,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Card } from \"@/components/ui/card\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { \n  api, \n  type ClaudeSettings,\n  type ClaudeInstallation\n} from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { Toast, ToastContainer } from \"@/components/ui/toast\";\nimport { ClaudeVersionSelector } from \"./ClaudeVersionSelector\";\nimport { StorageTab } from \"./StorageTab\";\nimport { HooksEditor } from \"./HooksEditor\";\nimport { SlashCommandsManager } from \"./SlashCommandsManager\";\nimport { ProxySettings } from \"./ProxySettings\";\nimport { useTheme, useTrackEvent } from \"@/hooks\";\nimport { analytics } from \"@/lib/analytics\";\nimport { TabPersistenceService } from \"@/services/tabPersistence\";\n\ninterface SettingsProps {\n  /**\n   * Callback to go back to the main view\n   */\n  onBack: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\ninterface PermissionRule {\n  id: string;\n  value: string;\n}\n\ninterface EnvironmentVariable {\n  id: string;\n  key: string;\n  value: string;\n}\n\n/**\n * Comprehensive Settings UI for managing Claude Code settings\n * Provides a no-code interface for editing the settings.json file\n */\nexport const Settings: React.FC<SettingsProps> = ({\n  className,\n}) => {\n  const [settings, setSettings] = useState<ClaudeSettings | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [saving, setSaving] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [activeTab, setActiveTab] = useState(\"general\");\n  const [currentBinaryPath, setCurrentBinaryPath] = useState<string | null>(null);\n  const [selectedInstallation, setSelectedInstallation] = useState<ClaudeInstallation | null>(null);\n  const [binaryPathChanged, setBinaryPathChanged] = useState(false);\n  const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);\n  \n  // Permission rules state\n  const [allowRules, setAllowRules] = useState<PermissionRule[]>([]);\n  const [denyRules, setDenyRules] = useState<PermissionRule[]>([]);\n  \n  // Environment variables state\n  const [envVars, setEnvVars] = useState<EnvironmentVariable[]>([]);\n  \n  // Hooks state\n  const [userHooksChanged, setUserHooksChanged] = useState(false);\n  const getUserHooks = React.useRef<(() => any) | null>(null);\n  \n  // Theme hook\n  const { theme, setTheme, customColors, setCustomColors } = useTheme();\n  \n  // Proxy state\n  const [proxySettingsChanged, setProxySettingsChanged] = useState(false);\n  const saveProxySettings = React.useRef<(() => Promise<void>) | null>(null);\n  \n  // Analytics state\n  const [analyticsEnabled, setAnalyticsEnabled] = useState(false);\n  const trackEvent = useTrackEvent();\n  \n  // Tab persistence state\n  const [tabPersistenceEnabled, setTabPersistenceEnabled] = useState(true);\n  // Startup intro preference\n  const [startupIntroEnabled, setStartupIntroEnabled] = useState(true);\n  \n  // Load settings on mount\n  useEffect(() => {\n    loadSettings();\n    loadClaudeBinaryPath();\n    loadAnalyticsSettings();\n    // Load tab persistence setting\n    setTabPersistenceEnabled(TabPersistenceService.isEnabled());\n    // Load startup intro setting (default to true if not set)\n    (async () => {\n      const pref = await api.getSetting('startup_intro_enabled');\n      setStartupIntroEnabled(pref === null ? true : pref === 'true');\n    })();\n  }, []);\n\n  /**\n   * Loads analytics settings\n   */\n  const loadAnalyticsSettings = async () => {\n    const settings = analytics.getSettings();\n    if (settings) {\n      setAnalyticsEnabled(settings.enabled);\n    }\n  };\n\n  /**\n   * Loads the current Claude binary path\n   */\n  const loadClaudeBinaryPath = async () => {\n    try {\n      const path = await api.getClaudeBinaryPath();\n      setCurrentBinaryPath(path);\n    } catch (err) {\n      console.error(\"Failed to load Claude binary path:\", err);\n    }\n  };\n\n  /**\n   * Loads the current Claude settings\n   */\n  const loadSettings = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const loadedSettings = await api.getClaudeSettings();\n      \n      // Ensure loadedSettings is an object\n      if (!loadedSettings || typeof loadedSettings !== 'object') {\n        console.warn(\"Loaded settings is not an object:\", loadedSettings);\n        setSettings({});\n        return;\n      }\n      \n      setSettings(loadedSettings);\n\n      // Parse permissions\n      if (loadedSettings.permissions && typeof loadedSettings.permissions === 'object') {\n        if (Array.isArray(loadedSettings.permissions.allow)) {\n          setAllowRules(\n            loadedSettings.permissions.allow.map((rule: string, index: number) => ({\n              id: `allow-${index}`,\n              value: rule,\n            }))\n          );\n        }\n        if (Array.isArray(loadedSettings.permissions.deny)) {\n          setDenyRules(\n            loadedSettings.permissions.deny.map((rule: string, index: number) => ({\n              id: `deny-${index}`,\n              value: rule,\n            }))\n          );\n        }\n      }\n\n      // Parse environment variables\n      if (loadedSettings.env && typeof loadedSettings.env === 'object' && !Array.isArray(loadedSettings.env)) {\n        setEnvVars(\n          Object.entries(loadedSettings.env).map(([key, value], index) => ({\n            id: `env-${index}`,\n            key,\n            value: value as string,\n          }))\n        );\n      }\n    } catch (err) {\n      console.error(\"Failed to load settings:\", err);\n      setError(\"Failed to load settings. Please ensure ~/.claude directory exists.\");\n      setSettings({});\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Saves the current settings\n   */\n  const saveSettings = async () => {\n    try {\n      setSaving(true);\n      setError(null);\n      setToast(null);\n\n      // Build the settings object\n      const updatedSettings: ClaudeSettings = {\n        ...settings,\n        permissions: {\n          allow: allowRules.map(rule => rule.value).filter(v => v && String(v).trim()),\n          deny: denyRules.map(rule => rule.value).filter(v => v && String(v).trim()),\n        },\n        env: envVars.reduce((acc, { key, value }) => {\n          if (key && String(key).trim() && value && String(value).trim()) {\n            acc[key] = String(value);\n          }\n          return acc;\n        }, {} as Record<string, string>),\n      };\n\n      await api.saveClaudeSettings(updatedSettings);\n      setSettings(updatedSettings);\n\n      // Save Claude binary path if changed\n      if (binaryPathChanged && selectedInstallation) {\n        await api.setClaudeBinaryPath(selectedInstallation.path);\n        setCurrentBinaryPath(selectedInstallation.path);\n        setBinaryPathChanged(false);\n      }\n\n      // Save user hooks if changed\n      if (userHooksChanged && getUserHooks.current) {\n        const hooks = getUserHooks.current();\n        await api.updateHooksConfig('user', hooks);\n        setUserHooksChanged(false);\n      }\n\n      // Save proxy settings if changed\n      if (proxySettingsChanged && saveProxySettings.current) {\n        await saveProxySettings.current();\n        setProxySettingsChanged(false);\n      }\n\n      setToast({ message: \"Settings saved successfully!\", type: \"success\" });\n    } catch (err) {\n      console.error(\"Failed to save settings:\", err);\n      setError(\"Failed to save settings.\");\n      setToast({ message: \"Failed to save settings\", type: \"error\" });\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  /**\n   * Updates a simple setting value\n   */\n  const updateSetting = (key: string, value: any) => {\n    setSettings(prev => ({ ...prev, [key]: value }));\n  };\n\n  /**\n   * Adds a new permission rule\n   */\n  const addPermissionRule = (type: \"allow\" | \"deny\") => {\n    const newRule: PermissionRule = {\n      id: `${type}-${Date.now()}`,\n      value: \"\",\n    };\n    \n    if (type === \"allow\") {\n      setAllowRules(prev => [...prev, newRule]);\n    } else {\n      setDenyRules(prev => [...prev, newRule]);\n    }\n  };\n\n  /**\n   * Updates a permission rule\n   */\n  const updatePermissionRule = (type: \"allow\" | \"deny\", id: string, value: string) => {\n    if (type === \"allow\") {\n      setAllowRules(prev => prev.map(rule => \n        rule.id === id ? { ...rule, value } : rule\n      ));\n    } else {\n      setDenyRules(prev => prev.map(rule => \n        rule.id === id ? { ...rule, value } : rule\n      ));\n    }\n  };\n\n  /**\n   * Removes a permission rule\n   */\n  const removePermissionRule = (type: \"allow\" | \"deny\", id: string) => {\n    if (type === \"allow\") {\n      setAllowRules(prev => prev.filter(rule => rule.id !== id));\n    } else {\n      setDenyRules(prev => prev.filter(rule => rule.id !== id));\n    }\n  };\n\n  /**\n   * Adds a new environment variable\n   */\n  const addEnvVar = () => {\n    const newVar: EnvironmentVariable = {\n      id: `env-${Date.now()}`,\n      key: \"\",\n      value: \"\",\n    };\n    setEnvVars(prev => [...prev, newVar]);\n  };\n\n  /**\n   * Updates an environment variable\n   */\n  const updateEnvVar = (id: string, field: \"key\" | \"value\", value: string) => {\n    setEnvVars(prev => prev.map(envVar => \n      envVar.id === id ? { ...envVar, [field]: value } : envVar\n    ));\n  };\n\n  /**\n   * Removes an environment variable\n   */\n  const removeEnvVar = (id: string) => {\n    setEnvVars(prev => prev.filter(envVar => envVar.id !== id));\n  };\n\n  /**\n   * Handle Claude installation selection\n   */\n  const handleClaudeInstallationSelect = (installation: ClaudeInstallation) => {\n    setSelectedInstallation(installation);\n    setBinaryPathChanged(installation.path !== currentBinaryPath);\n  };\n\n  return (\n    <div className={cn(\"h-full overflow-y-auto\", className)}>\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <h1 className=\"text-heading-1\">Settings</h1>\n              <p className=\"mt-1 text-body-small text-muted-foreground\">\n                Configure Claude Code preferences\n              </p>\n            </div>\n            <motion.div\n              whileTap={{ scale: 0.97 }}\n              transition={{ duration: 0.15 }}\n            >\n              <Button\n                onClick={saveSettings}\n                disabled={saving || loading}\n                size=\"default\"\n              >\n                {saving ? (\n                  <>\n                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                    Saving...\n                  </>\n                ) : (\n                  <>\n                    <Save className=\"mr-2 h-4 w-4\" />\n                    Save Settings\n                  </>\n                )}\n              </Button>\n            </motion.div>\n          </div>\n        </div>\n      \n      {/* Error message */}\n      <AnimatePresence>\n        {error && (\n          <motion.div\n            initial={{ opacity: 0, y: 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -8 }}\n            transition={{ duration: 0.15 }}\n            className=\"mx-4 mt-4 p-3 rounded-lg bg-destructive/10 border border-destructive/50 flex items-center gap-2 text-body-small text-destructive\"\n          >\n            <AlertCircle className=\"h-4 w-4\" />\n            {error}\n          </motion.div>\n        )}\n      </AnimatePresence>\n      \n      {/* Content */}\n      {loading ? (\n        <div className=\"flex-1 flex items-center justify-center\">\n          <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n        </div>\n      ) : (\n        <div className=\"flex-1 overflow-y-auto p-6\">\n          <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n            <TabsList className=\"grid grid-cols-8 w-full mb-6 h-auto p-1\">\n              <TabsTrigger value=\"general\" className=\"py-2.5 px-3\">General</TabsTrigger>\n              <TabsTrigger value=\"permissions\" className=\"py-2.5 px-3\">Permissions</TabsTrigger>\n              <TabsTrigger value=\"environment\" className=\"py-2.5 px-3\">Environment</TabsTrigger>\n              <TabsTrigger value=\"advanced\" className=\"py-2.5 px-3\">Advanced</TabsTrigger>\n              <TabsTrigger value=\"hooks\" className=\"py-2.5 px-3\">Hooks</TabsTrigger>\n              <TabsTrigger value=\"commands\" className=\"py-2.5 px-3\">Commands</TabsTrigger>\n              <TabsTrigger value=\"storage\" className=\"py-2.5 px-3\">Storage</TabsTrigger>\n              <TabsTrigger value=\"proxy\" className=\"py-2.5 px-3\">Proxy</TabsTrigger>\n            </TabsList>\n            \n            {/* General Settings */}\n            <TabsContent value=\"general\" className=\"space-y-6 mt-6\">\n              <Card className=\"p-6 space-y-6\">\n                <div>\n                  <h3 className=\"text-heading-4 mb-4\">General Settings</h3>\n                  \n                  <div className=\"space-y-4\">\n                    {/* Theme Selector */}\n                    <div className=\"flex items-center justify-between\">\n                      <div>\n                        <Label>Theme</Label>\n                        <p className=\"text-caption text-muted-foreground mt-1\">\n                          Choose your preferred color theme\n                        </p>\n                      </div>\n                      <div className=\"flex items-center gap-1 p-1 bg-muted/30 rounded-lg\">\n                        <button\n                          onClick={() => setTheme('dark')}\n                          className={cn(\n                            \"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all\",\n                            theme === 'dark' \n                              ? \"bg-background shadow-sm\" \n                              : \"hover:bg-background/50\"\n                          )}\n                        >\n                          {theme === 'dark' && <Check className=\"h-3 w-3\" />}\n                          Dark\n                        </button>\n                        <button\n                          onClick={() => setTheme('gray')}\n                          className={cn(\n                            \"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all\",\n                            theme === 'gray' \n                              ? \"bg-background shadow-sm\" \n                              : \"hover:bg-background/50\"\n                          )}\n                        >\n                          {theme === 'gray' && <Check className=\"h-3 w-3\" />}\n                          Gray\n                        </button>\n                        <button\n                          onClick={() => setTheme('light')}\n                          className={cn(\n                            \"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all\",\n                            theme === 'light' \n                              ? \"bg-background shadow-sm\" \n                              : \"hover:bg-background/50\"\n                          )}\n                        >\n                          {theme === 'light' && <Check className=\"h-3 w-3\" />}\n                          Light\n                        </button>\n                        <button\n                          onClick={() => setTheme('custom')}\n                          className={cn(\n                            \"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all\",\n                            theme === 'custom' \n                              ? \"bg-background shadow-sm\" \n                              : \"hover:bg-background/50\"\n                          )}\n                        >\n                          {theme === 'custom' && <Check className=\"h-3 w-3\" />}\n                          Custom\n                        </button>\n                      </div>\n                    </div>\n                    \n                    {/* Custom Color Editor */}\n                    {theme === 'custom' && (\n                      <div className=\"space-y-4 p-4 border rounded-lg bg-muted/20\">\n                        <h4 className=\"text-label\">Custom Theme Colors</h4>\n                        \n                        <div className=\"grid grid-cols-2 gap-4\">\n                          {/* Background Color */}\n                          <div className=\"space-y-2\">\n                            <Label htmlFor=\"color-background\" className=\"text-caption\">Background</Label>\n                            <div className=\"flex gap-2\">\n                              <Input\n                                id=\"color-background\"\n                                type=\"text\"\n                                value={customColors.background}\n                                onChange={(e) => setCustomColors({ background: e.target.value })}\n                                placeholder=\"oklch(0.12 0.01 240)\"\n                                className=\"font-mono text-xs\"\n                              />\n                              <div \n                                className=\"w-10 h-10 rounded border\"\n                                style={{ backgroundColor: customColors.background }}\n                              />\n                            </div>\n                          </div>\n                          \n                          {/* Foreground Color */}\n                          <div className=\"space-y-2\">\n                            <Label htmlFor=\"color-foreground\" className=\"text-caption\">Foreground</Label>\n                            <div className=\"flex gap-2\">\n                              <Input\n                                id=\"color-foreground\"\n                                type=\"text\"\n                                value={customColors.foreground}\n                                onChange={(e) => setCustomColors({ foreground: e.target.value })}\n                                placeholder=\"oklch(0.98 0.01 240)\"\n                                className=\"font-mono text-xs\"\n                              />\n                              <div \n                                className=\"w-10 h-10 rounded border\"\n                                style={{ backgroundColor: customColors.foreground }}\n                              />\n                            </div>\n                          </div>\n                          \n                          {/* Primary Color */}\n                          <div className=\"space-y-2\">\n                            <Label htmlFor=\"color-primary\" className=\"text-caption\">Primary</Label>\n                            <div className=\"flex gap-2\">\n                              <Input\n                                id=\"color-primary\"\n                                type=\"text\"\n                                value={customColors.primary}\n                                onChange={(e) => setCustomColors({ primary: e.target.value })}\n                                placeholder=\"oklch(0.98 0.01 240)\"\n                                className=\"font-mono text-xs\"\n                              />\n                              <div \n                                className=\"w-10 h-10 rounded border\"\n                                style={{ backgroundColor: customColors.primary }}\n                              />\n                            </div>\n                          </div>\n                          \n                          {/* Card Color */}\n                          <div className=\"space-y-2\">\n                            <Label htmlFor=\"color-card\" className=\"text-caption\">Card</Label>\n                            <div className=\"flex gap-2\">\n                              <Input\n                                id=\"color-card\"\n                                type=\"text\"\n                                value={customColors.card}\n                                onChange={(e) => setCustomColors({ card: e.target.value })}\n                                placeholder=\"oklch(0.14 0.01 240)\"\n                                className=\"font-mono text-xs\"\n                              />\n                              <div \n                                className=\"w-10 h-10 rounded border\"\n                                style={{ backgroundColor: customColors.card }}\n                              />\n                            </div>\n                          </div>\n                          \n                          {/* Accent Color */}\n                          <div className=\"space-y-2\">\n                            <Label htmlFor=\"color-accent\" className=\"text-caption\">Accent</Label>\n                            <div className=\"flex gap-2\">\n                              <Input\n                                id=\"color-accent\"\n                                type=\"text\"\n                                value={customColors.accent}\n                                onChange={(e) => setCustomColors({ accent: e.target.value })}\n                                placeholder=\"oklch(0.16 0.01 240)\"\n                                className=\"font-mono text-xs\"\n                              />\n                              <div \n                                className=\"w-10 h-10 rounded border\"\n                                style={{ backgroundColor: customColors.accent }}\n                              />\n                            </div>\n                          </div>\n                          \n                          {/* Destructive Color */}\n                          <div className=\"space-y-2\">\n                            <Label htmlFor=\"color-destructive\" className=\"text-caption\">Destructive</Label>\n                            <div className=\"flex gap-2\">\n                              <Input\n                                id=\"color-destructive\"\n                                type=\"text\"\n                                value={customColors.destructive}\n                                onChange={(e) => setCustomColors({ destructive: e.target.value })}\n                                placeholder=\"oklch(0.6 0.2 25)\"\n                                className=\"font-mono text-xs\"\n                              />\n                              <div \n                                className=\"w-10 h-10 rounded border\"\n                                style={{ backgroundColor: customColors.destructive }}\n                              />\n                            </div>\n                          </div>\n                        </div>\n                        \n                        <p className=\"text-caption text-muted-foreground\">\n                          Use CSS color values (hex, rgb, oklch, etc.). Changes apply immediately.\n                        </p>\n                      </div>\n                    )}\n                    \n                    {/* Include Co-authored By */}\n                    <div className=\"flex items-center justify-between\">\n                      <div className=\"space-y-0.5 flex-1\">\n                        <Label htmlFor=\"coauthored\">Include \"Co-authored by Claude\"</Label>\n                        <p className=\"text-caption text-muted-foreground\">\n                          Add Claude attribution to git commits and pull requests\n                        </p>\n                      </div>\n                      <Switch\n                        id=\"coauthored\"\n                        checked={settings?.includeCoAuthoredBy !== false}\n                        onCheckedChange={(checked) => updateSetting(\"includeCoAuthoredBy\", checked)}\n                      />\n                    </div>\n                    \n                    {/* Verbose Output */}\n                    <div className=\"flex items-center justify-between\">\n                      <div className=\"space-y-0.5 flex-1\">\n                        <Label htmlFor=\"verbose\">Verbose Output</Label>\n                        <p className=\"text-caption text-muted-foreground\">\n                          Show full bash and command outputs\n                        </p>\n                      </div>\n                      <Switch\n                        id=\"verbose\"\n                        checked={settings?.verbose === true}\n                        onCheckedChange={(checked) => updateSetting(\"verbose\", checked)}\n                      />\n                    </div>\n                    \n                    {/* Cleanup Period */}\n                    <div className=\"space-y-2\">\n                      <div className=\"flex items-center justify-between\">\n                        <div className=\"flex-1\">\n                          <Label htmlFor=\"cleanup\">Chat Transcript Retention (days)</Label>\n                          <p className=\"text-caption text-muted-foreground mt-1\">\n                            How long to retain chat transcripts locally (default: 30 days)\n                          </p>\n                        </div>\n                        <Input\n                          id=\"cleanup\"\n                          type=\"number\"\n                          min=\"1\"\n                          placeholder=\"30\"\n                          value={settings?.cleanupPeriodDays || \"\"}\n                          onChange={(e) => {\n                            const value = e.target.value ? parseInt(e.target.value) : undefined;\n                            updateSetting(\"cleanupPeriodDays\", value);\n                          }}\n                          className=\"w-24\"\n                        />\n                      </div>\n                    </div>\n                    \n                    {/* Claude Binary Path Selector */}\n                    <div className=\"space-y-3\">\n                      <ClaudeVersionSelector\n                        selectedPath={currentBinaryPath}\n                        onSelect={handleClaudeInstallationSelect}\n                        simplified={true}\n                      />\n                      {binaryPathChanged && (\n                        <p className=\"text-caption text-amber-600 dark:text-amber-400 flex items-center gap-1\">\n                          <AlertCircle className=\"h-3 w-3\" />\n                          Changes will be applied when you save settings.\n                        </p>\n                      )}\n                    </div>\n\n                    {/* Separator */}\n                    <div className=\"border-t border-border pt-4 mt-6\" />\n                    \n                    {/* Analytics Toggle */}\n                    <div className=\"flex items-center justify-between\">\n                      <div className=\"space-y-1\">\n                        <Label htmlFor=\"analytics-enabled\">Enable Analytics</Label>\n                        <p className=\"text-caption text-muted-foreground\">\n                          Help improve opcode by sharing anonymous usage data\n                        </p>\n                      </div>\n                      <Switch\n                        id=\"analytics-enabled\"\n                        checked={analyticsEnabled}\n                        onCheckedChange={async (checked) => {\n                          if (checked) {\n                            await analytics.enable();\n                            setAnalyticsEnabled(true);\n                            trackEvent.settingsChanged('analytics_enabled', true);\n                            setToast({ message: \"Analytics enabled\", type: \"success\" });\n                          } else {\n                            await analytics.disable();\n                            setAnalyticsEnabled(false);\n                            trackEvent.settingsChanged('analytics_enabled', false);\n                            setToast({ message: \"Analytics disabled\", type: \"success\" });\n                          }\n                        }}\n                      />\n                    </div>\n                    \n                    {/* Privacy Info */}\n                    {analyticsEnabled && (\n                      <div className=\"rounded-lg border border-border bg-muted/50 p-3\">\n                        <div className=\"flex gap-2\">\n                          <Shield className=\"h-4 w-4 text-primary flex-shrink-0 mt-0.5\" />\n                          <div className=\"space-y-1\">\n                            <p className=\"text-xs font-medium text-foreground\">Your privacy is protected</p>\n                            <ul className=\"text-xs text-muted-foreground space-y-0.5\">\n                              <li>• No personal information or file contents collected</li>\n                              <li>• All data is anonymous with random IDs</li>\n                              <li>• You can disable analytics at any time</li>\n                            </ul>\n                          </div>\n                        </div>\n                      </div>\n                    )}\n                    \n                    {/* Tab Persistence Toggle */}\n                    <div className=\"flex items-center justify-between\">\n                      <div className=\"space-y-1\">\n                        <Label htmlFor=\"tab-persistence\">Remember Open Tabs</Label>\n                        <p className=\"text-caption text-muted-foreground\">\n                          Restore your tabs when you restart the app\n                        </p>\n                      </div>\n                      <Switch\n                        id=\"tab-persistence\"\n                        checked={tabPersistenceEnabled}\n                        onCheckedChange={(checked) => {\n                          TabPersistenceService.setEnabled(checked);\n                          setTabPersistenceEnabled(checked);\n                          trackEvent.settingsChanged('tab_persistence_enabled', checked);\n                          setToast({ \n                            message: checked \n                              ? \"Tab persistence enabled - your tabs will be restored on restart\" \n                              : \"Tab persistence disabled - tabs will not be saved\", \n                            type: \"success\" \n                          });\n                        }}\n                      />\n                    </div>\n\n                    {/* Startup Intro Toggle */}\n                    <div className=\"flex items-center justify-between\">\n                      <div className=\"space-y-1\">\n                        <Label htmlFor=\"startup-intro\">Show Welcome Intro on Startup</Label>\n                        <p className=\"text-caption text-muted-foreground\">\n                          Display a brief welcome animation when the app launches\n                        </p>\n                      </div>\n                      <Switch\n                        id=\"startup-intro\"\n                        checked={startupIntroEnabled}\n                        onCheckedChange={async (checked) => {\n                          setStartupIntroEnabled(checked);\n                          try {\n                            await api.saveSetting('startup_intro_enabled', checked ? 'true' : 'false');\n                            trackEvent.settingsChanged('startup_intro_enabled', checked);\n                            setToast({ \n                              message: checked \n                                ? 'Welcome intro enabled' \n                                : 'Welcome intro disabled', \n                              type: 'success' \n                            });\n                          } catch (e) {\n                            setToast({ message: 'Failed to update preference', type: 'error' });\n                          }\n                        }}\n                      />\n                    </div>\n                  </div>\n                </div>\n              </Card>\n            </TabsContent>\n            \n            {/* Permissions Settings */}\n            <TabsContent value=\"permissions\" className=\"space-y-6\">\n              <Card className=\"p-6\">\n                <div className=\"space-y-6\">\n                  <div>\n                    <h3 className=\"text-heading-4 mb-2\">Permission Rules</h3>\n                    <p className=\"text-body-small text-muted-foreground mb-4\">\n                      Control which tools Claude Code can use without manual approval\n                    </p>\n                  </div>\n                  \n                  {/* Allow Rules */}\n                  <div className=\"space-y-3\">\n                    <div className=\"flex items-center justify-between\">\n                      <Label className=\"text-label text-green-500\">Allow Rules</Label>\n                      <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => addPermissionRule(\"allow\")}\n                        className=\"gap-2 hover:border-green-500/50 hover:text-green-500\"\n                      >\n                        <Plus className=\"h-3 w-3\" />\n                        Add Rule\n                      </Button>\n                    </div>\n                    <div className=\"space-y-2\">\n                      {allowRules.length === 0 ? (\n                        <p className=\"text-xs text-muted-foreground py-2\">\n                          No allow rules configured. Claude will ask for approval for all tools.\n                        </p>\n                      ) : (\n                        allowRules.map((rule) => (\n                          <motion.div\n                            key={rule.id}\n                            initial={{ opacity: 0, x: -8 }}\n                            animate={{ opacity: 1, x: 0 }}\n                            transition={{ duration: 0.15 }}\n                            className=\"flex items-center gap-2\"\n                          >\n                            <Input\n                              placeholder=\"e.g., Bash(npm run test:*)\"\n                              value={rule.value}\n                              onChange={(e) => updatePermissionRule(\"allow\", rule.id, e.target.value)}\n                              className=\"flex-1\"\n                            />\n                            <Button\n                              variant=\"ghost\"\n                              size=\"icon\"\n                              onClick={() => removePermissionRule(\"allow\", rule.id)}\n                              className=\"h-8 w-8\"\n                            >\n                              <Trash2 className=\"h-4 w-4\" />\n                            </Button>\n                          </motion.div>\n                        ))\n                      )}\n                    </div>\n                  </div>\n                  \n                  {/* Deny Rules */}\n                  <div className=\"space-y-3\">\n                    <div className=\"flex items-center justify-between\">\n                      <Label className=\"text-label text-red-500\">Deny Rules</Label>\n                      <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => addPermissionRule(\"deny\")}\n                        className=\"gap-2 hover:border-red-500/50 hover:text-red-500\"\n                      >\n                        <Plus className=\"h-3 w-3\" />\n                        Add Rule\n                      </Button>\n                    </div>\n                    <div className=\"space-y-2\">\n                      {denyRules.length === 0 ? (\n                        <p className=\"text-xs text-muted-foreground py-2\">\n                          No deny rules configured.\n                        </p>\n                      ) : (\n                        denyRules.map((rule) => (\n                          <motion.div\n                            key={rule.id}\n                            initial={{ opacity: 0, x: -8 }}\n                            animate={{ opacity: 1, x: 0 }}\n                            transition={{ duration: 0.15 }}\n                            className=\"flex items-center gap-2\"\n                          >\n                            <Input\n                              placeholder=\"e.g., Bash(curl:*)\"\n                              value={rule.value}\n                              onChange={(e) => updatePermissionRule(\"deny\", rule.id, e.target.value)}\n                              className=\"flex-1\"\n                            />\n                            <Button\n                              variant=\"ghost\"\n                              size=\"icon\"\n                              onClick={() => removePermissionRule(\"deny\", rule.id)}\n                              className=\"h-8 w-8\"\n                            >\n                              <Trash2 className=\"h-4 w-4\" />\n                            </Button>\n                          </motion.div>\n                        ))\n                      )}\n                    </div>\n                  </div>\n                  \n                  <div className=\"pt-2 space-y-2\">\n                    <p className=\"text-xs text-muted-foreground\">\n                      <strong>Examples:</strong>\n                    </p>\n                    <ul className=\"text-caption text-muted-foreground space-y-1 ml-4\">\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400\">Bash</code> - Allow all bash commands</li>\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400\">Bash(npm run build)</code> - Allow exact command</li>\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400\">Bash(npm run test:*)</code> - Allow commands with prefix</li>\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400\">Read(~/.zshrc)</code> - Allow reading specific file</li>\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400\">Edit(docs/**)</code> - Allow editing files in docs directory</li>\n                    </ul>\n                  </div>\n                </div>\n              </Card>\n            </TabsContent>\n            \n            {/* Environment Variables */}\n            <TabsContent value=\"environment\" className=\"space-y-6\">\n              <Card className=\"p-6\">\n                <div className=\"space-y-6\">\n                  <div className=\"flex items-center justify-between\">\n                    <div>\n                      <h3 className=\"text-heading-4\">Environment Variables</h3>\n                      <p className=\"text-sm text-muted-foreground mt-1\">\n                        Environment variables applied to every Claude Code session\n                      </p>\n                    </div>\n                    <Button\n                      variant=\"outline\"\n                      size=\"sm\"\n                      onClick={addEnvVar}\n                      className=\"gap-2\"\n                    >\n                      <Plus className=\"h-3 w-3\" />\n                      Add Variable\n                    </Button>\n                  </div>\n                  \n                  <div className=\"space-y-3\">\n                    {envVars.length === 0 ? (\n                      <p className=\"text-xs text-muted-foreground py-2\">\n                        No environment variables configured.\n                      </p>\n                    ) : (\n                      envVars.map((envVar) => (\n                        <motion.div\n                          key={envVar.id}\n                          initial={{ opacity: 0, x: -20 }}\n                          animate={{ opacity: 1, x: 0 }}\n                          className=\"flex items-center gap-2\"\n                        >\n                          <Input\n                            placeholder=\"KEY\"\n                            value={envVar.key}\n                            onChange={(e) => updateEnvVar(envVar.id, \"key\", e.target.value)}\n                            className=\"flex-1 font-mono text-sm\"\n                          />\n                          <span className=\"text-muted-foreground\">=</span>\n                          <Input\n                            placeholder=\"value\"\n                            value={envVar.value}\n                            onChange={(e) => updateEnvVar(envVar.id, \"value\", e.target.value)}\n                            className=\"flex-1 font-mono text-sm\"\n                          />\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={() => removeEnvVar(envVar.id)}\n                            className=\"h-8 w-8 hover:text-destructive\"\n                          >\n                            <Trash2 className=\"h-4 w-4\" />\n                          </Button>\n                        </motion.div>\n                      ))\n                    )}\n                  </div>\n                  \n                  <div className=\"pt-2 space-y-2\">\n                    <p className=\"text-xs text-muted-foreground\">\n                      <strong>Common variables:</strong>\n                    </p>\n                    <ul className=\"text-caption text-muted-foreground space-y-1 ml-4\">\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400\">CLAUDE_CODE_ENABLE_TELEMETRY</code> - Enable/disable telemetry (0 or 1)</li>\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400\">ANTHROPIC_MODEL</code> - Custom model name</li>\n                      <li>• <code className=\"px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400\">DISABLE_COST_WARNINGS</code> - Disable cost warnings (1)</li>\n                    </ul>\n                  </div>\n                </div>\n              </Card>\n            </TabsContent>\n            {/* Advanced Settings */}\n            <TabsContent value=\"advanced\" className=\"space-y-6\">\n              <Card className=\"p-6\">\n                <div className=\"space-y-6\">\n                  <div>\n                    <h3 className=\"text-base font-semibold mb-4\">Advanced Settings</h3>\n                    <p className=\"text-sm text-muted-foreground mb-6\">\n                      Additional configuration options for advanced users\n                    </p>\n                  </div>\n                  \n                  {/* API Key Helper */}\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"apiKeyHelper\">API Key Helper Script</Label>\n                    <Input\n                      id=\"apiKeyHelper\"\n                      placeholder=\"/path/to/generate_api_key.sh\"\n                      value={settings?.apiKeyHelper || \"\"}\n                      onChange={(e) => updateSetting(\"apiKeyHelper\", e.target.value || undefined)}\n                    />\n                    <p className=\"text-xs text-muted-foreground\">\n                      Custom script to generate auth values for API requests\n                    </p>\n                  </div>\n                  \n                  {/* Raw JSON Editor */}\n                  <div className=\"space-y-2\">\n                    <Label>Raw Settings (JSON)</Label>\n                    <div className=\"p-3 rounded-md bg-muted font-mono text-xs overflow-x-auto whitespace-pre-wrap\">\n                      <pre>{JSON.stringify(settings, null, 2)}</pre>\n                    </div>\n                    <p className=\"text-xs text-muted-foreground\">\n                      This shows the raw JSON that will be saved to ~/.claude/settings.json\n                    </p>\n                  </div>\n                </div>\n              </Card>\n            </TabsContent>\n            \n            {/* Hooks Settings */}\n            <TabsContent value=\"hooks\" className=\"space-y-6\">\n              <Card className=\"p-6\">\n                <div className=\"space-y-4\">\n                  <div>\n                    <h3 className=\"text-base font-semibold mb-2\">User Hooks</h3>\n                    <p className=\"text-body-small text-muted-foreground mb-4\">\n                      Configure hooks that apply to all Claude Code sessions for your user account.\n                      These are stored in <code className=\"mx-1 px-2 py-1 bg-muted rounded text-xs\">~/.claude/settings.json</code>\n                    </p>\n                  </div>\n                  \n                  <HooksEditor\n                    key={activeTab}\n                    scope=\"user\"\n                    className=\"border-0\"\n                    hideActions={true}\n                    onChange={(hasChanges, getHooks) => {\n                      setUserHooksChanged(hasChanges);\n                      getUserHooks.current = getHooks;\n                    }}\n                  />\n                </div>\n              </Card>\n            </TabsContent>\n            \n            {/* Commands Tab */}\n            <TabsContent value=\"commands\">\n              <Card className=\"p-6\">\n                <SlashCommandsManager className=\"p-0\" />\n              </Card>\n            </TabsContent>\n            \n            {/* Storage Tab */}\n            <TabsContent value=\"storage\">\n              <StorageTab />\n            </TabsContent>\n            \n            {/* Proxy Settings */}\n            <TabsContent value=\"proxy\">\n              <Card className=\"p-6\">\n                <ProxySettings \n                  setToast={setToast}\n                  onChange={(hasChanges, _getSettings, save) => {\n                    setProxySettingsChanged(hasChanges);\n                    saveProxySettings.current = save;\n                  }}\n                />\n              </Card>\n            </TabsContent>\n            \n          </Tabs>\n        </div>\n      )}\n      </div>\n      \n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n      \n      \n    </div>\n  );\n}; \n"
  },
  {
    "path": "src/components/SlashCommandPicker.tsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { api } from \"@/lib/api\";\nimport { \n  X, \n  Command,\n  Search,\n  Globe,\n  FolderOpen,\n  Zap,\n  FileCode,\n  Terminal,\n  AlertCircle,\n  User,\n  Building2\n} from \"lucide-react\";\nimport type { SlashCommand } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { useTrackEvent, useFeatureAdoptionTracking } from \"@/hooks\";\n\ninterface SlashCommandPickerProps {\n  /**\n   * The project path for loading project-specific commands\n   */\n  projectPath?: string;\n  /**\n   * Callback when a command is selected\n   */\n  onSelect: (command: SlashCommand) => void;\n  /**\n   * Callback to close the picker\n   */\n  onClose: () => void;\n  /**\n   * Initial search query (text after /)\n   */\n  initialQuery?: string;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n// Get icon for command based on its properties\nconst getCommandIcon = (command: SlashCommand) => {\n  // If it has bash commands, show terminal icon\n  if (command.has_bash_commands) return Terminal;\n  \n  // If it has file references, show file icon\n  if (command.has_file_references) return FileCode;\n  \n  // If it accepts arguments, show zap icon\n  if (command.accepts_arguments) return Zap;\n  \n  // Based on scope\n  if (command.scope === \"project\") return FolderOpen;\n  if (command.scope === \"user\") return Globe;\n  \n  // Default\n  return Command;\n};\n\n/**\n * SlashCommandPicker component - Autocomplete UI for slash commands\n * \n * @example\n * <SlashCommandPicker\n *   projectPath=\"/Users/example/project\"\n *   onSelect={(command) => console.log('Selected:', command)}\n *   onClose={() => setShowPicker(false)}\n * />\n */\nexport const SlashCommandPicker: React.FC<SlashCommandPickerProps> = ({\n  projectPath,\n  onSelect,\n  onClose,\n  initialQuery = \"\",\n  className,\n}) => {\n  const [commands, setCommands] = useState<SlashCommand[]>([]);\n  const [filteredCommands, setFilteredCommands] = useState<SlashCommand[]>([]);\n  const [isLoading, setIsLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [searchQuery, setSearchQuery] = useState(initialQuery);\n  const [activeTab, setActiveTab] = useState<string>(\"custom\");\n  \n  const commandListRef = useRef<HTMLDivElement>(null);\n  \n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n  const slashCommandFeatureTracking = useFeatureAdoptionTracking('slash_commands');\n  \n  // Load commands on mount or when project path changes\n  useEffect(() => {\n    loadCommands();\n  }, [projectPath]);\n  \n  // Filter commands based on search query and active tab\n  useEffect(() => {\n    if (!commands.length) {\n      setFilteredCommands([]);\n      return;\n    }\n    \n    const query = searchQuery.toLowerCase();\n    let filteredByTab: SlashCommand[];\n    \n    // Filter by active tab\n    if (activeTab === \"default\") {\n      // Show default/built-in commands\n      filteredByTab = commands.filter(cmd => cmd.scope === \"default\");\n    } else {\n      // Show all custom commands (both user and project)\n      filteredByTab = commands.filter(cmd => cmd.scope !== \"default\");\n    }\n    \n    // Then filter by search query\n    let filtered: SlashCommand[];\n    if (!query) {\n      filtered = filteredByTab;\n    } else {\n      filtered = filteredByTab.filter(cmd => {\n        // Match against command name\n        if (cmd.name.toLowerCase().includes(query)) return true;\n        \n        // Match against full command\n        if (cmd.full_command.toLowerCase().includes(query)) return true;\n        \n        // Match against namespace\n        if (cmd.namespace && cmd.namespace.toLowerCase().includes(query)) return true;\n        \n        // Match against description\n        if (cmd.description && cmd.description.toLowerCase().includes(query)) return true;\n        \n        return false;\n      });\n      \n      // Sort by relevance\n      filtered.sort((a, b) => {\n        // Exact name match first\n        const aExact = a.name.toLowerCase() === query;\n        const bExact = b.name.toLowerCase() === query;\n        if (aExact && !bExact) return -1;\n        if (!aExact && bExact) return 1;\n        \n        // Then by name starts with\n        const aStarts = a.name.toLowerCase().startsWith(query);\n        const bStarts = b.name.toLowerCase().startsWith(query);\n        if (aStarts && !bStarts) return -1;\n        if (!aStarts && bStarts) return 1;\n        \n        // Then alphabetically\n        return a.name.localeCompare(b.name);\n      });\n    }\n    \n    setFilteredCommands(filtered);\n    \n    // Reset selected index when filtered list changes\n    setSelectedIndex(0);\n  }, [searchQuery, commands, activeTab]);\n  \n  // Keyboard navigation\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      switch (e.key) {\n        case 'Escape':\n          e.preventDefault();\n          onClose();\n          break;\n          \n        case 'Enter':\n          e.preventDefault();\n          if (filteredCommands.length > 0 && selectedIndex < filteredCommands.length) {\n            const command = filteredCommands[selectedIndex];\n            trackEvent.slashCommandSelected({\n              command_name: command.name,\n              selection_method: 'keyboard'\n            });\n            slashCommandFeatureTracking.trackUsage();\n            onSelect(command);\n          }\n          break;\n          \n        case 'ArrowUp':\n          e.preventDefault();\n          setSelectedIndex(prev => Math.max(0, prev - 1));\n          break;\n          \n        case 'ArrowDown':\n          e.preventDefault();\n          setSelectedIndex(prev => Math.min(filteredCommands.length - 1, prev + 1));\n          break;\n      }\n    };\n    \n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [filteredCommands, selectedIndex, onSelect, onClose]);\n  \n  // Scroll selected item into view\n  useEffect(() => {\n    if (commandListRef.current) {\n      const selectedElement = commandListRef.current.querySelector(`[data-index=\"${selectedIndex}\"]`);\n      if (selectedElement) {\n        selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n      }\n    }\n  }, [selectedIndex]);\n  \n  const loadCommands = async () => {\n    try {\n      setIsLoading(true);\n      setError(null);\n      \n      // Always load fresh commands from filesystem\n      const loadedCommands = await api.slashCommandsList(projectPath);\n      setCommands(loadedCommands);\n    } catch (err) {\n      console.error(\"Failed to load slash commands:\", err);\n      setError(err instanceof Error ? err.message : 'Failed to load commands');\n      setCommands([]);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n  \n  const handleCommandClick = (command: SlashCommand) => {\n    trackEvent.slashCommandSelected({\n      command_name: command.name,\n      selection_method: 'click'\n    });\n    slashCommandFeatureTracking.trackUsage();\n    onSelect(command);\n  };\n  \n  // Group commands by scope and namespace for the Custom tab\n  const groupedCommands = filteredCommands.reduce((acc, cmd) => {\n    let key: string;\n    if (cmd.scope === \"user\") {\n      key = cmd.namespace ? `User Commands: ${cmd.namespace}` : \"User Commands\";\n    } else if (cmd.scope === \"project\") {\n      key = cmd.namespace ? `Project Commands: ${cmd.namespace}` : \"Project Commands\";\n    } else {\n      key = cmd.namespace || \"Commands\";\n    }\n    \n    if (!acc[key]) {\n      acc[key] = [];\n    }\n    acc[key].push(cmd);\n    return acc;\n  }, {} as Record<string, SlashCommand[]>);\n  \n  // Update search query from parent\n  useEffect(() => {\n    setSearchQuery(initialQuery);\n  }, [initialQuery]);\n  \n  return (\n    <motion.div\n      initial={{ opacity: 0, scale: 0.95 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={{ opacity: 0, scale: 0.95 }}\n      className={cn(\n        \"absolute bottom-full mb-2 left-0 z-50\",\n        \"w-[600px] h-[400px]\",\n        \"bg-background border border-border rounded-lg shadow-lg\",\n        \"flex flex-col overflow-hidden\",\n        className\n      )}\n    >\n      {/* Header */}\n      <div className=\"border-b border-border p-3\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-2\">\n            <Command className=\"h-4 w-4 text-muted-foreground\" />\n            <span className=\"text-sm font-medium\">Slash Commands</span>\n            {searchQuery && (\n              <span className=\"text-xs text-muted-foreground\">\n                Searching: \"{searchQuery}\"\n              </span>\n            )}\n          </div>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={onClose}\n            className=\"h-8 w-8\"\n          >\n            <X className=\"h-4 w-4\" />\n          </Button>\n        </div>\n        \n        {/* Tabs */}\n        <div className=\"mt-3\">\n          <Tabs value={activeTab} onValueChange={setActiveTab}>\n            <TabsList className=\"grid w-full grid-cols-2\">\n              <TabsTrigger value=\"default\">Default</TabsTrigger>\n              <TabsTrigger value=\"custom\">Custom</TabsTrigger>\n            </TabsList>\n          </Tabs>\n        </div>\n      </div>\n\n      {/* Command List */}\n      <div className=\"flex-1 overflow-y-auto relative\">\n        {isLoading && (\n          <div className=\"flex items-center justify-center h-full\">\n            <span className=\"text-sm text-muted-foreground\">Loading commands...</span>\n          </div>\n        )}\n\n        {error && (\n          <div className=\"flex flex-col items-center justify-center h-full p-4\">\n            <AlertCircle className=\"h-8 w-8 text-destructive mb-2\" />\n            <span className=\"text-sm text-destructive text-center\">{error}</span>\n          </div>\n        )}\n\n        {!isLoading && !error && (\n          <>\n            {/* Default Tab Content */}\n            {activeTab === \"default\" && (\n              <>\n                {filteredCommands.length === 0 && (\n                  <div className=\"flex flex-col items-center justify-center h-full\">\n                    <Command className=\"h-8 w-8 text-muted-foreground mb-2\" />\n                    <span className=\"text-sm text-muted-foreground\">\n                      {searchQuery ? 'No commands found' : 'No default commands available'}\n                    </span>\n                    {!searchQuery && (\n                      <p className=\"text-xs text-muted-foreground mt-2 text-center px-4\">\n                        Default commands are built-in system commands\n                      </p>\n                    )}\n                  </div>\n                )}\n\n                {filteredCommands.length > 0 && (\n                  <div className=\"p-2\" ref={commandListRef}>\n                    <div className=\"space-y-0.5\">\n                      {filteredCommands.map((command, index) => {\n                        const Icon = getCommandIcon(command);\n                        const isSelected = index === selectedIndex;\n                        \n                        return (\n                          <button\n                            key={command.id}\n                            data-index={index}\n                            onClick={() => handleCommandClick(command)}\n                            onMouseEnter={() => setSelectedIndex(index)}\n                            className={cn(\n                              \"w-full flex items-start gap-3 px-3 py-2 rounded-md\",\n                              \"hover:bg-accent transition-colors\",\n                              \"text-left\",\n                              isSelected && \"bg-accent\"\n                            )}\n                          >\n                            <Icon className=\"h-4 w-4 text-muted-foreground mt-1 flex-shrink-0\" />\n                            <div className=\"flex-1 overflow-hidden\">\n                              <div className=\"flex items-center gap-2\">\n                                <span className=\"font-medium\">\n                                  {command.full_command}\n                                </span>\n                                <span className=\"text-xs text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n                                  {command.scope}\n                                </span>\n                              </div>\n                              {command.description && (\n                                <p className=\"text-xs text-muted-foreground mt-1 leading-relaxed\">\n                                  {command.description}\n                                </p>\n                              )}\n                            </div>\n                          </button>\n                        );\n                      })}\n                    </div>\n                  </div>\n                )}\n              </>\n            )}\n            \n            {/* Custom Tab Content */}\n            {activeTab === \"custom\" && (\n              <>\n                {filteredCommands.length === 0 && (\n                  <div className=\"flex flex-col items-center justify-center h-full\">\n                    <Search className=\"h-8 w-8 text-muted-foreground mb-2\" />\n                    <span className=\"text-sm text-muted-foreground\">\n                      {searchQuery ? 'No commands found' : 'No custom commands available'}\n                    </span>\n                    {!searchQuery && (\n                      <p className=\"text-xs text-muted-foreground mt-2 text-center px-4\">\n                        Create commands in <code className=\"px-1\">.claude/commands/</code> or <code className=\"px-1\">~/.claude/commands/</code>\n                      </p>\n                    )}\n                  </div>\n                )}\n\n                {filteredCommands.length > 0 && (\n                  <div className=\"p-2\" ref={commandListRef}>\n                    {/* If no grouping needed, show flat list */}\n                    {Object.keys(groupedCommands).length === 1 ? (\n                      <div className=\"space-y-0.5\">\n                        {filteredCommands.map((command, index) => {\n                          const Icon = getCommandIcon(command);\n                          const isSelected = index === selectedIndex;\n                          \n                          return (\n                            <button\n                              key={command.id}\n                              data-index={index}\n                              onClick={() => handleCommandClick(command)}\n                              onMouseEnter={() => setSelectedIndex(index)}\n                              className={cn(\n                                \"w-full flex items-start gap-3 px-3 py-2 rounded-md\",\n                                \"hover:bg-accent transition-colors\",\n                                \"text-left\",\n                                isSelected && \"bg-accent\"\n                              )}\n                            >\n                              <Icon className=\"h-4 w-4 mt-0.5 flex-shrink-0 text-muted-foreground\" />\n                              \n                              <div className=\"flex-1 min-w-0\">\n                                <div className=\"flex items-baseline gap-2\">\n                                  <span className=\"font-mono text-sm text-primary\">\n                                    {command.full_command}\n                                  </span>\n                                  {command.accepts_arguments && (\n                                    <span className=\"text-xs text-muted-foreground\">\n                                      [args]\n                                    </span>\n                                  )}\n                                </div>\n                                \n                                {command.description && (\n                                  <p className=\"text-xs text-muted-foreground mt-0.5 truncate\">\n                                    {command.description}\n                                  </p>\n                                )}\n                                \n                                <div className=\"flex items-center gap-3 mt-1\">\n                                  {command.allowed_tools.length > 0 && (\n                                    <span className=\"text-xs text-muted-foreground\">\n                                      {command.allowed_tools.length} tool{command.allowed_tools.length === 1 ? '' : 's'}\n                                    </span>\n                                  )}\n                                  \n                                  {command.has_bash_commands && (\n                                    <span className=\"text-xs text-blue-600 dark:text-blue-400\">\n                                      Bash\n                                    </span>\n                                  )}\n                                  \n                                  {command.has_file_references && (\n                                    <span className=\"text-xs text-green-600 dark:text-green-400\">\n                                      Files\n                                    </span>\n                                  )}\n                                </div>\n                              </div>\n                            </button>\n                          );\n                        })}\n                      </div>\n                    ) : (\n                      // Show grouped by scope/namespace\n                      <div className=\"space-y-4\">\n                        {Object.entries(groupedCommands).map(([groupKey, groupCommands]) => (\n                          <div key={groupKey}>\n                            <h3 className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider px-3 mb-1 flex items-center gap-2\">\n                              {groupKey.startsWith(\"User Commands\") && <User className=\"h-3 w-3\" />}\n                              {groupKey.startsWith(\"Project Commands\") && <Building2 className=\"h-3 w-3\" />}\n                              {groupKey}\n                            </h3>\n                            \n                            <div className=\"space-y-0.5\">\n                              {groupCommands.map((command) => {\n                                const Icon = getCommandIcon(command);\n                                const globalIndex = filteredCommands.indexOf(command);\n                                const isSelected = globalIndex === selectedIndex;\n                                \n                                return (\n                                  <button\n                                    key={command.id}\n                                    data-index={globalIndex}\n                                    onClick={() => handleCommandClick(command)}\n                                    onMouseEnter={() => setSelectedIndex(globalIndex)}\n                                    className={cn(\n                                      \"w-full flex items-start gap-3 px-3 py-2 rounded-md\",\n                                      \"hover:bg-accent transition-colors\",\n                                      \"text-left\",\n                                      isSelected && \"bg-accent\"\n                                    )}\n                                  >\n                                    <Icon className=\"h-4 w-4 mt-0.5 flex-shrink-0 text-muted-foreground\" />\n                                    \n                                    <div className=\"flex-1 min-w-0\">\n                                      <div className=\"flex items-baseline gap-2\">\n                                        <span className=\"font-mono text-sm text-primary\">\n                                          {command.full_command}\n                                        </span>\n                                        {command.accepts_arguments && (\n                                          <span className=\"text-xs text-muted-foreground\">\n                                            [args]\n                                          </span>\n                                        )}\n                                      </div>\n                                      \n                                      {command.description && (\n                                        <p className=\"text-xs text-muted-foreground mt-0.5 truncate\">\n                                          {command.description}\n                                        </p>\n                                      )}\n                                      \n                                      <div className=\"flex items-center gap-3 mt-1\">\n                                        {command.allowed_tools.length > 0 && (\n                                          <span className=\"text-xs text-muted-foreground\">\n                                            {command.allowed_tools.length} tool{command.allowed_tools.length === 1 ? '' : 's'}\n                                          </span>\n                                        )}\n                                        \n                                        {command.has_bash_commands && (\n                                          <span className=\"text-xs text-blue-600 dark:text-blue-400\">\n                                            Bash\n                                          </span>\n                                        )}\n                                        \n                                        {command.has_file_references && (\n                                          <span className=\"text-xs text-green-600 dark:text-green-400\">\n                                            Files\n                                          </span>\n                                        )}\n                                      </div>\n                                    </div>\n                                  </button>\n                                );\n                              })}\n                            </div>\n                          </div>\n                        ))}\n                      </div>\n                    )}\n                  </div>\n                )}\n              </>\n            )}\n          </>\n        )}\n      </div>\n\n      {/* Footer */}\n      <div className=\"border-t border-border p-2\">\n        <p className=\"text-xs text-muted-foreground text-center\">\n          ↑↓ Navigate • Enter Select • Esc Close\n        </p>\n      </div>\n    </motion.div>\n  );\n}; \n"
  },
  {
    "path": "src/components/SlashCommandsManager.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { \n  Plus, \n  Trash2, \n  Edit,\n  Save,\n  Command,\n  Globe,\n  FolderOpen,\n  Terminal,\n  FileCode,\n  Zap,\n  Code,\n  AlertCircle,\n  Loader2,\n  Search,\n  ChevronDown,\n  ChevronRight\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Card } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from \"@/components/ui/dialog\";\nimport { api, type SlashCommand } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { COMMON_TOOL_MATCHERS } from \"@/types/hooks\";\nimport { useTrackEvent } from \"@/hooks\";\n\ninterface SlashCommandsManagerProps {\n  projectPath?: string;\n  className?: string;\n  scopeFilter?: 'project' | 'user' | 'all';\n}\n\ninterface CommandForm {\n  name: string;\n  namespace: string;\n  content: string;\n  description: string;\n  allowedTools: string[];\n  scope: 'project' | 'user';\n}\n\nconst EXAMPLE_COMMANDS = [\n  {\n    name: \"review\",\n    description: \"Review code for best practices\",\n    content: \"Review the following code for best practices, potential issues, and improvements:\\n\\n@$ARGUMENTS\",\n    allowedTools: [\"Read\", \"Grep\"]\n  },\n  {\n    name: \"explain\",\n    description: \"Explain how something works\",\n    content: \"Explain how $ARGUMENTS works in detail, including its purpose, implementation, and usage examples.\",\n    allowedTools: [\"Read\", \"Grep\", \"WebSearch\"]\n  },\n  {\n    name: \"fix-issue\",\n    description: \"Fix a specific issue\",\n    content: \"Fix issue #$ARGUMENTS following our coding standards and best practices.\",\n    allowedTools: [\"Read\", \"Edit\", \"MultiEdit\", \"Write\"]\n  },\n  {\n    name: \"test\",\n    description: \"Write tests for code\",\n    content: \"Write comprehensive tests for:\\n\\n@$ARGUMENTS\\n\\nInclude unit tests, edge cases, and integration tests where appropriate.\",\n    allowedTools: [\"Read\", \"Write\", \"Edit\"]\n  }\n];\n\n// Get icon for command based on its properties\nconst getCommandIcon = (command: SlashCommand) => {\n  if (command.has_bash_commands) return Terminal;\n  if (command.has_file_references) return FileCode;\n  if (command.accepts_arguments) return Zap;\n  if (command.scope === \"project\") return FolderOpen;\n  if (command.scope === \"user\") return Globe;\n  return Command;\n};\n\n/**\n * SlashCommandsManager component for managing custom slash commands\n * Provides a no-code interface for creating, editing, and deleting commands\n */\nexport const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({\n  projectPath,\n  className,\n  scopeFilter = 'all',\n}) => {\n  const [commands, setCommands] = useState<SlashCommand[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [saving, setSaving] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [selectedScope, setSelectedScope] = useState<'all' | 'project' | 'user'>(scopeFilter === 'all' ? 'all' : scopeFilter as 'project' | 'user');\n  const [expandedCommands, setExpandedCommands] = useState<Set<string>>(new Set());\n  \n  // Edit dialog state\n  const [editDialogOpen, setEditDialogOpen] = useState(false);\n  const [editingCommand, setEditingCommand] = useState<SlashCommand | null>(null);\n  const [commandForm, setCommandForm] = useState<CommandForm>({\n    name: \"\",\n    namespace: \"\",\n    content: \"\",\n    description: \"\",\n    allowedTools: [],\n    scope: 'user'\n  });\n\n  // Delete confirmation dialog state\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n  const [commandToDelete, setCommandToDelete] = useState<SlashCommand | null>(null);\n  const [deleting, setDeleting] = useState(false);\n  \n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n\n  // Load commands on mount\n  useEffect(() => {\n    loadCommands();\n  }, [projectPath]);\n\n  const loadCommands = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const loadedCommands = await api.slashCommandsList(projectPath);\n      setCommands(loadedCommands);\n    } catch (err) {\n      console.error(\"Failed to load slash commands:\", err);\n      setError(\"Failed to load commands\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleCreateNew = () => {\n    setEditingCommand(null);\n    setCommandForm({\n      name: \"\",\n      namespace: \"\",\n      content: \"\",\n      description: \"\",\n      allowedTools: [],\n      scope: scopeFilter !== 'all' ? scopeFilter : (projectPath ? 'project' : 'user')\n    });\n    setEditDialogOpen(true);\n  };\n\n  const handleEdit = (command: SlashCommand) => {\n    setEditingCommand(command);\n    setCommandForm({\n      name: command.name,\n      namespace: command.namespace || \"\",\n      content: command.content,\n      description: command.description || \"\",\n      allowedTools: command.allowed_tools,\n      scope: command.scope as 'project' | 'user'\n    });\n    setEditDialogOpen(true);\n  };\n\n  const handleSave = async () => {\n    try {\n      setSaving(true);\n      setError(null);\n\n      await api.slashCommandSave(\n        commandForm.scope,\n        commandForm.name,\n        commandForm.namespace || undefined,\n        commandForm.content,\n        commandForm.description || undefined,\n        commandForm.allowedTools,\n        commandForm.scope === 'project' ? projectPath : undefined\n      );\n      \n      // Track command creation\n      trackEvent.slashCommandCreated({\n        command_type: editingCommand ? 'custom' : 'custom',\n        has_parameters: commandForm.content.includes('$ARGUMENTS')\n      });\n\n      setEditDialogOpen(false);\n      await loadCommands();\n    } catch (err) {\n      console.error(\"Failed to save command:\", err);\n      setError(err instanceof Error ? err.message : \"Failed to save command\");\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  const handleDeleteClick = (command: SlashCommand) => {\n    setCommandToDelete(command);\n    setDeleteDialogOpen(true);\n  };\n\n  const confirmDelete = async () => {\n    if (!commandToDelete) return;\n\n    try {\n      setDeleting(true);\n      setError(null);\n      await api.slashCommandDelete(commandToDelete.id, projectPath);\n      setDeleteDialogOpen(false);\n      setCommandToDelete(null);\n      await loadCommands();\n    } catch (err) {\n      console.error(\"Failed to delete command:\", err);\n      const errorMessage = err instanceof Error ? err.message : \"Failed to delete command\";\n      setError(errorMessage);\n    } finally {\n      setDeleting(false);\n    }\n  };\n\n  const cancelDelete = () => {\n    setDeleteDialogOpen(false);\n    setCommandToDelete(null);\n  };\n\n  const toggleExpanded = (commandId: string) => {\n    setExpandedCommands(prev => {\n      const next = new Set(prev);\n      if (next.has(commandId)) {\n        next.delete(commandId);\n      } else {\n        next.add(commandId);\n      }\n      return next;\n    });\n  };\n\n  const handleToolToggle = (tool: string) => {\n    setCommandForm(prev => ({\n      ...prev,\n      allowedTools: prev.allowedTools.includes(tool)\n        ? prev.allowedTools.filter(t => t !== tool)\n        : [...prev.allowedTools, tool]\n    }));\n  };\n\n  const applyExample = (example: typeof EXAMPLE_COMMANDS[0]) => {\n    setCommandForm(prev => ({\n      ...prev,\n      name: example.name,\n      description: example.description,\n      content: example.content,\n      allowedTools: example.allowedTools\n    }));\n  };\n\n  // Filter commands\n  const filteredCommands = commands.filter(cmd => {\n    // Hide default commands\n    if (cmd.scope === 'default') {\n      return false;\n    }\n\n    // Apply scopeFilter if set to specific scope\n    if (scopeFilter !== 'all' && cmd.scope !== scopeFilter) {\n      return false;\n    }\n\n    // Scope filter\n    if (selectedScope !== 'all' && cmd.scope !== selectedScope) {\n      return false;\n    }\n\n    // Search filter\n    if (searchQuery) {\n      const query = searchQuery.toLowerCase();\n      return (\n        cmd.name.toLowerCase().includes(query) ||\n        cmd.full_command.toLowerCase().includes(query) ||\n        (cmd.description && cmd.description.toLowerCase().includes(query)) ||\n        (cmd.namespace && cmd.namespace.toLowerCase().includes(query))\n      );\n    }\n\n    return true;\n  });\n\n  // Group commands by namespace and scope\n  const groupedCommands = filteredCommands.reduce((acc, cmd) => {\n    const key = cmd.namespace \n      ? `${cmd.namespace} (${cmd.scope})` \n      : `${cmd.scope === 'project' ? 'Project' : 'User'} Commands`;\n    if (!acc[key]) {\n      acc[key] = [];\n    }\n    acc[key].push(cmd);\n    return acc;\n  }, {} as Record<string, SlashCommand[]>);\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      {/* Header */}\n      <div className=\"flex items-center justify-between\">\n        <div>\n          <h3 className=\"text-lg font-semibold\">\n            {scopeFilter === 'project' ? 'Project Slash Commands' : 'Slash Commands'}\n          </h3>\n          <p className=\"text-sm text-muted-foreground mt-1\">\n            {scopeFilter === 'project' \n              ? 'Create custom commands for this project' \n              : 'Create custom commands to streamline your workflow'}\n          </p>\n        </div>\n        <Button onClick={handleCreateNew} size=\"sm\" className=\"gap-2\">\n          <Plus className=\"h-4 w-4\" />\n          New Command\n        </Button>\n      </div>\n\n      {/* Filters */}\n      <div className=\"flex items-center gap-4\">\n        <div className=\"flex-1\">\n          <div className=\"relative\">\n            <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n            <Input\n              placeholder=\"Search commands...\"\n              value={searchQuery}\n              onChange={(e) => setSearchQuery(e.target.value)}\n              className=\"pl-9\"\n            />\n          </div>\n        </div>\n        {scopeFilter === 'all' && (\n          <Select value={selectedScope} onValueChange={(value: any) => setSelectedScope(value)}>\n            <SelectTrigger className=\"w-[150px]\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"all\">All Commands</SelectItem>\n              <SelectItem value=\"project\">Project</SelectItem>\n              <SelectItem value=\"user\">User</SelectItem>\n            </SelectContent>\n          </Select>\n        )}\n      </div>\n\n      {/* Error Message */}\n      {error && (\n        <div className=\"flex items-center gap-2 p-3 rounded-lg bg-destructive/10 text-destructive\">\n          <AlertCircle className=\"h-4 w-4\" />\n          <span className=\"text-sm\">{error}</span>\n        </div>\n      )}\n\n      {/* Commands List */}\n      {loading ? (\n        <div className=\"flex items-center justify-center py-8\">\n          <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n        </div>\n      ) : filteredCommands.length === 0 ? (\n        <Card className=\"p-8\">\n          <div className=\"text-center\">\n            <Command className=\"h-12 w-12 mx-auto text-muted-foreground mb-4\" />\n            <p className=\"text-sm text-muted-foreground\">\n              {searchQuery \n                ? \"No commands found\" \n                : scopeFilter === 'project' \n                  ? \"No project commands created yet\" \n                  : \"No commands created yet\"}\n            </p>\n            {!searchQuery && (\n              <Button onClick={handleCreateNew} variant=\"outline\" size=\"sm\" className=\"mt-4\">\n                {scopeFilter === 'project' \n                  ? \"Create your first project command\" \n                  : \"Create your first command\"}\n              </Button>\n            )}\n          </div>\n        </Card>\n      ) : (\n        <div className=\"space-y-4\">\n          {Object.entries(groupedCommands).map(([groupKey, groupCommands]) => (\n            <Card key={groupKey} className=\"overflow-hidden\">\n              <div className=\"p-4 bg-muted/50 border-b\">\n                <h4 className=\"text-sm font-medium\">\n                  {groupKey}\n                </h4>\n              </div>\n              \n              <div className=\"divide-y\">\n                {groupCommands.map((command) => {\n                  const Icon = getCommandIcon(command);\n                  const isExpanded = expandedCommands.has(command.id);\n                  \n                  return (\n                    <div key={command.id}>\n                      <div className=\"p-4\">\n                        <div className=\"flex items-start gap-4\">\n                          <Icon className=\"h-5 w-5 mt-0.5 text-muted-foreground flex-shrink-0\" />\n                          \n                          <div className=\"flex-1 min-w-0\">\n                            <div className=\"flex items-center gap-2 mb-1\">\n                              <code className=\"text-sm font-mono text-primary\">\n                                {command.full_command}\n                              </code>\n                              {command.accepts_arguments && (\n                                <Badge variant=\"secondary\" className=\"text-xs\">\n                                  Arguments\n                                </Badge>\n                              )}\n                            </div>\n                            \n                            {command.description && (\n                              <p className=\"text-sm text-muted-foreground mb-2\">\n                                {command.description}\n                              </p>\n                            )}\n                            \n                            <div className=\"flex items-center gap-4 text-xs\">\n                              {command.allowed_tools.length > 0 && (\n                                <span className=\"text-muted-foreground\">\n                                  {command.allowed_tools.length} tool{command.allowed_tools.length === 1 ? '' : 's'}\n                                </span>\n                              )}\n                              \n                              {command.has_bash_commands && (\n                                <Badge variant=\"outline\" className=\"text-xs\">\n                                  Bash\n                                </Badge>\n                              )}\n                              \n                              {command.has_file_references && (\n                                <Badge variant=\"outline\" className=\"text-xs\">\n                                  Files\n                                </Badge>\n                              )}\n                              \n                              <button\n                                onClick={() => toggleExpanded(command.id)}\n                                className=\"flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors\"\n                              >\n                                {isExpanded ? (\n                                  <>\n                                    <ChevronDown className=\"h-3 w-3\" />\n                                    Hide content\n                                  </>\n                                ) : (\n                                  <>\n                                    <ChevronRight className=\"h-3 w-3\" />\n                                    Show content\n                                  </>\n                                )}\n                              </button>\n                            </div>\n                          </div>\n                          \n                          <div className=\"flex items-center gap-2\">\n                            <Button\n                              variant=\"ghost\"\n                              size=\"icon\"\n                              onClick={() => handleEdit(command)}\n                              className=\"h-8 w-8\"\n                            >\n                              <Edit className=\"h-4 w-4\" />\n                            </Button>\n                            <Button\n                              variant=\"ghost\"\n                              size=\"icon\"\n                              onClick={() => handleDeleteClick(command)}\n                              className=\"h-8 w-8 text-destructive hover:text-destructive\"\n                            >\n                              <Trash2 className=\"h-4 w-4\" />\n                            </Button>\n                          </div>\n                        </div>\n                        \n                        <AnimatePresence>\n                          {isExpanded && (\n                            <motion.div\n                              initial={{ height: 0, opacity: 0 }}\n                              animate={{ height: \"auto\", opacity: 1 }}\n                              exit={{ height: 0, opacity: 0 }}\n                              transition={{ duration: 0.2 }}\n                              className=\"overflow-hidden\"\n                            >\n                              <div className=\"mt-4 p-3 bg-muted/50 rounded-md\">\n                                <pre className=\"text-xs font-mono whitespace-pre-wrap\">\n                                  {command.content}\n                                </pre>\n                              </div>\n                            </motion.div>\n                          )}\n                        </AnimatePresence>\n                      </div>\n                    </div>\n                  );\n                })}\n              </div>\n            </Card>\n          ))}\n        </div>\n      )}\n\n      {/* Edit Dialog */}\n      <Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>\n        <DialogContent className=\"max-w-4xl max-h-[90vh] overflow-y-auto\">\n          <DialogHeader>\n            <DialogTitle>\n              {editingCommand ? \"Edit Command\" : \"Create New Command\"}\n            </DialogTitle>\n          </DialogHeader>\n\n          <div className=\"space-y-4 py-4\">\n            {/* Scope */}\n            <div className=\"space-y-2\">\n              <Label>Scope</Label>\n              <Select \n                value={commandForm.scope} \n                onValueChange={(value: 'project' | 'user') => setCommandForm(prev => ({ ...prev, scope: value }))}\n                disabled={scopeFilter !== 'all' || (!projectPath && commandForm.scope === 'project')}\n              >\n                <SelectTrigger>\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  {(scopeFilter === 'all' || scopeFilter === 'user') && (\n                    <SelectItem value=\"user\">\n                      <div className=\"flex items-center gap-2\">\n                        <Globe className=\"h-4 w-4\" />\n                        User (Global)\n                      </div>\n                    </SelectItem>\n                  )}\n                  {(scopeFilter === 'all' || scopeFilter === 'project') && (\n                    <SelectItem value=\"project\" disabled={!projectPath}>\n                      <div className=\"flex items-center gap-2\">\n                        <FolderOpen className=\"h-4 w-4\" />\n                        Project\n                      </div>\n                    </SelectItem>\n                  )}\n                </SelectContent>\n              </Select>\n              <p className=\"text-xs text-muted-foreground\">\n                {commandForm.scope === 'user' \n                  ? \"Available across all projects\" \n                  : \"Only available in this project\"}\n              </p>\n            </div>\n\n            {/* Name and Namespace */}\n            <div className=\"grid grid-cols-2 gap-4\">\n              <div className=\"space-y-2\">\n                <Label>Command Name*</Label>\n                <Input\n                  placeholder=\"e.g., review, fix-issue\"\n                  value={commandForm.name}\n                  onChange={(e) => setCommandForm(prev => ({ ...prev, name: e.target.value }))}\n                />\n              </div>\n              \n              <div className=\"space-y-2\">\n                <Label>Namespace (Optional)</Label>\n                <Input\n                  placeholder=\"e.g., frontend, backend\"\n                  value={commandForm.namespace}\n                  onChange={(e) => setCommandForm(prev => ({ ...prev, namespace: e.target.value }))}\n                />\n              </div>\n            </div>\n\n            {/* Description */}\n            <div className=\"space-y-2\">\n              <Label>Description (Optional)</Label>\n              <Input\n                placeholder=\"Brief description of what this command does\"\n                value={commandForm.description}\n                onChange={(e) => setCommandForm(prev => ({ ...prev, description: e.target.value }))}\n              />\n            </div>\n\n            {/* Content */}\n            <div className=\"space-y-2\">\n              <Label>Command Content*</Label>\n              <Textarea\n                placeholder=\"Enter the prompt content. Use $ARGUMENTS for dynamic values.\"\n                value={commandForm.content}\n                onChange={(e) => setCommandForm(prev => ({ ...prev, content: e.target.value }))}\n                className=\"min-h-[150px] font-mono text-sm\"\n              />\n              <p className=\"text-xs text-muted-foreground\">\n                Use <code>$ARGUMENTS</code> for user input, <code>@filename</code> for files, \n                and <code>!`command`</code> for bash commands\n              </p>\n            </div>\n\n            {/* Allowed Tools */}\n            <div className=\"space-y-2\">\n              <Label>Allowed Tools</Label>\n              <div className=\"flex flex-wrap gap-2\">\n                {COMMON_TOOL_MATCHERS.map((tool) => (\n                  <Button\n                    key={tool}\n                    variant={commandForm.allowedTools.includes(tool) ? \"default\" : \"outline\"}\n                    size=\"sm\"\n                    onClick={() => handleToolToggle(tool)}\n                    type=\"button\"\n                  >\n                    {tool}\n                  </Button>\n                ))}\n              </div>\n              <p className=\"text-xs text-muted-foreground\">\n                Select which tools Claude can use with this command\n              </p>\n            </div>\n\n            {/* Examples */}\n            {!editingCommand && (\n              <div className=\"space-y-2\">\n                <Label>Examples</Label>\n                <div className=\"grid grid-cols-2 gap-2\">\n                  {EXAMPLE_COMMANDS.map((example) => (\n                    <Button\n                      key={example.name}\n                      variant=\"outline\"\n                      size=\"sm\"\n                      onClick={() => applyExample(example)}\n                      className=\"justify-start\"\n                    >\n                      <Code className=\"h-4 w-4 mr-2\" />\n                      {example.name}\n                    </Button>\n                  ))}\n                </div>\n              </div>\n            )}\n\n            {/* Preview */}\n            {commandForm.name && (\n              <div className=\"space-y-2\">\n                <Label>Preview</Label>\n                <div className=\"p-3 bg-muted rounded-md\">\n                  <code className=\"text-sm\">\n                    /\n                    {commandForm.namespace && `${commandForm.namespace}:`}\n                    {commandForm.name}\n                    {commandForm.content.includes('$ARGUMENTS') && ' [arguments]'}\n                  </code>\n                </div>\n              </div>\n            )}\n          </div>\n\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setEditDialogOpen(false)}>\n              Cancel\n            </Button>\n            <Button\n              onClick={handleSave}\n              disabled={!commandForm.name || !commandForm.content || saving}\n            >\n              {saving ? (\n                <>\n                  <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                  Saving...\n                </>\n              ) : (\n                <>\n                  <Save className=\"h-4 w-4 mr-2\" />\n                  Save\n                </>\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete Confirmation Dialog */}\n      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>\n        <DialogContent className=\"max-w-md\">\n          <DialogHeader>\n            <DialogTitle>Delete Command</DialogTitle>\n          </DialogHeader>\n\n          <div className=\"space-y-4 py-4\">\n            <p>Are you sure you want to delete this command?</p>\n            {commandToDelete && (\n              <div className=\"p-3 bg-muted rounded-md\">\n                <code className=\"text-sm font-mono\">{commandToDelete.full_command}</code>\n                {commandToDelete.description && (\n                  <p className=\"text-sm text-muted-foreground mt-1\">{commandToDelete.description}</p>\n                )}\n              </div>\n            )}\n            <p className=\"text-sm text-muted-foreground\">\n              This action cannot be undone. The command file will be permanently deleted.\n            </p>\n          </div>\n\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={cancelDelete} disabled={deleting}>\n              Cancel\n            </Button>\n            <Button\n              variant=\"destructive\"\n              onClick={confirmDelete}\n              disabled={deleting}\n            >\n              {deleting ? (\n                <>\n                  <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                  Deleting...\n                </>\n              ) : (\n                <>\n                  <Trash2 className=\"h-4 w-4 mr-2\" />\n                  Delete\n                </>\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}; \n"
  },
  {
    "path": "src/components/StartupIntro.tsx",
    "content": "import { AnimatePresence, motion } from \"framer-motion\";\nimport opcodeLogo from \"../../src-tauri/icons/icon.png\";\nimport type { CSSProperties } from \"react\";\n\n/**\n * StartupIntro - a lightweight startup overlay shown on app launch.\n * - Non-interactive; auto-fades after parent hides it via the `visible` prop.\n * - Uses existing shimmer/rotating-symbol styles from shimmer.css.\n */\nexport function StartupIntro({ visible }: { visible: boolean }) {\n  // Simple entrance animations only\n  return (\n    <AnimatePresence>\n      {visible && (\n        <motion.div\n          initial={{ opacity: 1 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.35 }}\n          className=\"fixed inset-0 z-[60] flex items-center justify-center bg-background\"\n          aria-hidden=\"true\"\n        >\n          {/* Ambient radial glow */}\n          <motion.div\n            className=\"absolute inset-0\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            transition={{ duration: 0.25 }}\n            style={{\n              background:\n                \"radial-gradient(800px circle at 50% 55%, var(--color-primary)/8, transparent 65%)\",\n              pointerEvents: \"none\",\n            } as CSSProperties}\n          />\n\n          {/* Subtle vignette */}\n          <div\n            className=\"absolute inset-0 pointer-events-none\"\n            style={{\n              background:\n                \"radial-gradient(1200px circle at 50% 40%, transparent 60%, rgba(0,0,0,0.25))\",\n            }}\n          />\n\n          {/* Content */}\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            transition={{ type: \"spring\", stiffness: 280, damping: 22 }}\n            className=\"relative flex flex-col items-center justify-center gap-1\"\n          >\n\n            {/* opcode logo slides left; brand text reveals to the right */}\n            <div className=\"relative flex items-center justify-center\">\n              {/* Logo wrapper that gently slides left */}\n              <motion.div\n                className=\"relative z-10\"\n                initial={{ opacity: 0, scale: 1, x: 0 }}\n                animate={{ opacity: 1, scale: 1, x: -14 }}\n                transition={{ duration: 0.35, ease: \"easeOut\", delay: 0.2 }}\n              >\n                <motion.div\n                  className=\"absolute inset-0 rounded-full bg-primary/15 blur-2xl\"\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: [0, 1, 0.9] }}\n                  transition={{ duration: 0.9, ease: \"easeOut\" }}\n                />\n                <motion.img\n                  src={opcodeLogo}\n                  alt=\"opcode\"\n                  className=\"h-20 w-20 rounded-lg shadow-sm\"\n                  transition={{ repeat: Infinity, repeatType: \"loop\", ease: \"linear\", duration: 0.5 }}\n                />\n              </motion.div>\n\n              {/* Brand text reveals left-to-right in the freed space */}\n              <motion.div\n                initial={{ x: -35, opacity: 0, clipPath: \"inset(0 100% 0 0)\" }}\n                animate={{ x: 2, opacity: 1, clipPath: \"inset(0 0% 0 0)\" }}\n                transition={{ duration: 0.6, ease: \"easeOut\", delay: 0.1 }}\n                style={{ willChange: \"transform, opacity, clip-path\" }}\n              >\n                <BrandText />\n              </motion.div>\n            </div>\n\n\n          </motion.div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nexport default StartupIntro;\n\nfunction BrandText() {\n  return (\n    <div className=\"text-5xl font-extrabold tracking-tight brand-text\">\n      <span className=\"brand-text-solid\">opcode</span>\n      <span aria-hidden=\"true\" className=\"brand-text-shimmer\">opcode</span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/StorageTab.tsx",
    "content": "import React, { useState, useEffect, useCallback } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport {\n  Database,\n  Search,\n  Plus,\n  Edit3,\n  Trash2,\n  RefreshCw,\n  ChevronLeft,\n  ChevronRight,\n  Terminal,\n  AlertTriangle,\n  Check,\n  X,\n  Table,\n  Loader2,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card } from \"@/components/ui/card\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { api } from \"@/lib/api\";\nimport { Toast, ToastContainer } from \"./ui/toast\";\n\ninterface TableInfo {\n  name: string;\n  row_count: number;\n  columns: ColumnInfo[];\n}\n\ninterface ColumnInfo {\n  cid: number;\n  name: string;\n  type_name: string;\n  notnull: boolean;\n  dflt_value: string | null;\n  pk: boolean;\n}\n\ninterface TableData {\n  table_name: string;\n  columns: ColumnInfo[];\n  rows: Record<string, any>[];\n  total_rows: number;\n  page: number;\n  page_size: number;\n  total_pages: number;\n}\n\ninterface QueryResult {\n  columns: string[];\n  rows: any[][];\n  rows_affected?: number;\n  last_insert_rowid?: number;\n}\n\n/**\n * StorageTab component - A beautiful SQLite database viewer/editor\n */\nexport const StorageTab: React.FC = () => {\n  const [tables, setTables] = useState<TableInfo[]>([]);\n  const [selectedTable, setSelectedTable] = useState<string>(\"\");\n  const [tableData, setTableData] = useState<TableData | null>(null);\n  const [currentPage, setCurrentPage] = useState(1);\n  const [pageSize] = useState(25);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n\n  // Dialog states\n  const [editingRow, setEditingRow] = useState<Record<string, any> | null>(null);\n  const [newRow, setNewRow] = useState<Record<string, any> | null>(null);\n  const [deletingRow, setDeletingRow] = useState<Record<string, any> | null>(null);\n  const [showResetConfirm, setShowResetConfirm] = useState(false);\n  const [showSqlEditor, setShowSqlEditor] = useState(false);\n  const [sqlQuery, setSqlQuery] = useState(\"\");\n  const [sqlResult, setSqlResult] = useState<QueryResult | null>(null);\n  const [sqlError, setSqlError] = useState<string | null>(null);\n  const [toast, setToast] = useState<{ message: string; type: \"success\" | \"error\" } | null>(null);\n\n  /**\n   * Load all tables on mount\n   */\n  useEffect(() => {\n    loadTables();\n  }, []);\n\n  /**\n   * Load table data when selected table changes\n   */\n  useEffect(() => {\n    if (selectedTable) {\n      loadTableData(1);\n    }\n  }, [selectedTable]);\n\n  /**\n   * Load all tables from the database\n   */\n  const loadTables = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const result = await api.storageListTables();\n      setTables(result);\n      if (result.length > 0 && !selectedTable) {\n        setSelectedTable(result[0].name);\n      }\n    } catch (err) {\n      console.error(\"Failed to load tables:\", err);\n      setError(\"Failed to load tables\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Load data for the selected table\n   */\n  const loadTableData = async (page: number, search?: string) => {\n    if (!selectedTable) return;\n\n    try {\n      setLoading(true);\n      setError(null);\n      const result = await api.storageReadTable(\n        selectedTable,\n        page,\n        pageSize,\n        search || searchQuery || undefined\n      );\n      setTableData(result);\n      setCurrentPage(page);\n    } catch (err) {\n      console.error(\"Failed to load table data:\", err);\n      setError(\"Failed to load table data\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Handle search\n   */\n  const handleSearch = useCallback(\n    (value: string) => {\n      setSearchQuery(value);\n      loadTableData(1, value);\n    },\n    [selectedTable]\n  );\n\n  /**\n   * Get primary key values for a row\n   */\n  const getPrimaryKeyValues = (row: Record<string, any>): Record<string, any> => {\n    if (!tableData) return {};\n    \n    const pkColumns = tableData.columns.filter(col => col.pk);\n    const pkValues: Record<string, any> = {};\n    \n    pkColumns.forEach(col => {\n      pkValues[col.name] = row[col.name];\n    });\n    \n    return pkValues;\n  };\n\n  /**\n   * Handle row update\n   */\n  const handleUpdateRow = async (updates: Record<string, any>) => {\n    if (!editingRow || !selectedTable) return;\n\n    try {\n      setLoading(true);\n      const pkValues = getPrimaryKeyValues(editingRow);\n      await api.storageUpdateRow(selectedTable, pkValues, updates);\n      await loadTableData(currentPage);\n      setEditingRow(null);\n    } catch (err) {\n      console.error(\"Failed to update row:\", err);\n      setError(\"Failed to update row\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Handle row deletion\n   */\n  const handleDeleteRow = async () => {\n    if (!deletingRow || !selectedTable) return;\n\n    try {\n      setLoading(true);\n      const pkValues = getPrimaryKeyValues(deletingRow);\n      await api.storageDeleteRow(selectedTable, pkValues);\n      await loadTableData(currentPage);\n      setDeletingRow(null);\n    } catch (err) {\n      console.error(\"Failed to delete row:\", err);\n      setError(\"Failed to delete row\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Handle new row insertion\n   */\n  const handleInsertRow = async (values: Record<string, any>) => {\n    if (!selectedTable) return;\n\n    try {\n      setLoading(true);\n      await api.storageInsertRow(selectedTable, values);\n      await loadTableData(currentPage);\n      setNewRow(null);\n    } catch (err) {\n      console.error(\"Failed to insert row:\", err);\n      setError(\"Failed to insert row\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Handle SQL query execution\n   */\n  const handleExecuteSql = async () => {\n    try {\n      setLoading(true);\n      setSqlError(null);\n      const result = await api.storageExecuteSql(sqlQuery);\n      setSqlResult(result);\n      \n      // Refresh tables and data if it was a non-SELECT query\n      if (result.rows_affected !== undefined) {\n        await loadTables();\n        if (selectedTable) {\n          await loadTableData(currentPage);\n        }\n      }\n    } catch (err) {\n      console.error(\"Failed to execute SQL:\", err);\n      setSqlError(err instanceof Error ? err.message : \"Failed to execute SQL\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Handle database reset\n   */\n  const handleResetDatabase = async () => {\n    try {\n      setLoading(true);\n      await api.storageResetDatabase();\n      await loadTables();\n      setSelectedTable(\"\");\n      setTableData(null);\n      setShowResetConfirm(false);\n      setToast({\n        message: \"Database Reset Complete: The database has been restored to its default state with empty tables (agents, agent_runs, app_settings).\",\n        type: \"success\",\n      });\n    } catch (err) {\n      console.error(\"Failed to reset database:\", err);\n      setError(\"Failed to reset database\");\n      setToast({\n        message: \"Reset Failed: Failed to reset the database. Please try again.\",\n        type: \"error\",\n      });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  /**\n   * Format cell value for display\n   */\n  const formatCellValue = (value: any, maxLength: number = 100): string => {\n    if (value === null) return \"NULL\";\n    if (value === undefined) return \"\";\n    if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n    if (typeof value === \"object\") return JSON.stringify(value);\n    \n    const stringValue = String(value);\n    if (stringValue.length > maxLength) {\n      return stringValue.substring(0, maxLength) + \"...\";\n    }\n    return stringValue;\n  };\n\n  /**\n   * Get input type for column\n   */\n  const getInputType = (column: ColumnInfo): string => {\n    const type = column.type_name.toUpperCase();\n    if (type.includes(\"INT\")) return \"number\";\n    if (type.includes(\"REAL\") || type.includes(\"FLOAT\") || type.includes(\"DOUBLE\")) return \"number\";\n    if (type.includes(\"BOOL\")) return \"checkbox\";\n    return \"text\";\n  };\n\n  return (\n    <div className=\"space-y-6\">\n      {/* Header */}\n      <Card className=\"p-6\">\n        <div className=\"space-y-4\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-3\">\n              <Database className=\"h-4 w-4 text-primary\" />\n              <h3 className=\"text-sm font-semibold\">Database Storage</h3>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={() => setShowSqlEditor(true)}\n                className=\"gap-2 h-8 text-xs\"\n              >\n                <Terminal className=\"h-3 w-3\" />\n                SQL Query\n              </Button>\n              <Button\n                variant=\"destructive\"\n                size=\"sm\"\n                onClick={() => setShowResetConfirm(true)}\n                className=\"gap-2 h-8 text-xs\"\n              >\n                <RefreshCw className=\"h-3 w-3\" />\n                Reset DB\n              </Button>\n            </div>\n          </div>\n\n          {/* Table Selector and Search */}\n          <div className=\"flex items-center gap-3\">\n            <Select value={selectedTable} onValueChange={setSelectedTable}>\n              <SelectTrigger className=\"w-[200px] h-8 text-xs\">\n                <SelectValue placeholder=\"Select a table\">\n                  {selectedTable && (\n                    <div className=\"flex items-center gap-2\">\n                      <Table className=\"h-3 w-3\" />\n                      {selectedTable}\n                    </div>\n                  )}\n                </SelectValue>\n              </SelectTrigger>\n              <SelectContent>\n                {tables.map((table) => (\n                  <SelectItem key={table.name} value={table.name} className=\"text-xs\">\n                    <div className=\"flex items-center justify-between w-full\">\n                      <span>{table.name}</span>\n                      <span className=\"text-[10px] text-muted-foreground ml-2\">\n                        {table.row_count} rows\n                      </span>\n                    </div>\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n\n            <div className=\"flex-1 relative\">\n              <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-3 w-3 text-muted-foreground\" />\n              <Input\n                placeholder=\"Search in table...\"\n                value={searchQuery}\n                onChange={(e) => handleSearch(e.target.value)}\n                className=\"pl-8 h-8 text-xs\"\n              />\n            </div>\n\n            {tableData && (\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={() => setNewRow({})}\n                className=\"gap-2 h-8 text-xs\"\n              >\n                <Plus className=\"h-3 w-3\" />\n                New Row\n              </Button>\n            )}\n          </div>\n        </div>\n      </Card>\n\n      {/* Table Data */}\n      {tableData && (\n        <Card className=\"overflow-hidden\">\n          <div className=\"overflow-x-auto\">\n            <table className=\"w-full\">\n              <thead>\n                <tr className=\"border-b bg-muted/50\">\n                  {tableData.columns.map((column) => (\n                    <th\n                      key={column.name}\n                      className=\"px-3 py-2 text-left text-xs font-medium text-muted-foreground\"\n                    >\n                      <div className=\"flex items-center gap-1\">\n                        {column.name}\n                        {column.pk && (\n                          <span className=\"text-[10px] text-primary\">PK</span>\n                        )}\n                      </div>\n                      <div className=\"text-[10px] font-normal\">\n                        {column.type_name}\n                      </div>\n                    </th>\n                  ))}\n                  <th className=\"px-3 py-2 text-right text-xs font-medium text-muted-foreground\">\n                    Actions\n                  </th>\n                </tr>\n              </thead>\n              <tbody>\n                <AnimatePresence>\n                  {tableData.rows.map((row, index) => (\n                    <motion.tr\n                      key={index}\n                      initial={{ opacity: 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0 }}\n                      className=\"border-b hover:bg-muted/25 transition-colors\"\n                    >\n                      {tableData.columns.map((column) => {\n                        const value = row[column.name];\n                        const formattedValue = formatCellValue(value, 50);\n                        const fullValue = value === null ? \"NULL\" : \n                                        value === undefined ? \"\" : \n                                        typeof value === \"object\" ? JSON.stringify(value, null, 2) : \n                                        String(value);\n                        const isTruncated = fullValue.length > 50;\n                        \n                        return (\n                          <td\n                            key={column.name}\n                            className=\"px-3 py-2 text-xs font-mono\"\n                          >\n                            {isTruncated ? (\n                              <TooltipProvider>\n                                <Tooltip>\n                                  <TooltipTrigger asChild>\n                                    <span className=\"cursor-help block truncate max-w-[200px]\">\n                                      {formattedValue}\n                                    </span>\n                                  </TooltipTrigger>\n                                  <TooltipContent \n                                    side=\"bottom\" \n                                    className=\"max-w-[500px] max-h-[300px] overflow-auto\"\n                                  >\n                                    <pre className=\"text-xs whitespace-pre-wrap\">{fullValue}</pre>\n                                  </TooltipContent>\n                                </Tooltip>\n                              </TooltipProvider>\n                            ) : (\n                              <span className=\"block truncate max-w-[200px]\">\n                                {formattedValue}\n                              </span>\n                            )}\n                          </td>\n                        );\n                      })}\n                      <td className=\"px-3 py-2 text-right\">\n                        <div className=\"flex items-center justify-end gap-1\">\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={() => setEditingRow(row)}\n                            className=\"h-6 w-6\"\n                          >\n                            <Edit3 className=\"h-3 w-3\" />\n                          </Button>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={() => setDeletingRow(row)}\n                            className=\"h-6 w-6 hover:text-destructive\"\n                          >\n                            <Trash2 className=\"h-3 w-3\" />\n                          </Button>\n                        </div>\n                      </td>\n                    </motion.tr>\n                  ))}\n                </AnimatePresence>\n              </tbody>\n            </table>\n          </div>\n\n          {/* Pagination */}\n          {tableData.total_pages > 1 && (\n            <div className=\"flex items-center justify-between p-3 border-t\">\n              <div className=\"text-xs text-muted-foreground\">\n                Showing {(currentPage - 1) * pageSize + 1} to{\" \"}\n                {Math.min(currentPage * pageSize, tableData.total_rows)} of{\" \"}\n                {tableData.total_rows} rows\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() => loadTableData(currentPage - 1)}\n                  disabled={currentPage === 1}\n                  className=\"h-7 text-xs\"\n                >\n                  <ChevronLeft className=\"h-3 w-3\" />\n                  Previous\n                </Button>\n                <div className=\"text-xs\">\n                  Page {currentPage} of {tableData.total_pages}\n                </div>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() => loadTableData(currentPage + 1)}\n                  disabled={currentPage === tableData.total_pages}\n                  className=\"h-7 text-xs\"\n                >\n                  Next\n                  <ChevronRight className=\"h-3 w-3\" />\n                </Button>\n              </div>\n            </div>\n          )}\n        </Card>\n      )}\n\n      {/* Loading State */}\n      {loading && (\n        <div className=\"flex items-center justify-center py-12\">\n          <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n        </div>\n      )}\n\n      {/* Error State */}\n      {error && (\n        <Card className=\"p-6 border-destructive/50 bg-destructive/10\">\n          <div className=\"flex items-center gap-3 text-destructive\">\n            <AlertTriangle className=\"h-5 w-5\" />\n            <span className=\"font-medium\">{error}</span>\n          </div>\n        </Card>\n      )}\n\n      {/* Edit Row Dialog */}\n      <Dialog open={!!editingRow} onOpenChange={() => setEditingRow(null)}>\n        <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto\">\n          <DialogHeader>\n            <DialogTitle>Edit Row</DialogTitle>\n            <DialogDescription>\n              Update the values for this row in the {selectedTable} table.\n            </DialogDescription>\n          </DialogHeader>\n          {editingRow && tableData && (\n            <div className=\"space-y-4\">\n              {tableData.columns.map((column) => (\n                <div key={column.name} className=\"space-y-2\">\n                  <Label htmlFor={`edit-${column.name}`}>\n                    {column.name}\n                    {column.pk && (\n                      <span className=\"text-xs text-muted-foreground ml-2\">\n                        (Primary Key)\n                      </span>\n                    )}\n                  </Label>\n                  {getInputType(column) === \"checkbox\" ? (\n                    <input\n                      type=\"checkbox\"\n                      id={`edit-${column.name}`}\n                      checked={!!editingRow[column.name]}\n                      onChange={(e) =>\n                        setEditingRow({\n                          ...editingRow,\n                          [column.name]: e.target.checked,\n                        })\n                      }\n                      disabled={column.pk}\n                      className=\"h-4 w-4\"\n                    />\n                  ) : (\n                    <Input\n                      id={`edit-${column.name}`}\n                      type={getInputType(column)}\n                      value={editingRow[column.name] ?? \"\"}\n                      onChange={(e) =>\n                        setEditingRow({\n                          ...editingRow,\n                          [column.name]: e.target.value,\n                        })\n                      }\n                      disabled={column.pk}\n                      placeholder={column.dflt_value || \"NULL\"}\n                    />\n                  )}\n                  <p className=\"text-xs text-muted-foreground\">\n                    Type: {column.type_name}\n                    {column.notnull && \", NOT NULL\"}\n                    {column.dflt_value && `, Default: ${column.dflt_value}`}\n                  </p>\n                </div>\n              ))}\n            </div>\n          )}\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setEditingRow(null)}>\n              Cancel\n            </Button>\n            <Button\n              onClick={() => handleUpdateRow(editingRow!)}\n              disabled={loading}\n            >\n              {loading ? (\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n              ) : (\n                \"Update\"\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* New Row Dialog */}\n      <Dialog open={!!newRow} onOpenChange={() => setNewRow(null)}>\n        <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto\">\n          <DialogHeader>\n            <DialogTitle>New Row</DialogTitle>\n            <DialogDescription>\n              Add a new row to the {selectedTable} table.\n            </DialogDescription>\n          </DialogHeader>\n          {newRow && tableData && (\n            <div className=\"space-y-4\">\n              {tableData.columns.map((column) => (\n                <div key={column.name} className=\"space-y-2\">\n                  <Label htmlFor={`new-${column.name}`}>\n                    {column.name}\n                    {column.notnull && (\n                      <span className=\"text-xs text-destructive ml-2\">\n                        (Required)\n                      </span>\n                    )}\n                  </Label>\n                  {getInputType(column) === \"checkbox\" ? (\n                    <input\n                      type=\"checkbox\"\n                      id={`new-${column.name}`}\n                      checked={newRow[column.name] || false}\n                      onChange={(e) =>\n                        setNewRow({\n                          ...newRow,\n                          [column.name]: e.target.checked,\n                        })\n                      }\n                      className=\"h-4 w-4\"\n                    />\n                  ) : (\n                    <Input\n                      id={`new-${column.name}`}\n                      type={getInputType(column)}\n                      value={newRow[column.name] ?? \"\"}\n                      onChange={(e) =>\n                        setNewRow({\n                          ...newRow,\n                          [column.name]: e.target.value,\n                        })\n                      }\n                      placeholder={column.dflt_value || \"NULL\"}\n                    />\n                  )}\n                  <p className=\"text-xs text-muted-foreground\">\n                    Type: {column.type_name}\n                    {column.dflt_value && `, Default: ${column.dflt_value}`}\n                  </p>\n                </div>\n              ))}\n            </div>\n          )}\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setNewRow(null)}>\n              Cancel\n            </Button>\n            <Button\n              onClick={() => handleInsertRow(newRow!)}\n              disabled={loading}\n            >\n              {loading ? (\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n              ) : (\n                \"Insert\"\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete Confirmation Dialog */}\n      <Dialog open={!!deletingRow} onOpenChange={() => setDeletingRow(null)}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Delete Row</DialogTitle>\n            <DialogDescription>\n              Are you sure you want to delete this row? This action cannot be\n              undone.\n            </DialogDescription>\n          </DialogHeader>\n          {deletingRow && (\n            <div className=\"rounded-md bg-muted p-4\">\n              <pre className=\"text-xs font-mono overflow-x-auto max-h-[200px] overflow-y-auto\">\n                {JSON.stringify(\n                  Object.fromEntries(\n                    Object.entries(deletingRow).map(([key, value]) => [\n                      key,\n                      typeof value === \"string\" && value.length > 100\n                        ? value.substring(0, 100) + \"...\"\n                        : value\n                    ])\n                  ),\n                  null,\n                  2\n                )}\n              </pre>\n            </div>\n          )}\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setDeletingRow(null)}>\n              Cancel\n            </Button>\n            <Button\n              variant=\"destructive\"\n              onClick={handleDeleteRow}\n              disabled={loading}\n            >\n              {loading ? (\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n              ) : (\n                \"Delete\"\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Reset Database Confirmation */}\n      <Dialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Reset Database</DialogTitle>\n            <DialogDescription>\n              This will delete all data and recreate the database with its default structure \n              (empty tables for agents, agent_runs, and app_settings). The database will be \n              restored to the same state as when you first installed the app. This action \n              cannot be undone.\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"flex items-center gap-3 p-4 rounded-md bg-destructive/10 text-destructive\">\n            <AlertTriangle className=\"h-5 w-5\" />\n            <span className=\"text-sm font-medium\">\n              All your agents, runs, and settings will be permanently deleted!\n            </span>\n          </div>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => setShowResetConfirm(false)}\n            >\n              Cancel\n            </Button>\n            <Button\n              variant=\"destructive\"\n              onClick={handleResetDatabase}\n              disabled={loading}\n            >\n              {loading ? (\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n              ) : (\n                \"Reset Database\"\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* SQL Query Editor */}\n      <Dialog open={showSqlEditor} onOpenChange={setShowSqlEditor}>\n        <DialogContent className=\"max-w-4xl max-h-[80vh]\">\n          <DialogHeader>\n            <DialogTitle>SQL Query Editor</DialogTitle>\n            <DialogDescription>\n              Execute raw SQL queries on the database. Use with caution.\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"space-y-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"sql-query\">SQL Query</Label>\n              <Textarea\n                id=\"sql-query\"\n                value={sqlQuery}\n                onChange={(e) => setSqlQuery(e.target.value)}\n                placeholder=\"SELECT * FROM agents LIMIT 10;\"\n                className=\"font-mono text-sm h-32\"\n              />\n            </div>\n\n            {sqlError && (\n              <div className=\"p-3 rounded-md bg-destructive/10 text-destructive text-sm\">\n                <div className=\"flex items-center gap-2\">\n                  <X className=\"h-4 w-4\" />\n                  {sqlError}\n                </div>\n              </div>\n            )}\n\n            {sqlResult && (\n              <div className=\"space-y-2\">\n                {sqlResult.rows_affected !== undefined ? (\n                  <div className=\"p-3 rounded-md bg-green-500/10 text-green-600 dark:text-green-400 text-sm\">\n                    <div className=\"flex items-center gap-2\">\n                      <Check className=\"h-4 w-4\" />\n                      Query executed successfully. {sqlResult.rows_affected} rows\n                      affected.\n                      {sqlResult.last_insert_rowid && (\n                        <span>\n                          Last insert ID: {sqlResult.last_insert_rowid}\n                        </span>\n                      )}\n                    </div>\n                  </div>\n                ) : (\n                  <div className=\"border rounded-md overflow-hidden\">\n                    <div className=\"overflow-x-auto max-h-96\">\n                      <table className=\"w-full text-xs\">\n                        <thead>\n                          <tr className=\"border-b bg-muted/50\">\n                            {sqlResult.columns.map((col, i) => (\n                              <th\n                                key={i}\n                                className=\"px-2 py-1 text-left font-medium\"\n                              >\n                                {col}\n                              </th>\n                            ))}\n                          </tr>\n                        </thead>\n                        <tbody>\n                          {sqlResult.rows.map((row, i) => (\n                            <tr key={i} className=\"border-b\">\n                              {row.map((cell, j) => {\n                                const formattedValue = formatCellValue(cell, 50);\n                                const fullValue = cell === null ? \"NULL\" : \n                                                cell === undefined ? \"\" : \n                                                typeof cell === \"object\" ? JSON.stringify(cell, null, 2) : \n                                                String(cell);\n                                const isTruncated = fullValue.length > 50;\n                                \n                                return (\n                                  <td key={j} className=\"px-2 py-1 font-mono\">\n                                    {isTruncated ? (\n                                      <TooltipProvider>\n                                        <Tooltip>\n                                          <TooltipTrigger asChild>\n                                            <span className=\"cursor-help block truncate max-w-[200px]\">\n                                              {formattedValue}\n                                            </span>\n                                          </TooltipTrigger>\n                                          <TooltipContent \n                                            side=\"bottom\" \n                                            className=\"max-w-[500px] max-h-[300px] overflow-auto\"\n                                          >\n                                            <pre className=\"text-xs whitespace-pre-wrap\">{fullValue}</pre>\n                                          </TooltipContent>\n                                        </Tooltip>\n                                      </TooltipProvider>\n                                    ) : (\n                                      <span className=\"block truncate max-w-[200px]\">\n                                        {formattedValue}\n                                      </span>\n                                    )}\n                                  </td>\n                                );\n                              })}\n                            </tr>\n                          ))}\n                        </tbody>\n                      </table>\n                    </div>\n                  </div>\n                )}\n              </div>\n            )}\n          </div>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                setShowSqlEditor(false);\n                setSqlQuery(\"\");\n                setSqlResult(null);\n                setSqlError(null);\n              }}\n            >\n              Close\n            </Button>\n            <Button\n              onClick={handleExecuteSql}\n              disabled={loading || !sqlQuery.trim()}\n            >\n              {loading ? (\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n              ) : (\n                \"Execute\"\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Toast Notification */}\n      <ToastContainer>\n        {toast && (\n          <Toast\n            message={toast.message}\n            type={toast.type}\n            onDismiss={() => setToast(null)}\n          />\n        )}\n      </ToastContainer>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/StreamMessage.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { \n  Terminal, \n  User, \n  Bot, \n  AlertCircle, \n  CheckCircle2\n} from \"lucide-react\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { getClaudeSyntaxTheme } from \"@/lib/claudeSyntaxTheme\";\nimport { useTheme } from \"@/hooks\";\nimport type { ClaudeStreamMessage } from \"./AgentExecution\";\nimport {\n  TodoWidget,\n  TodoReadWidget,\n  LSWidget,\n  ReadWidget,\n  ReadResultWidget,\n  GlobWidget,\n  BashWidget,\n  WriteWidget,\n  GrepWidget,\n  EditWidget,\n  EditResultWidget,\n  MCPWidget,\n  CommandWidget,\n  CommandOutputWidget,\n  SummaryWidget,\n  MultiEditWidget,\n  MultiEditResultWidget,\n  SystemReminderWidget,\n  SystemInitializedWidget,\n  TaskWidget,\n  LSResultWidget,\n  ThinkingWidget,\n  WebSearchWidget,\n  WebFetchWidget\n} from \"./ToolWidgets\";\n\ninterface StreamMessageProps {\n  message: ClaudeStreamMessage;\n  className?: string;\n  streamMessages: ClaudeStreamMessage[];\n  onLinkDetected?: (url: string) => void;\n}\n\n/**\n * Component to render a single Claude Code stream message\n */\nconst StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, className, streamMessages, onLinkDetected }) => {\n  // State to track tool results mapped by tool call ID\n  const [toolResults, setToolResults] = useState<Map<string, any>>(new Map());\n  \n  // Get current theme\n  const { theme } = useTheme();\n  const syntaxTheme = getClaudeSyntaxTheme(theme);\n  \n  // Extract all tool results from stream messages\n  useEffect(() => {\n    const results = new Map<string, any>();\n    \n    // Iterate through all messages to find tool results\n    streamMessages.forEach(msg => {\n      if (msg.type === \"user\" && msg.message?.content && Array.isArray(msg.message.content)) {\n        msg.message.content.forEach((content: any) => {\n          if (content.type === \"tool_result\" && content.tool_use_id) {\n            results.set(content.tool_use_id, content);\n          }\n        });\n      }\n    });\n    \n    setToolResults(results);\n  }, [streamMessages]);\n  \n  // Helper to get tool result for a specific tool call ID\n  const getToolResult = (toolId: string | undefined): any => {\n    if (!toolId) return null;\n    return toolResults.get(toolId) || null;\n  };\n  \n  try {\n    // Skip rendering for meta messages that don't have meaningful content\n    if (message.isMeta && !message.leafUuid && !message.summary) {\n      return null;\n    }\n\n    // Handle summary messages\n    if (message.leafUuid && message.summary && (message as any).type === \"summary\") {\n      return <SummaryWidget summary={message.summary} leafUuid={message.leafUuid} />;\n    }\n\n    // System initialization message\n    if (message.type === \"system\" && message.subtype === \"init\") {\n      return (\n        <SystemInitializedWidget\n          sessionId={message.session_id}\n          model={message.model}\n          cwd={message.cwd}\n          tools={message.tools}\n        />\n      );\n    }\n\n    // Assistant message\n    if (message.type === \"assistant\" && message.message) {\n      const msg = message.message;\n      \n      let renderedSomething = false;\n      \n      const renderedCard = (\n        <Card className={cn(\"border-primary/20 bg-primary/5\", className)}>\n          <CardContent className=\"p-4\">\n            <div className=\"flex items-start gap-3\">\n              <Bot className=\"h-5 w-5 text-primary mt-0.5\" />\n              <div className=\"flex-1 space-y-2 min-w-0\">\n                {msg.content && Array.isArray(msg.content) && msg.content.map((content: any, idx: number) => {\n                  // Text content - render as markdown\n                  if (content.type === \"text\") {\n                    // Ensure we have a string to render\n                    const textContent = typeof content.text === 'string' \n                      ? content.text \n                      : (content.text?.text || JSON.stringify(content.text || content));\n                    \n                    renderedSomething = true;\n                    return (\n                      <div key={idx} className=\"prose prose-sm dark:prose-invert max-w-none\">\n                        <ReactMarkdown\n                          remarkPlugins={[remarkGfm]}\n                          components={{\n                            code({ node, inline, className, children, ...props }: any) {\n                              const match = /language-(\\w+)/.exec(className || '');\n                              return !inline && match ? (\n                                <SyntaxHighlighter\n                                  style={syntaxTheme}\n                                  language={match[1]}\n                                  PreTag=\"div\"\n                                  {...props}\n                                >\n                                  {String(children).replace(/\\n$/, '')}\n                                </SyntaxHighlighter>\n                              ) : (\n                                <code className={className} {...props}>\n                                  {children}\n                                </code>\n                              );\n                            }\n                          }}\n                        >\n                          {textContent}\n                        </ReactMarkdown>\n                      </div>\n                    );\n                  }\n                  \n                  // Thinking content - render with ThinkingWidget\n                  if (content.type === \"thinking\") {\n                    renderedSomething = true;\n                    return (\n                      <div key={idx}>\n                        <ThinkingWidget \n                          thinking={content.thinking || ''} \n                          signature={content.signature}\n                        />\n                      </div>\n                    );\n                  }\n                  \n                  // Tool use - render custom widgets based on tool name\n                  if (content.type === \"tool_use\") {\n                    const toolName = content.name?.toLowerCase();\n                    const input = content.input;\n                    const toolId = content.id;\n                    \n                    // Get the tool result if available\n                    const toolResult = getToolResult(toolId);\n                    \n                    // Function to render the appropriate tool widget\n                    const renderToolWidget = () => {\n                      // Task tool - for sub-agent tasks\n                      if (toolName === \"task\" && input) {\n                        renderedSomething = true;\n                        return <TaskWidget description={input.description} prompt={input.prompt} result={toolResult} />;\n                      }\n                      \n                      // Edit tool\n                      if (toolName === \"edit\" && input?.file_path) {\n                        renderedSomething = true;\n                        return <EditWidget {...input} result={toolResult} />;\n                      }\n                      \n                      // MultiEdit tool\n                      if (toolName === \"multiedit\" && input?.file_path && input?.edits) {\n                        renderedSomething = true;\n                        return <MultiEditWidget {...input} result={toolResult} />;\n                      }\n                      \n                      // MCP tools (starting with mcp__)\n                      if (content.name?.startsWith(\"mcp__\")) {\n                        renderedSomething = true;\n                        return <MCPWidget toolName={content.name} input={input} result={toolResult} />;\n                      }\n                      \n                      // TodoWrite tool\n                      if (toolName === \"todowrite\" && input?.todos) {\n                        renderedSomething = true;\n                        return <TodoWidget todos={input.todos} result={toolResult} />;\n                      }\n                      \n                      // TodoRead tool\n                      if (toolName === \"todoread\") {\n                        renderedSomething = true;\n                        return <TodoReadWidget todos={input?.todos} result={toolResult} />;\n                      }\n                      \n                      // LS tool\n                      if (toolName === \"ls\" && input?.path) {\n                        renderedSomething = true;\n                        return <LSWidget path={input.path} result={toolResult} />;\n                      }\n                      \n                      // Read tool\n                      if (toolName === \"read\" && input?.file_path) {\n                        renderedSomething = true;\n                        return <ReadWidget filePath={input.file_path} result={toolResult} />;\n                      }\n                      \n                      // Glob tool\n                      if (toolName === \"glob\" && input?.pattern) {\n                        renderedSomething = true;\n                        return <GlobWidget pattern={input.pattern} result={toolResult} />;\n                      }\n                      \n                      // Bash tool\n                      if (toolName === \"bash\" && input?.command) {\n                        renderedSomething = true;\n                        return <BashWidget command={input.command} description={input.description} result={toolResult} />;\n                      }\n                      \n                      // Write tool\n                      if (toolName === \"write\" && input?.file_path && input?.content) {\n                        renderedSomething = true;\n                        return <WriteWidget filePath={input.file_path} content={input.content} result={toolResult} />;\n                      }\n                      \n                      // Grep tool\n                      if (toolName === \"grep\" && input?.pattern) {\n                        renderedSomething = true;\n                        return <GrepWidget pattern={input.pattern} include={input.include} path={input.path} exclude={input.exclude} result={toolResult} />;\n                      }\n                      \n                      // WebSearch tool\n                      if (toolName === \"websearch\" && input?.query) {\n                        renderedSomething = true;\n                        return <WebSearchWidget query={input.query} result={toolResult} />;\n                      }\n                      \n                      // WebFetch tool\n                      if (toolName === \"webfetch\" && input?.url) {\n                        renderedSomething = true;\n                        return <WebFetchWidget url={input.url} prompt={input.prompt} result={toolResult} />;\n                      }\n                      \n                      // Default - return null\n                      return null;\n                    };\n                    \n                    // Render the tool widget\n                    const widget = renderToolWidget();\n                    if (widget) {\n                      renderedSomething = true;\n                      return <div key={idx}>{widget}</div>;\n                    }\n                    \n                    // Fallback to basic tool display\n                    renderedSomething = true;\n                    return (\n                      <div key={idx} className=\"space-y-2\">\n                        <div className=\"flex items-center gap-2\">\n                          <Terminal className=\"h-4 w-4 text-muted-foreground\" />\n                          <span className=\"text-sm font-medium\">\n                            Using tool: <code className=\"font-mono\">{content.name}</code>\n                          </span>\n                        </div>\n                        {content.input && (\n                          <div className=\"ml-6 p-2 bg-background rounded-md border\">\n                            <pre className=\"text-xs font-mono overflow-x-auto\">\n                              {JSON.stringify(content.input, null, 2)}\n                            </pre>\n                          </div>\n                        )}\n                      </div>\n                    );\n                  }\n                  \n                  return null;\n                })}\n                \n                {msg.usage && (\n                  <div className=\"text-xs text-muted-foreground mt-2\">\n                    Tokens: {msg.usage.input_tokens} in, {msg.usage.output_tokens} out\n                  </div>\n                )}\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      );\n      \n      if (!renderedSomething) return null;\n      return renderedCard;\n    }\n\n    // User message - handle both nested and direct content structures\n    if (message.type === \"user\") {\n      // Don't render meta messages, which are for system use\n      if (message.isMeta) return null;\n\n      // Handle different message structures\n      const msg = message.message || message;\n      \n      let renderedSomething = false;\n      \n      const renderedCard = (\n        <Card className={cn(\"border-muted-foreground/20 bg-muted/20\", className)}>\n          <CardContent className=\"p-4\">\n            <div className=\"flex items-start gap-3\">\n              <User className=\"h-5 w-5 text-muted-foreground mt-0.5\" />\n              <div className=\"flex-1 space-y-2 min-w-0\">\n                {/* Handle content that is a simple string (e.g. from user commands) */}\n                {(typeof msg.content === 'string' || (msg.content && !Array.isArray(msg.content))) && (\n                  (() => {\n                    const contentStr = typeof msg.content === 'string' ? msg.content : String(msg.content);\n                    if (contentStr.trim() === '') return null;\n                    renderedSomething = true;\n                    \n                    // Check if it's a command message\n                    const commandMatch = contentStr.match(/<command-name>(.+?)<\\/command-name>[\\s\\S]*?<command-message>(.+?)<\\/command-message>[\\s\\S]*?<command-args>(.*?)<\\/command-args>/);\n                    if (commandMatch) {\n                      const [, commandName, commandMessage, commandArgs] = commandMatch;\n                      return (\n                        <CommandWidget \n                          commandName={commandName.trim()} \n                          commandMessage={commandMessage.trim()}\n                          commandArgs={commandArgs?.trim()}\n                        />\n                      );\n                    }\n                    \n                    // Check if it's command output\n                    const stdoutMatch = contentStr.match(/<local-command-stdout>([\\s\\S]*?)<\\/local-command-stdout>/);\n                    if (stdoutMatch) {\n                      const [, output] = stdoutMatch;\n                      return <CommandOutputWidget output={output} onLinkDetected={onLinkDetected} />;\n                    }\n                    \n                    // Otherwise render as plain text\n                    return (\n                      <div className=\"text-sm\">\n                        {contentStr}\n                      </div>\n                    );\n                  })()\n                )}\n\n                {/* Handle content that is an array of parts */}\n                {Array.isArray(msg.content) && msg.content.map((content: any, idx: number) => {\n                  // Tool result\n                  if (content.type === \"tool_result\") {\n                    // Skip duplicate tool_result if a dedicated widget is present\n                    let hasCorrespondingWidget = false;\n                    if (content.tool_use_id && streamMessages) {\n                      for (let i = streamMessages.length - 1; i >= 0; i--) {\n                        const prevMsg = streamMessages[i];\n                        if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {\n                          const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id);\n                          if (toolUse) {\n                            const toolName = toolUse.name?.toLowerCase();\n                            const toolsWithWidgets = ['task','edit','multiedit','todowrite','todoread','ls','read','glob','bash','write','grep','websearch','webfetch'];\n                            if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {\n                              hasCorrespondingWidget = true;\n                            }\n                            break;\n                          }\n                        }\n                      }\n                    }\n\n                    if (hasCorrespondingWidget) {\n                      return null;\n                    }\n                    // Extract the actual content string\n                    let contentText = '';\n                    if (typeof content.content === 'string') {\n                      contentText = content.content;\n                    } else if (content.content && typeof content.content === 'object') {\n                      // Handle object with text property\n                      if (content.content.text) {\n                        contentText = content.content.text;\n                      } else if (Array.isArray(content.content)) {\n                        // Handle array of content blocks\n                        contentText = content.content\n                          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n                          .join('\\n');\n                      } else {\n                        // Fallback to JSON stringify\n                        contentText = JSON.stringify(content.content, null, 2);\n                      }\n                    }\n                    \n                    // Always show system reminders regardless of widget status\n                    const reminderMatch = contentText.match(/<system-reminder>(.*?)<\\/system-reminder>/s);\n                    if (reminderMatch) {\n                      const reminderMessage = reminderMatch[1].trim();\n                      const beforeReminder = contentText.substring(0, reminderMatch.index || 0).trim();\n                      const afterReminder = contentText.substring((reminderMatch.index || 0) + reminderMatch[0].length).trim();\n                      \n                      renderedSomething = true;\n                      return (\n                        <div key={idx} className=\"space-y-2\">\n                          <div className=\"flex items-center gap-2\">\n                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                            <span className=\"text-sm font-medium\">Tool Result</span>\n                          </div>\n                          \n                          {beforeReminder && (\n                            <div className=\"ml-6 p-2 bg-background rounded-md border\">\n                              <pre className=\"text-xs font-mono overflow-x-auto whitespace-pre-wrap\">\n                                {beforeReminder}\n                              </pre>\n                            </div>\n                          )}\n                          \n                          <div className=\"ml-6\">\n                            <SystemReminderWidget message={reminderMessage} />\n                          </div>\n                          \n                          {afterReminder && (\n                            <div className=\"ml-6 p-2 bg-background rounded-md border\">\n                              <pre className=\"text-xs font-mono overflow-x-auto whitespace-pre-wrap\">\n                                {afterReminder}\n                              </pre>\n                            </div>\n                          )}\n                        </div>\n                      );\n                    }\n                    \n                    // Check if this is an Edit tool result\n                    const isEditResult = contentText.includes(\"has been updated. Here's the result of running `cat -n`\");\n                    \n                    if (isEditResult) {\n                      renderedSomething = true;\n                      return (\n                        <div key={idx} className=\"space-y-2\">\n                          <div className=\"flex items-center gap-2\">\n                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                            <span className=\"text-sm font-medium\">Edit Result</span>\n                          </div>\n                          <EditResultWidget content={contentText} />\n                        </div>\n                      );\n                    }\n                    \n                    // Check if this is a MultiEdit tool result\n                    const isMultiEditResult = contentText.includes(\"has been updated with multiple edits\") || \n                                             contentText.includes(\"MultiEdit completed successfully\") ||\n                                             contentText.includes(\"Applied multiple edits to\");\n                    \n                    if (isMultiEditResult) {\n                      renderedSomething = true;\n                      return (\n                        <div key={idx} className=\"space-y-2\">\n                          <div className=\"flex items-center gap-2\">\n                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                            <span className=\"text-sm font-medium\">MultiEdit Result</span>\n                          </div>\n                          <MultiEditResultWidget content={contentText} />\n                        </div>\n                      );\n                    }\n                    \n                    // Check if this is an LS tool result (directory tree structure)\n                    const isLSResult = (() => {\n                      if (!content.tool_use_id || typeof contentText !== 'string') return false;\n                      \n                      // Check if this result came from an LS tool by looking for the tool call\n                      let isFromLSTool = false;\n                      \n                      // Search in previous assistant messages for the matching tool_use\n                      if (streamMessages) {\n                        for (let i = streamMessages.length - 1; i >= 0; i--) {\n                          const prevMsg = streamMessages[i];\n                          // Only check assistant messages\n                          if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {\n                            const toolUse = prevMsg.message.content.find((c: any) => \n                              c.type === 'tool_use' && \n                              c.id === content.tool_use_id &&\n                              c.name?.toLowerCase() === 'ls'\n                            );\n                            if (toolUse) {\n                              isFromLSTool = true;\n                              break;\n                            }\n                          }\n                        }\n                      }\n                      \n                      // Only proceed if this is from an LS tool\n                      if (!isFromLSTool) return false;\n                      \n                      // Additional validation: check for tree structure pattern\n                      const lines = contentText.split('\\n');\n                      const hasTreeStructure = lines.some(line => /^\\s*-\\s+/.test(line));\n                      const hasNoteAtEnd = lines.some(line => line.trim().startsWith('NOTE: do any of the files'));\n                      \n                      return hasTreeStructure || hasNoteAtEnd;\n                    })();\n                    \n                    if (isLSResult) {\n                      renderedSomething = true;\n                      return (\n                        <div key={idx} className=\"space-y-2\">\n                          <div className=\"flex items-center gap-2\">\n                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                            <span className=\"text-sm font-medium\">Directory Contents</span>\n                          </div>\n                          <LSResultWidget content={contentText} />\n                        </div>\n                      );\n                    }\n                    \n                    // Check if this is a Read tool result (contains line numbers with arrow separator)\n                    const isReadResult = content.tool_use_id && typeof contentText === 'string' && \n                      /^\\s*\\d+→/.test(contentText);\n                    \n                    if (isReadResult) {\n                      // Try to find the corresponding Read tool call to get the file path\n                      let filePath: string | undefined;\n                      \n                      // Search in previous assistant messages for the matching tool_use\n                      if (streamMessages) {\n                        for (let i = streamMessages.length - 1; i >= 0; i--) {\n                          const prevMsg = streamMessages[i];\n                          // Only check assistant messages\n                          if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {\n                            const toolUse = prevMsg.message.content.find((c: any) => \n                              c.type === 'tool_use' && \n                              c.id === content.tool_use_id &&\n                              c.name?.toLowerCase() === 'read'\n                            );\n                            if (toolUse?.input?.file_path) {\n                              filePath = toolUse.input.file_path;\n                              break;\n                            }\n                          }\n                        }\n                      }\n                      \n                      renderedSomething = true;\n                      return (\n                        <div key={idx} className=\"space-y-2\">\n                          <div className=\"flex items-center gap-2\">\n                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                            <span className=\"text-sm font-medium\">Read Result</span>\n                          </div>\n                          <ReadResultWidget content={contentText} filePath={filePath} />\n                        </div>\n                      );\n                    }\n                    \n                    // Handle empty tool results\n                    if (!contentText || contentText.trim() === '') {\n                      renderedSomething = true;\n                      return (\n                        <div key={idx} className=\"space-y-2\">\n                          <div className=\"flex items-center gap-2\">\n                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                            <span className=\"text-sm font-medium\">Tool Result</span>\n                          </div>\n                          <div className=\"ml-6 p-3 bg-muted/50 rounded-md border text-sm text-muted-foreground italic\">\n                            Tool did not return any output\n                          </div>\n                        </div>\n                      );\n                    }\n                    \n                    renderedSomething = true;\n                    return (\n                      <div key={idx} className=\"space-y-2\">\n                        <div className=\"flex items-center gap-2\">\n                          {content.is_error ? (\n                            <AlertCircle className=\"h-4 w-4 text-destructive\" />\n                          ) : (\n                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                          )}\n                          <span className=\"text-sm font-medium\">Tool Result</span>\n                        </div>\n                        <div className=\"ml-6 p-2 bg-background rounded-md border\">\n                          <pre className=\"text-xs font-mono overflow-x-auto whitespace-pre-wrap\">\n                            {contentText}\n                          </pre>\n                        </div>\n                      </div>\n                    );\n                  }\n                  \n                  // Text content\n                  if (content.type === \"text\") {\n                    // Handle both string and object formats\n                    const textContent = typeof content.text === 'string' \n                      ? content.text \n                      : (content.text?.text || JSON.stringify(content.text));\n                    \n                    renderedSomething = true;\n                    return (\n                      <div key={idx} className=\"text-sm\">\n                        {textContent}\n                      </div>\n                    );\n                  }\n                  \n                  return null;\n                })}\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      );\n      if (!renderedSomething) return null;\n      return renderedCard;\n    }\n\n    // Result message - render with markdown\n    if (message.type === \"result\") {\n      const isError = message.is_error || message.subtype?.includes(\"error\");\n      \n      return (\n        <Card className={cn(\n          isError ? \"border-destructive/20 bg-destructive/5\" : \"border-green-500/20 bg-green-500/5\",\n          className\n        )}>\n          <CardContent className=\"p-4\">\n            <div className=\"flex items-start gap-3\">\n              {isError ? (\n                <AlertCircle className=\"h-5 w-5 text-destructive mt-0.5\" />\n              ) : (\n                <CheckCircle2 className=\"h-5 w-5 text-green-500 mt-0.5\" />\n              )}\n              <div className=\"flex-1 space-y-2\">\n                <h4 className=\"font-semibold text-sm\">\n                  {isError ? \"Execution Failed\" : \"Execution Complete\"}\n                </h4>\n                \n                {message.result && (\n                  <div className=\"prose prose-sm dark:prose-invert max-w-none\">\n                    <ReactMarkdown\n                      remarkPlugins={[remarkGfm]}\n                      components={{\n                        code({ node, inline, className, children, ...props }: any) {\n                          const match = /language-(\\w+)/.exec(className || '');\n                          return !inline && match ? (\n                            <SyntaxHighlighter\n                              style={syntaxTheme}\n                              language={match[1]}\n                              PreTag=\"div\"\n                              {...props}\n                            >\n                              {String(children).replace(/\\n$/, '')}\n                            </SyntaxHighlighter>\n                          ) : (\n                            <code className={className} {...props}>\n                              {children}\n                            </code>\n                          );\n                        }\n                      }}\n                    >\n                      {message.result}\n                    </ReactMarkdown>\n                  </div>\n                )}\n                \n                {message.error && (\n                  <div className=\"text-sm text-destructive\">{message.error}</div>\n                )}\n                \n                <div className=\"text-xs text-muted-foreground space-y-1 mt-2\">\n                  {(message.cost_usd !== undefined || message.total_cost_usd !== undefined) && (\n                    <div>Cost: ${((message.cost_usd || message.total_cost_usd)!).toFixed(4)} USD</div>\n                  )}\n                  {message.duration_ms !== undefined && (\n                    <div>Duration: {(message.duration_ms / 1000).toFixed(2)}s</div>\n                  )}\n                  {message.num_turns !== undefined && (\n                    <div>Turns: {message.num_turns}</div>\n                  )}\n                  {message.usage && (\n                    <div>\n                      Total tokens: {message.usage.input_tokens + message.usage.output_tokens} \n                      ({message.usage.input_tokens} in, {message.usage.output_tokens} out)\n                    </div>\n                  )}\n                </div>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      );\n    }\n\n    // Skip rendering if no meaningful content\n    return null;\n  } catch (error) {\n    // If any error occurs during rendering, show a safe error message\n    console.error(\"Error rendering stream message:\", error, message);\n    return (\n      <Card className={cn(\"border-destructive/20 bg-destructive/5\", className)}>\n        <CardContent className=\"p-4\">\n          <div className=\"flex items-start gap-3\">\n            <AlertCircle className=\"h-5 w-5 text-destructive mt-0.5\" />\n            <div className=\"flex-1\">\n              <p className=\"text-sm font-medium\">Error rendering message</p>\n              <p className=\"text-xs text-muted-foreground mt-1\">\n                {error instanceof Error ? error.message : 'Unknown error'}\n              </p>\n            </div>\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n};\n\nexport const StreamMessage = React.memo(StreamMessageComponent);\n"
  },
  {
    "path": "src/components/TabContent.tsx",
    "content": "import React, { Suspense, lazy, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { useTabState } from '@/hooks/useTabState';\nimport { useScreenTracking } from '@/hooks/useAnalytics';\nimport { Tab } from '@/contexts/TabContext';\nimport { Loader2, Plus, ArrowLeft } from 'lucide-react';\nimport { api, type Project, type Session, type ClaudeMdFile } from '@/lib/api';\nimport { ProjectList } from '@/components/ProjectList';\nimport { SessionList } from '@/components/SessionList';\nimport { Button } from '@/components/ui/button';\n\n// Lazy load heavy components\nconst ClaudeCodeSession = lazy(() => import('@/components/ClaudeCodeSession').then(m => ({ default: m.ClaudeCodeSession })));\nconst AgentRunOutputViewer = lazy(() => import('@/components/AgentRunOutputViewer'));\nconst AgentExecution = lazy(() => import('@/components/AgentExecution').then(m => ({ default: m.AgentExecution })));\nconst CreateAgent = lazy(() => import('@/components/CreateAgent').then(m => ({ default: m.CreateAgent })));\nconst Agents = lazy(() => import('@/components/Agents').then(m => ({ default: m.Agents })));\nconst UsageDashboard = lazy(() => import('@/components/UsageDashboard').then(m => ({ default: m.UsageDashboard })));\nconst MCPManager = lazy(() => import('@/components/MCPManager').then(m => ({ default: m.MCPManager })));\nconst Settings = lazy(() => import('@/components/Settings').then(m => ({ default: m.Settings })));\nconst MarkdownEditor = lazy(() => import('@/components/MarkdownEditor').then(m => ({ default: m.MarkdownEditor })));\n// const ClaudeFileEditor = lazy(() => import('@/components/ClaudeFileEditor').then(m => ({ default: m.ClaudeFileEditor })));\n\n// Import non-lazy components for projects view\n\ninterface TabPanelProps {\n  tab: Tab;\n  isActive: boolean;\n}\n\nconst TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {\n  const { updateTab } = useTabState();\n  const [projects, setProjects] = React.useState<Project[]>([]);\n  const [selectedProject, setSelectedProject] = React.useState<Project | null>(null);\n  const [sessions, setSessions] = React.useState<Session[]>([]);\n  const [loading, setLoading] = React.useState(false);\n  \n  // Track screen when tab becomes active\n  useScreenTracking(isActive ? tab.type : undefined, isActive ? tab.id : undefined);\n  const [error, setError] = React.useState<string | null>(null);\n  \n  // Load projects when tab becomes active and is of type 'projects'\n  useEffect(() => {\n    if (isActive && tab.type === 'projects') {\n      loadProjects();\n    }\n  }, [isActive, tab.type]);\n  \n  const loadProjects = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n      const projectList = await api.listProjects();\n      setProjects(projectList);\n    } catch (err) {\n      console.error(\"Failed to load projects:\", err);\n      setError(\"Failed to load projects. Please ensure ~/.claude directory exists.\");\n    } finally {\n      setLoading(false);\n    }\n  };\n  \n  const handleProjectClick = async (project: Project) => {\n    try {\n      setLoading(true);\n      setError(null);\n      const sessionList = await api.getProjectSessions(project.id);\n      setSessions(sessionList);\n      setSelectedProject(project);\n      \n      // Update tab title to show project name\n      const projectName = project.path.split('/').pop() || 'Project';\n      updateTab(tab.id, {\n        title: projectName\n      });\n    } catch (err) {\n      console.error(\"Failed to load sessions:\", err);\n      setError(\"Failed to load sessions for this project.\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleOpenProject = async () => {\n    console.log('handleOpenProject called');\n    try {\n      // Use native dialog to pick folder\n      const { open } = await import('@tauri-apps/plugin-dialog');\n      const selected = await open({\n        directory: true,\n        multiple: false,\n        title: 'Select Project Folder',\n        defaultPath: await api.getHomeDirectory(),\n      });\n      \n      console.log('Selected folder:', selected);\n      \n      if (selected && typeof selected === 'string') {\n        // Create or open project for the selected directory\n        const project = await api.createProject(selected);\n        await loadProjects();\n        await handleProjectClick(project);\n      }\n    } catch (err) {\n      console.error('Failed to open folder picker:', err);\n      setError('Failed to open folder picker');\n    }\n  };\n  \n  const handleNewSession = () => {\n    // Update current tab to show new chat session instead of creating a new tab\n    if (selectedProject) {\n      const projectName = selectedProject.path.split('/').pop() || 'Session';\n      updateTab(tab.id, {\n        type: 'chat',\n        title: projectName,\n        sessionId: undefined,\n        sessionData: undefined,\n        initialProjectPath: selectedProject.path\n      });\n    } else {\n      updateTab(tab.id, {\n        type: 'chat',\n        title: 'New Session',\n        sessionId: undefined,\n        sessionData: undefined,\n        initialProjectPath: undefined\n      });\n    }\n  };\n  \n  // Panel visibility - hide when not active\n  const panelVisibilityClass = isActive ? \"\" : \"hidden\";\n  \n  const renderContent = () => {\n    switch (tab.type) {\n      case 'projects':\n        return (\n          <div className=\"h-full\">\n              {/* Content based on selection */}\n              {selectedProject ? (\n                <div className=\"h-full overflow-y-auto\">\n                  <div className=\"max-w-6xl mx-auto p-6\">\n                    <div className=\"mb-6\">\n                      <div className=\"flex items-center justify-between\">\n                        <div className=\"flex items-center gap-3\">\n                          <motion.div\n                            whileTap={{ scale: 0.97 }}\n                            transition={{ duration: 0.15 }}\n                          >\n                            <Button\n                              variant=\"ghost\"\n                              size=\"icon\"\n                              onClick={() => {\n                                setSelectedProject(null);\n                                setSessions([]);\n                                // Restore tab title to \"Projects\"\n                                updateTab(tab.id, {\n                                  title: 'Projects'\n                                });\n                              }}\n                              className=\"h-8 w-8 -ml-2\"\n                              title=\"Back to Projects\"\n                            >\n                              <ArrowLeft className=\"h-4 w-4\" />\n                            </Button>\n                          </motion.div>\n                          <div>\n                            <h1 className=\"text-3xl font-bold tracking-tight\">\n                              {selectedProject.path.split('/').pop()}\n                            </h1>\n                            <p className=\"mt-1 text-sm text-muted-foreground\">\n                              {`${sessions.length} session${sessions.length !== 1 ? 's' : ''}`}\n                            </p>\n                          </div>\n                        </div>\n                        <motion.div\n                          whileTap={{ scale: 0.97 }}\n                          transition={{ duration: 0.15 }}\n                        >\n                          <Button\n                            onClick={handleNewSession}\n                            size=\"default\"\n                          >\n                            <Plus className=\"mr-2 h-4 w-4\" />\n                            New session\n                          </Button>\n                        </motion.div>\n                      </div>\n                    </div>\n\n                    {/* Error display */}\n                    {error && (\n                      <motion.div\n                        initial={{ opacity: 0, y: 4 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        transition={{ duration: 0.15 }}\n                        className=\"mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-xs text-destructive\"\n                      >\n                        {error}\n                      </motion.div>\n                    )}\n\n                    {/* Loading state */}\n                    {loading && (\n                      <div className=\"flex items-center justify-center py-8\">\n                        <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n                      </div>\n                    )}\n\n                    {/* Session List */}\n                    {!loading && (\n                      <SessionList\n                        sessions={sessions}\n                        projectPath={selectedProject.path}\n                        onSessionClick={(session) => {\n                          // Update current tab to show the selected session\n                          updateTab(tab.id, {\n                            type: 'chat',\n                            title: session.project_path.split('/').pop() || 'Session',\n                            sessionId: session.id,\n                            sessionData: session,\n                            initialProjectPath: session.project_path\n                          });\n                        }}\n                        onEditClaudeFile={(file: ClaudeMdFile) => {\n                          // Open CLAUDE.md file in a new tab\n                          window.dispatchEvent(new CustomEvent('open-claude-file', { \n                            detail: { file } \n                          }));\n                        }}\n                      />\n                    )}\n                  </div>\n                </div>\n              ) : (\n                /* Projects List View */\n                <ProjectList\n                  projects={projects}\n                  onProjectClick={handleProjectClick}\n                  onOpenProject={handleOpenProject}\n                  loading={loading}\n                />\n              )}\n          </div>\n        );\n      \n      case 'chat':\n        return (\n          <div className=\"h-full\">\n            <ClaudeCodeSession\n              session={tab.sessionData} // Pass the full session object if available\n              initialProjectPath={tab.initialProjectPath || tab.sessionId}\n              onBack={() => {\n                // Go back to projects view in the same tab\n                updateTab(tab.id, {\n                  type: 'projects',\n                  title: 'Projects',\n                });\n              }}\n              onProjectPathChange={(path: string) => {\n                // Update tab title with directory name\n                const dirName = path.split('/').pop() || path.split('\\\\').pop() || 'Session';\n                updateTab(tab.id, {\n                  title: dirName\n                });\n              }}\n            />\n          </div>\n        );\n      \n      case 'agent':\n        if (!tab.agentRunId) {\n          return (\n            <div className=\"h-full\">\n              <div className=\"p-4\">No agent run ID specified</div>\n            </div>\n          );\n        }\n        return (\n          <div className=\"h-full\">\n            <AgentRunOutputViewer\n              agentRunId={tab.agentRunId}\n              tabId={tab.id}\n            />\n          </div>\n        );\n      \n      case 'agents':\n        return (\n          <div className=\"h-full\">\n            <Agents />\n          </div>\n        );\n      \n      case 'usage':\n        return (\n          <div className=\"h-full\">\n            <UsageDashboard onBack={() => {}} />\n          </div>\n        );\n      \n      case 'mcp':\n        return (\n          <div className=\"h-full\">\n            <MCPManager onBack={() => {}} />\n          </div>\n        );\n      \n      case 'settings':\n        return (\n          <div className=\"h-full\">\n            <Settings onBack={() => {}} />\n          </div>\n        );\n      \n      case 'claude-md':\n        return (\n          <div className=\"h-full\">\n            <MarkdownEditor onBack={() => {}} />\n          </div>\n        );\n      \n      case 'claude-file':\n        if (!tab.claudeFileId) {\n          return <div className=\"p-4\">No Claude file ID specified</div>;\n        }\n        // Note: We need to get the actual file object for ClaudeFileEditor\n        // For now, returning a placeholder\n        return <div className=\"p-4\">Claude file editor not yet implemented in tabs</div>;\n      \n      case 'agent-execution':\n        if (!tab.agentData) {\n          return <div className=\"p-4\">No agent data specified</div>;\n        }\n        return (\n          <AgentExecution\n            agent={tab.agentData}\n            projectPath={tab.projectPath}\n            tabId={tab.id}\n            onBack={() => {}}\n          />\n        );\n      \n      case 'create-agent':\n        return (\n          <CreateAgent\n            onAgentCreated={() => {\n              // Close this tab after agent is created\n              window.dispatchEvent(new CustomEvent('close-tab', { detail: { tabId: tab.id } }));\n            }}\n            onBack={() => {\n              // Close this tab when back is clicked\n              window.dispatchEvent(new CustomEvent('close-tab', { detail: { tabId: tab.id } }));\n            }}\n          />\n        );\n      \n      case 'import-agent':\n        // TODO: Implement import agent component\n        return (\n          <div className=\"h-full\">\n            <div className=\"p-4\">Import agent functionality coming soon...</div>\n          </div>\n        );\n      \n      default:\n        return (\n          <div className=\"h-full\">\n            <div className=\"p-4\">Unknown tab type: {tab.type}</div>\n          </div>\n        );\n    }\n  };\n\n  return (\n    <>\n      <motion.div\n        initial={{ opacity: 0, y: 8 }}\n        animate={{ opacity: 1, y: 0 }}\n        exit={{ opacity: 0, y: -8 }}\n        transition={{ duration: 0.15 }}\n        className={`h-full w-full ${panelVisibilityClass}`}\n      >\n        <Suspense\n          fallback={\n            <div className=\"flex items-center justify-center h-full\">\n              <Loader2 className=\"w-8 h-8 animate-spin text-muted-foreground\" />\n            </div>\n          }\n        >\n          {renderContent()}\n        </Suspense>\n      </motion.div>\n\n    </>\n  );\n};\n\nexport const TabContent: React.FC = () => {\n  const { tabs, activeTabId, createChatTab, createProjectsTab, findTabBySessionId, createClaudeFileTab, createAgentExecutionTab, createCreateAgentTab, createImportAgentTab, closeTab, updateTab } = useTabState();\n  \n  // Listen for events to open sessions in tabs\n  useEffect(() => {\n    const handleOpenSessionInTab = (event: CustomEvent) => {\n      const { session } = event.detail;\n      \n      // Check if tab already exists for this session\n      const existingTab = findTabBySessionId(session.id);\n      if (existingTab) {\n        // Update existing tab with session data and switch to it\n        updateTab(existingTab.id, {\n          sessionData: session,\n          title: session.project_path.split('/').pop() || 'Session'\n        });\n        window.dispatchEvent(new CustomEvent('switch-to-tab', { detail: { tabId: existingTab.id } }));\n      } else {\n        // Create new tab for this session\n        const projectName = session.project_path.split('/').pop() || 'Session';\n        const newTabId = createChatTab(session.id, projectName, session.project_path);\n        // Update the new tab with session data\n        updateTab(newTabId, {\n          sessionData: session,\n          initialProjectPath: session.project_path\n        });\n      }\n    };\n\n    const handleOpenClaudeFile = (event: CustomEvent) => {\n      const { file } = event.detail;\n      createClaudeFileTab(file.id, file.name || 'CLAUDE.md');\n    };\n\n    const handleOpenAgentExecution = (event: CustomEvent) => {\n      const { agent, tabId, projectPath } = event.detail;\n      createAgentExecutionTab(agent, tabId, projectPath);\n    };\n\n    const handleOpenCreateAgentTab = () => {\n      createCreateAgentTab();\n    };\n\n    const handleOpenImportAgentTab = () => {\n      createImportAgentTab();\n    };\n\n    const handleCloseTab = (event: CustomEvent) => {\n      const { tabId } = event.detail;\n      closeTab(tabId);\n    };\n\n    const handleClaudeSessionSelected = (event: CustomEvent) => {\n      const { session } = event.detail;\n      // Check if there's an existing tab for this session\n      const existingTab = findTabBySessionId(session.id);\n      if (existingTab) {\n        // If tab exists, just switch to it\n        updateTab(existingTab.id, {\n          sessionData: session,\n          title: session.project_path.split('/').pop() || 'Session',\n        });\n        window.dispatchEvent(new CustomEvent('switch-to-tab', { detail: { tabId: existingTab.id } }));\n      } else {\n        // If we're in a projects tab, update it to show the session\n        // Otherwise create a new tab (for compatibility with other parts of the app)\n        const currentTab = tabs.find(t => t.id === activeTabId);\n        if (currentTab && currentTab.type === 'projects') {\n          updateTab(currentTab.id, {\n            type: 'chat',\n            title: session.project_path.split('/').pop() || 'Session',\n            sessionId: session.id,\n            sessionData: session,\n            initialProjectPath: session.project_path\n          });\n        } else {\n          const projectName = session.project_path.split('/').pop() || 'Session';\n          const newTabId = createChatTab(session.id, projectName, session.project_path);\n          updateTab(newTabId, {\n            sessionData: session,\n            initialProjectPath: session.project_path,\n          });\n        }\n      }\n    };\n\n    window.addEventListener('open-session-in-tab', handleOpenSessionInTab as EventListener);\n    window.addEventListener('open-claude-file', handleOpenClaudeFile as EventListener);\n    window.addEventListener('open-agent-execution', handleOpenAgentExecution as EventListener);\n    window.addEventListener('open-create-agent-tab', handleOpenCreateAgentTab);\n    window.addEventListener('open-import-agent-tab', handleOpenImportAgentTab);\n    window.addEventListener('close-tab', handleCloseTab as EventListener);\n    window.addEventListener('claude-session-selected', handleClaudeSessionSelected as EventListener);\n    return () => {\n      window.removeEventListener('open-session-in-tab', handleOpenSessionInTab as EventListener);\n      window.removeEventListener('open-claude-file', handleOpenClaudeFile as EventListener);\n      window.removeEventListener('open-agent-execution', handleOpenAgentExecution as EventListener);\n      window.removeEventListener('open-create-agent-tab', handleOpenCreateAgentTab);\n      window.removeEventListener('open-import-agent-tab', handleOpenImportAgentTab);\n      window.removeEventListener('close-tab', handleCloseTab as EventListener);\n      window.removeEventListener('claude-session-selected', handleClaudeSessionSelected as EventListener);\n    };\n  }, [createChatTab, findTabBySessionId, createClaudeFileTab, createAgentExecutionTab, createCreateAgentTab, createImportAgentTab, closeTab, updateTab]);\n  \n  return (\n    <div className=\"flex-1 h-full relative\">\n      <AnimatePresence mode=\"wait\">\n        {tabs.map((tab) => (\n          <TabPanel\n            key={tab.id}\n            tab={tab}\n            isActive={tab.id === activeTabId}\n          />\n        ))}\n      </AnimatePresence>\n      \n      {tabs.length === 0 && (\n        <div className=\"flex items-center justify-center h-full text-muted-foreground\">\n          <div className=\"text-center\">\n            <p className=\"text-lg mb-2\">No projects open</p>\n            <p className=\"text-sm mb-4\">Click to start a new project</p>\n            <Button\n              onClick={() => createProjectsTab()}\n              size=\"default\"\n            >\n              <Plus className=\"w-4 h-4 mr-2\" />\n              New Project\n            </Button>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default TabContent;\n"
  },
  {
    "path": "src/components/TabManager.tsx",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { motion, AnimatePresence, Reorder } from 'framer-motion';\nimport { X, Plus, MessageSquare, Bot, AlertCircle, Loader2, Folder, BarChart, Server, Settings, FileText } from 'lucide-react';\nimport { useTabState } from '@/hooks/useTabState';\nimport { Tab, useTabContext } from '@/contexts/TabContext';\nimport { cn } from '@/lib/utils';\nimport { useTrackEvent } from '@/hooks';\n\ninterface TabItemProps {\n  tab: Tab;\n  isActive: boolean;\n  onClose: (id: string) => void;\n  onClick: (id: string) => void;\n  isDragging?: boolean;\n  setDraggedTabId?: (id: string | null) => void;\n}\n\nconst TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDragging = false, setDraggedTabId }) => {\n  const [isHovered, setIsHovered] = useState(false);\n  \n  const getIcon = () => {\n    switch (tab.type) {\n      case 'chat':\n        return MessageSquare;\n      case 'agent':\n      case 'agents':\n        return Bot;\n      case 'projects':\n        return Folder;\n      case 'usage':\n        return BarChart;\n      case 'mcp':\n        return Server;\n      case 'settings':\n        return Settings;\n      case 'claude-md':\n      case 'claude-file':\n        return FileText;\n      case 'agent-execution':\n      case 'create-agent':\n      case 'import-agent':\n        return Bot;\n      default:\n        return MessageSquare;\n    }\n  };\n\n  const getStatusIcon = () => {\n    switch (tab.status) {\n      case 'running':\n        return <Loader2 className=\"w-3 h-3 animate-spin\" />;\n      case 'error':\n        return <AlertCircle className=\"w-3 h-3 text-red-500\" />;\n      default:\n        return null;\n    }\n  };\n\n  const Icon = getIcon();\n  const statusIcon = getStatusIcon();\n\n  return (\n    <Reorder.Item\n      value={tab}\n      id={tab.id}\n      dragListener={true}\n      transition={{ duration: 0.1 }} // Snappy reorder animation\n      className={cn(\n        \"relative flex items-center gap-2 text-sm cursor-pointer select-none group\",\n        \"transition-colors duration-100 overflow-hidden border-r border-border/20\",\n        \"before:absolute before:bottom-0 before:left-0 before:right-0 before:h-0.5 before:transition-colors before:duration-100\",\n        isActive\n          ? \"bg-card text-card-foreground before:bg-primary\"\n          : \"bg-transparent text-muted-foreground hover:bg-muted/40 hover:text-foreground before:bg-transparent\",\n        isDragging && \"bg-card border-primary/50 shadow-sm z-50\",\n        \"min-w-[120px] max-w-[220px] h-8 px-3\"\n      )}\n      onMouseEnter={() => setIsHovered(true)}\n      onMouseLeave={() => setIsHovered(false)}\n      onClick={() => onClick(tab.id)}\n      onDragStart={() => setDraggedTabId?.(tab.id)}\n      onDragEnd={() => setDraggedTabId?.(null)}\n    >\n      {/* Tab Icon */}\n      <div className=\"flex-shrink-0\">\n        <Icon className=\"w-4 h-4\" />\n      </div>\n      \n      {/* Tab Title */}\n      <span className=\"flex-1 truncate text-xs font-medium min-w-0\">\n        {tab.title}\n      </span>\n\n      {/* Status Indicators - always takes up space */}\n      <div className=\"flex items-center gap-1.5 flex-shrink-0 w-6 justify-end\">\n        {statusIcon && (\n          <span className=\"flex items-center justify-center\">\n            {statusIcon}\n          </span>\n        )}\n\n        {tab.hasUnsavedChanges && !statusIcon && (\n          <span \n            className=\"w-1.5 h-1.5 bg-primary rounded-full\"\n            title=\"Unsaved changes\"\n          />\n        )}\n      </div>\n\n      {/* Close Button - Always reserves space */}\n      <button\n        onClick={(e) => {\n          e.stopPropagation();\n          onClose(tab.id);\n        }}\n        className={cn(\n          \"flex-shrink-0 w-4 h-4 flex items-center justify-center rounded-sm\",\n          \"transition-all duration-100 hover:bg-destructive/20 hover:text-destructive\",\n          \"focus:outline-none focus:ring-1 focus:ring-destructive/50\",\n          (isHovered || isActive) ? \"opacity-100\" : \"opacity-0\"\n        )}\n        title={`Close ${tab.title}`}\n        tabIndex={-1}\n      >\n        <X className=\"w-3 h-3\" />\n      </button>\n\n    </Reorder.Item>\n  );\n};\n\ninterface TabManagerProps {\n  className?: string;\n}\n\nexport const TabManager: React.FC<TabManagerProps> = ({ className }) => {\n  const {\n    tabs,\n    activeTabId,\n    createChatTab,\n    createProjectsTab,\n    closeTab,\n    switchToTab,\n    canAddTab\n  } = useTabState();\n\n  // Access reorderTabs from context\n  const { reorderTabs } = useTabContext();\n\n  const scrollContainerRef = useRef<HTMLDivElement>(null);\n  const [showLeftScroll, setShowLeftScroll] = useState(false);\n  const [showRightScroll, setShowRightScroll] = useState(false);\n  const [draggedTabId, setDraggedTabId] = useState<string | null>(null);\n  \n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n\n  // Listen for tab switch events\n  useEffect(() => {\n    const handleSwitchToTab = (event: CustomEvent) => {\n      const { tabId } = event.detail;\n      switchToTab(tabId);\n    };\n\n    window.addEventListener('switch-to-tab', handleSwitchToTab as EventListener);\n    return () => {\n      window.removeEventListener('switch-to-tab', handleSwitchToTab as EventListener);\n    };\n  }, [switchToTab]);\n\n  // Listen for keyboard shortcut events\n  useEffect(() => {\n    const handleCreateTab = () => {\n      createProjectsTab();\n      trackEvent.tabCreated('projects');\n    };\n\n    const handleCloseTab = async () => {\n      if (activeTabId) {\n        const tab = tabs.find(t => t.id === activeTabId);\n        if (tab) {\n          trackEvent.tabClosed(tab.type);\n        }\n        await closeTab(activeTabId);\n      }\n    };\n\n    const handleNextTab = () => {\n      const currentIndex = tabs.findIndex(tab => tab.id === activeTabId);\n      const nextIndex = (currentIndex + 1) % tabs.length;\n      if (tabs[nextIndex]) {\n        switchToTab(tabs[nextIndex].id);\n      }\n    };\n\n    const handlePreviousTab = () => {\n      const currentIndex = tabs.findIndex(tab => tab.id === activeTabId);\n      const previousIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;\n      if (tabs[previousIndex]) {\n        switchToTab(tabs[previousIndex].id);\n      }\n    };\n\n    const handleTabByIndex = (event: CustomEvent) => {\n      const { index } = event.detail;\n      if (tabs[index]) {\n        switchToTab(tabs[index].id);\n      }\n    };\n\n    window.addEventListener('create-chat-tab', handleCreateTab);\n    window.addEventListener('close-current-tab', handleCloseTab);\n    window.addEventListener('switch-to-next-tab', handleNextTab);\n    window.addEventListener('switch-to-previous-tab', handlePreviousTab);\n    window.addEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);\n\n    return () => {\n      window.removeEventListener('create-chat-tab', handleCreateTab);\n      window.removeEventListener('close-current-tab', handleCloseTab);\n      window.removeEventListener('switch-to-next-tab', handleNextTab);\n      window.removeEventListener('switch-to-previous-tab', handlePreviousTab);\n      window.removeEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);\n    };\n  }, [tabs, activeTabId, createChatTab, closeTab, switchToTab]);\n\n  // Check scroll buttons visibility\n  const checkScrollButtons = () => {\n    const container = scrollContainerRef.current;\n    if (!container) return;\n\n    const { scrollLeft, scrollWidth, clientWidth } = container;\n    setShowLeftScroll(scrollLeft > 0);\n    setShowRightScroll(scrollLeft + clientWidth < scrollWidth - 1);\n  };\n\n  useEffect(() => {\n    checkScrollButtons();\n    const container = scrollContainerRef.current;\n    if (!container) return;\n\n    container.addEventListener('scroll', checkScrollButtons);\n    window.addEventListener('resize', checkScrollButtons);\n\n    return () => {\n      container.removeEventListener('scroll', checkScrollButtons);\n      window.removeEventListener('resize', checkScrollButtons);\n    };\n  }, [tabs]);\n\n  const handleReorder = (newOrder: Tab[]) => {\n    // Find the positions that changed\n    const oldOrder = tabs.map(tab => tab.id);\n    const newOrderIds = newOrder.map(tab => tab.id);\n    \n    // Find what moved\n    const movedTabId = newOrderIds.find((id, index) => oldOrder[index] !== id);\n    if (!movedTabId) return;\n    \n    const oldIndex = oldOrder.indexOf(movedTabId);\n    const newIndex = newOrderIds.indexOf(movedTabId);\n    \n    if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {\n      // Use the context's reorderTabs function\n      reorderTabs(oldIndex, newIndex);\n      // Track the reorder event\n      trackEvent.featureUsed?.('tab_reorder', 'drag_drop', { \n        from_index: oldIndex, \n        to_index: newIndex \n      });\n    }\n  };\n\n  const handleCloseTab = async (id: string) => {\n    const tab = tabs.find(t => t.id === id);\n    if (tab) {\n      trackEvent.tabClosed(tab.type);\n    }\n    await closeTab(id);\n  };\n\n  const handleNewTab = () => {\n    if (canAddTab()) {\n      createProjectsTab();\n      trackEvent.tabCreated('projects');\n    }\n  };\n\n  const scrollTabs = (direction: 'left' | 'right') => {\n    const container = scrollContainerRef.current;\n    if (!container) return;\n\n    const scrollAmount = 200;\n    const newScrollLeft = direction === 'left'\n      ? container.scrollLeft - scrollAmount\n      : container.scrollLeft + scrollAmount;\n\n    container.scrollTo({\n      left: newScrollLeft,\n      behavior: 'smooth'\n    });\n  };\n\n  return (\n    <div className={cn(\"flex items-stretch bg-muted/15 relative border-b border-border/50\", className)}>\n      {/* Left fade gradient */}\n      {showLeftScroll && (\n        <div className=\"absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-muted/15 to-transparent pointer-events-none z-10\" />\n      )}\n      \n      {/* Left scroll button */}\n      <AnimatePresence>\n        {showLeftScroll && (\n          <motion.button\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            onClick={() => scrollTabs('left')}\n            className={cn(\n              \"p-1.5 hover:bg-muted/80 rounded-sm z-20 ml-1\",\n              \"transition-colors duration-200 flex items-center justify-center\",\n              \"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50\"\n            )}\n            title=\"Scroll tabs left\"\n          >\n            <svg className=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\">\n              <path d=\"M15 18l-6-6 6-6\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n          </motion.button>\n        )}\n      </AnimatePresence>\n\n      {/* Tabs container */}\n      <div\n        ref={scrollContainerRef}\n        className=\"flex-1 flex overflow-x-auto scrollbar-hide\"\n        style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}\n      >\n        <div className=\"flex items-stretch h-8\">\n          <Reorder.Group\n            axis=\"x\"\n            values={tabs}\n            onReorder={handleReorder}\n            className=\"flex items-stretch\"\n            layoutScroll={false}\n          >\n            {tabs.map((tab) => (\n              <TabItem\n                key={tab.id}\n                tab={tab}\n                isActive={tab.id === activeTabId}\n                onClose={handleCloseTab}\n                onClick={switchToTab}\n                isDragging={draggedTabId === tab.id}\n                setDraggedTabId={setDraggedTabId}\n              />\n            ))}\n          </Reorder.Group>\n          \n          {/* New tab button - positioned right after tabs */}\n          <motion.button\n            onClick={handleNewTab}\n            disabled={!canAddTab()}\n            whileTap={canAddTab() ? { scale: 0.97 } : {}}\n            transition={{ duration: 0.15 }}\n            className={cn(\n              \"px-2 mx-1 rounded-md flex items-center justify-center flex-shrink-0\",\n              \"bg-background/50 backdrop-blur-sm h-8\",\n              canAddTab()\n                ? \"hover:bg-muted/60 text-muted-foreground hover:text-foreground\"\n                : \"opacity-50 cursor-not-allowed text-muted-foreground\"\n            )}\n            title={canAddTab() ? \"New project (Ctrl+T)\" : \"Maximum tabs reached\"}\n          >\n            <Plus className=\"w-4 h-4\" />\n          </motion.button>\n        </div>\n      </div>\n\n      {/* Right fade gradient */}\n      {showRightScroll && (\n        <div className=\"absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-muted/15 to-transparent pointer-events-none z-10\" />\n      )}\n\n      {/* Right scroll button */}\n      <AnimatePresence>\n        {showRightScroll && (\n          <motion.button\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            onClick={() => scrollTabs('right')}\n            className={cn(\n              \"p-1.5 hover:bg-muted/80 rounded-sm z-20 mr-1\",\n              \"transition-colors duration-200 flex items-center justify-center\",\n              \"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50\"\n            )}\n            title=\"Scroll tabs right\"\n          >\n            <svg className=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\">\n              <path d=\"M9 18l6-6-6-6\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n          </motion.button>\n        )}\n      </AnimatePresence>\n\n    </div>\n  );\n};\n\nexport default TabManager;"
  },
  {
    "path": "src/components/TimelineNavigator.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { \n  GitBranch, \n  Save, \n  RotateCcw, \n  GitFork,\n  AlertCircle,\n  ChevronDown,\n  ChevronRight,\n  Hash,\n  FileCode,\n  Diff\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { api, type Checkpoint, type TimelineNode, type SessionTimeline, type CheckpointDiff } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { useTrackEvent } from \"@/hooks\";\n\ninterface TimelineNavigatorProps {\n  sessionId: string;\n  projectId: string;\n  projectPath: string;\n  currentMessageIndex: number;\n  onCheckpointSelect: (checkpoint: Checkpoint) => void;\n  onFork: (checkpointId: string) => void;\n  /**\n   * Incrementing value provided by parent to force timeline reload when checkpoints\n   * are created elsewhere (e.g., auto-checkpoint after tool execution).\n   */\n  refreshVersion?: number;\n  /**\n   * Callback when a new checkpoint is created\n   */\n  onCheckpointCreated?: () => void;\n  className?: string;\n}\n\n/**\n * Visual timeline navigator for checkpoint management\n */\nexport const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({\n  sessionId,\n  projectId,\n  projectPath,\n  currentMessageIndex,\n  onCheckpointSelect,\n  onFork,\n  refreshVersion = 0,\n  onCheckpointCreated,\n  className\n}) => {\n  const [timeline, setTimeline] = useState<SessionTimeline | null>(null);\n  const [selectedCheckpoint, setSelectedCheckpoint] = useState<Checkpoint | null>(null);\n  const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());\n  const [showCreateDialog, setShowCreateDialog] = useState(false);\n  const [showDiffDialog, setShowDiffDialog] = useState(false);\n  const [checkpointDescription, setCheckpointDescription] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [diff, setDiff] = useState<CheckpointDiff | null>(null);\n  const [compareCheckpoint, setCompareCheckpoint] = useState<Checkpoint | null>(null);\n\n  // Analytics tracking\n  const trackEvent = useTrackEvent();\n\n  // IME composition state\n  const isIMEComposingRef = React.useRef(false);\n\n  // Load timeline on mount and whenever refreshVersion bumps\n  useEffect(() => {\n    loadTimeline();\n  }, [sessionId, projectId, projectPath, refreshVersion]);\n\n  const loadTimeline = async () => {\n    try {\n      setIsLoading(true);\n      setError(null);\n      const timelineData = await api.getSessionTimeline(sessionId, projectId, projectPath);\n      setTimeline(timelineData);\n      \n      // Auto-expand nodes with current checkpoint\n      if (timelineData.currentCheckpointId && timelineData.rootNode) {\n        const pathToNode = findPathToCheckpoint(timelineData.rootNode, timelineData.currentCheckpointId);\n        setExpandedNodes(new Set(pathToNode));\n      }\n    } catch (err) {\n      console.error(\"Failed to load timeline:\", err);\n      setError(\"Failed to load timeline\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const findPathToCheckpoint = (node: TimelineNode, checkpointId: string, path: string[] = []): string[] => {\n    if (node.checkpoint.id === checkpointId) {\n      return path;\n    }\n    \n    for (const child of node.children) {\n      const childPath = findPathToCheckpoint(child, checkpointId, [...path, node.checkpoint.id]);\n      if (childPath.length > path.length) {\n        return childPath;\n      }\n    }\n    \n    return path;\n  };\n\n  const handleCreateCheckpoint = async () => {\n    try {\n      setIsLoading(true);\n      setError(null);\n      \n      const sessionStartTime = Date.now(); // Using current time as we don't have session start time\n      \n      await api.createCheckpoint(\n        sessionId,\n        projectId,\n        projectPath,\n        currentMessageIndex,\n        checkpointDescription || undefined\n      );\n      \n      // Track checkpoint creation\n      const checkpointNumber = timeline ? timeline.totalCheckpoints + 1 : 1;\n      trackEvent.checkpointCreated({\n        checkpoint_number: checkpointNumber,\n        session_duration_at_checkpoint: Date.now() - sessionStartTime\n      });\n      \n      // Call parent callback if provided\n      if (onCheckpointCreated) {\n        onCheckpointCreated();\n      }\n      \n      setCheckpointDescription(\"\");\n      setShowCreateDialog(false);\n      await loadTimeline();\n    } catch (err) {\n      console.error(\"Failed to create checkpoint:\", err);\n      setError(\"Failed to create checkpoint\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleRestoreCheckpoint = async (checkpoint: Checkpoint) => {\n    if (!confirm(`Restore to checkpoint \"${checkpoint.description || checkpoint.id.slice(0, 8)}\"? Current state will be saved as a new checkpoint.`)) {\n      return;\n    }\n\n    try {\n      setIsLoading(true);\n      setError(null);\n      \n      const checkpointTime = new Date(checkpoint.timestamp).getTime();\n      const timeSinceCheckpoint = Date.now() - checkpointTime;\n      \n      // First create a checkpoint of current state\n      await api.createCheckpoint(\n        sessionId,\n        projectId,\n        projectPath,\n        currentMessageIndex,\n        \"Auto-save before restore\"\n      );\n      \n      // Then restore\n      await api.restoreCheckpoint(checkpoint.id, sessionId, projectId, projectPath);\n      \n      // Track checkpoint restoration\n      trackEvent.checkpointRestored({\n        checkpoint_id: checkpoint.id,\n        time_since_checkpoint_ms: timeSinceCheckpoint\n      });\n      \n      await loadTimeline();\n      onCheckpointSelect(checkpoint);\n    } catch (err) {\n      console.error(\"Failed to restore checkpoint:\", err);\n      setError(\"Failed to restore checkpoint\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleFork = async (checkpoint: Checkpoint) => {\n    onFork(checkpoint.id);\n  };\n\n  const handleCompositionStart = () => {\n    isIMEComposingRef.current = true;\n  };\n\n  const handleCompositionEnd = () => {\n    setTimeout(() => {\n      isIMEComposingRef.current = false;\n    }, 0);\n  };\n\n  const handleCompare = async (checkpoint: Checkpoint) => {\n    if (!selectedCheckpoint) {\n      setSelectedCheckpoint(checkpoint);\n      return;\n    }\n\n    try {\n      setIsLoading(true);\n      setError(null);\n      \n      const diffData = await api.getCheckpointDiff(\n        selectedCheckpoint.id,\n        checkpoint.id,\n        sessionId,\n        projectId\n      );\n      \n      setDiff(diffData);\n      setCompareCheckpoint(checkpoint);\n      setShowDiffDialog(true);\n    } catch (err) {\n      console.error(\"Failed to get diff:\", err);\n      setError(\"Failed to compare checkpoints\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const toggleNodeExpansion = (nodeId: string) => {\n    const newExpanded = new Set(expandedNodes);\n    if (newExpanded.has(nodeId)) {\n      newExpanded.delete(nodeId);\n    } else {\n      newExpanded.add(nodeId);\n    }\n    setExpandedNodes(newExpanded);\n  };\n\n  const renderTimelineNode = (node: TimelineNode, depth: number = 0) => {\n    const isExpanded = expandedNodes.has(node.checkpoint.id);\n    const hasChildren = node.children.length > 0;\n    const isCurrent = timeline?.currentCheckpointId === node.checkpoint.id;\n    const isSelected = selectedCheckpoint?.id === node.checkpoint.id;\n\n    return (\n      <div key={node.checkpoint.id} className=\"relative\">\n        {/* Connection line */}\n        {depth > 0 && (\n          <div \n            className=\"absolute left-0 top-0 w-6 h-6 border-l-2 border-b-2 border-muted-foreground/30\"\n            style={{ \n              left: `${(depth - 1) * 24}px`,\n              borderBottomLeftRadius: '8px'\n            }}\n          />\n        )}\n        \n        {/* Node content */}\n        <motion.div\n          initial={{ opacity: 0, x: -20 }}\n          animate={{ opacity: 1, x: 0 }}\n          transition={{ duration: 0.2, delay: depth * 0.05 }}\n          className={cn(\n            \"flex items-start gap-2 py-2\",\n            depth > 0 && \"ml-6\"\n          )}\n          style={{ paddingLeft: `${depth * 24}px` }}\n        >\n          {/* Expand/collapse button */}\n          {hasChildren && (\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"h-6 w-6 -ml-1\"\n              onClick={() => toggleNodeExpansion(node.checkpoint.id)}\n            >\n              {isExpanded ? (\n                <ChevronDown className=\"h-3 w-3\" />\n              ) : (\n                <ChevronRight className=\"h-3 w-3\" />\n              )}\n            </Button>\n          )}\n          \n          {/* Checkpoint card */}\n          <Card \n            className={cn(\n              \"flex-1 cursor-pointer transition-all hover:shadow-md\",\n              isCurrent && \"border-primary ring-2 ring-primary/20\",\n              isSelected && \"border-blue-500 bg-blue-500/5\",\n              !hasChildren && \"ml-5\"\n            )}\n            onClick={() => setSelectedCheckpoint(node.checkpoint)}\n          >\n            <CardContent className=\"p-3\">\n              <div className=\"flex items-start justify-between gap-2\">\n                <div className=\"flex-1 min-w-0\">\n                  <div className=\"flex items-center gap-2 mb-1\">\n                    {isCurrent && (\n                      <Badge variant=\"default\" className=\"text-xs\">Current</Badge>\n                    )}\n                    <span className=\"text-xs font-mono text-muted-foreground\">\n                      {node.checkpoint.id.slice(0, 8)}\n                    </span>\n                    <span className=\"text-xs text-muted-foreground\">\n                      {formatDistanceToNow(new Date(node.checkpoint.timestamp), { addSuffix: true })}\n                    </span>\n                  </div>\n                  \n                  {node.checkpoint.description && (\n                    <p className=\"text-sm font-medium mb-1\">{node.checkpoint.description}</p>\n                  )}\n                  \n                  <p className=\"text-xs text-muted-foreground line-clamp-2\">\n                    {node.checkpoint.metadata.userPrompt || \"No prompt\"}\n                  </p>\n                  \n                  <div className=\"flex items-center gap-3 mt-2 text-xs text-muted-foreground\">\n                    <span className=\"flex items-center gap-1\">\n                      <Hash className=\"h-3 w-3\" />\n                      {node.checkpoint.metadata.totalTokens.toLocaleString()} tokens\n                    </span>\n                    <span className=\"flex items-center gap-1\">\n                      <FileCode className=\"h-3 w-3\" />\n                      {node.checkpoint.metadata.fileChanges} files\n                    </span>\n                  </div>\n                </div>\n                \n                {/* Actions */}\n                <div className=\"flex items-center gap-1\">\n                  <TooltipProvider>\n                    <Tooltip>\n                      <TooltipTrigger asChild>\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          className=\"h-7 w-7\"\n                          onClick={(e) => {\n                            e.stopPropagation();\n                            handleRestoreCheckpoint(node.checkpoint);\n                          }}\n                        >\n                          <RotateCcw className=\"h-3 w-3\" />\n                        </Button>\n                      </TooltipTrigger>\n                      <TooltipContent>Restore to this checkpoint</TooltipContent>\n                    </Tooltip>\n                  </TooltipProvider>\n                  \n                  <TooltipProvider>\n                    <Tooltip>\n                      <TooltipTrigger asChild>\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          className=\"h-7 w-7\"\n                          onClick={(e) => {\n                            e.stopPropagation();\n                            handleFork(node.checkpoint);\n                          }}\n                        >\n                          <GitFork className=\"h-3 w-3\" />\n                        </Button>\n                      </TooltipTrigger>\n                      <TooltipContent>Fork from this checkpoint</TooltipContent>\n                    </Tooltip>\n                  </TooltipProvider>\n                  \n                  <TooltipProvider>\n                    <Tooltip>\n                      <TooltipTrigger asChild>\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          className=\"h-7 w-7\"\n                          onClick={(e) => {\n                            e.stopPropagation();\n                            handleCompare(node.checkpoint);\n                          }}\n                        >\n                          <Diff className=\"h-3 w-3\" />\n                        </Button>\n                      </TooltipTrigger>\n                      <TooltipContent>Compare with another checkpoint</TooltipContent>\n                    </Tooltip>\n                  </TooltipProvider>\n                </div>\n              </div>\n            </CardContent>\n          </Card>\n        </motion.div>\n        \n        {/* Children */}\n        {isExpanded && hasChildren && (\n          <div className=\"relative\">\n            {/* Vertical line for children */}\n            {node.children.length > 1 && (\n              <div \n                className=\"absolute top-0 bottom-0 w-0.5 bg-muted-foreground/30\"\n                style={{ left: `${(depth + 1) * 24 - 1}px` }}\n              />\n            )}\n            \n            {node.children.map((child) => \n              renderTimelineNode(child, depth + 1)\n            )}\n          </div>\n        )}\n      </div>\n    );\n  };\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      {/* Experimental Feature Warning */}\n      <div className=\"rounded-lg border border-yellow-500/50 bg-yellow-500/10 p-3\">\n        <div className=\"flex items-start gap-2\">\n          <AlertCircle className=\"h-4 w-4 text-yellow-600 mt-0.5\" />\n          <div className=\"text-xs\">\n            <p className=\"font-medium text-yellow-600\">Experimental Feature</p>\n            <p className=\"text-yellow-600/80\">\n              Checkpointing may affect directory structure or cause data loss. Use with caution.\n            </p>\n          </div>\n        </div>\n      </div>\n      \n      {/* Header */}\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-2\">\n          <GitBranch className=\"h-5 w-5 text-muted-foreground\" />\n          <h3 className=\"text-sm font-medium\">Timeline</h3>\n          {timeline && (\n            <Badge variant=\"outline\" className=\"text-xs\">\n              {timeline.totalCheckpoints} checkpoints\n            </Badge>\n          )}\n        </div>\n        \n        <Button\n          size=\"sm\"\n          variant=\"default\"\n          onClick={() => setShowCreateDialog(true)}\n          disabled={isLoading}\n        >\n          <Save className=\"h-3 w-3 mr-1\" />\n          Checkpoint\n        </Button>\n      </div>\n      \n      {/* Error display */}\n      {error && (\n        <div className=\"flex items-center gap-2 text-xs text-destructive\">\n          <AlertCircle className=\"h-3 w-3\" />\n          {error}\n        </div>\n      )}\n      \n      {/* Timeline tree */}\n      {timeline?.rootNode ? (\n        <div className=\"relative overflow-x-auto\">\n          {renderTimelineNode(timeline.rootNode)}\n        </div>\n      ) : (\n        <div className=\"text-center py-8 text-sm text-muted-foreground\">\n          {isLoading ? \"Loading timeline...\" : \"No checkpoints yet\"}\n        </div>\n      )}\n      \n      {/* Create checkpoint dialog */}\n      <Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Create Checkpoint</DialogTitle>\n            <DialogDescription>\n              Save the current state of your session with an optional description.\n            </DialogDescription>\n          </DialogHeader>\n          \n          <div className=\"space-y-4 py-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"description\">Description (optional)</Label>\n              <Input\n                id=\"description\"\n                placeholder=\"e.g., Before major refactoring\"\n                value={checkpointDescription}\n                onChange={(e) => setCheckpointDescription(e.target.value)}\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\" && !isLoading) {\n                    if (e.nativeEvent.isComposing || isIMEComposingRef.current) {\n                      return;\n                    }\n                    handleCreateCheckpoint();\n                  }\n                }}\n                onCompositionStart={handleCompositionStart}\n                onCompositionEnd={handleCompositionEnd}\n              />\n            </div>\n          </div>\n          \n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => setShowCreateDialog(false)}\n              disabled={isLoading}\n            >\n              Cancel\n            </Button>\n            <Button\n              onClick={handleCreateCheckpoint}\n              disabled={isLoading}\n            >\n              Create Checkpoint\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n      \n      {/* Diff dialog */}\n      <Dialog open={showDiffDialog} onOpenChange={setShowDiffDialog}>\n        <DialogContent className=\"max-w-3xl\">\n          <DialogHeader>\n            <DialogTitle>Checkpoint Comparison</DialogTitle>\n            <DialogDescription>\n              Changes between \"{selectedCheckpoint?.description || selectedCheckpoint?.id.slice(0, 8)}\" \n              and \"{compareCheckpoint?.description || compareCheckpoint?.id.slice(0, 8)}\"\n            </DialogDescription>\n          </DialogHeader>\n          \n          {diff && (\n            <div className=\"space-y-4 py-4 max-h-[60vh] overflow-y-auto\">\n              {/* Summary */}\n              <div className=\"grid grid-cols-3 gap-4\">\n                <Card>\n                  <CardContent className=\"p-3\">\n                    <div className=\"text-xs text-muted-foreground\">Modified Files</div>\n                    <div className=\"text-2xl font-bold\">{diff.modifiedFiles.length}</div>\n                  </CardContent>\n                </Card>\n                <Card>\n                  <CardContent className=\"p-3\">\n                    <div className=\"text-xs text-muted-foreground\">Added Files</div>\n                    <div className=\"text-2xl font-bold text-green-600\">{diff.addedFiles.length}</div>\n                  </CardContent>\n                </Card>\n                <Card>\n                  <CardContent className=\"p-3\">\n                    <div className=\"text-xs text-muted-foreground\">Deleted Files</div>\n                    <div className=\"text-2xl font-bold text-red-600\">{diff.deletedFiles.length}</div>\n                  </CardContent>\n                </Card>\n              </div>\n              \n              {/* Token delta */}\n              <div className=\"flex items-center justify-center\">\n                <Badge variant={diff.tokenDelta > 0 ? \"default\" : \"secondary\"}>\n                  {diff.tokenDelta > 0 ? \"+\" : \"\"}{diff.tokenDelta.toLocaleString()} tokens\n                </Badge>\n              </div>\n              \n              {/* File lists */}\n              {diff.modifiedFiles.length > 0 && (\n                <div>\n                  <h4 className=\"text-sm font-medium mb-2\">Modified Files</h4>\n                  <div className=\"space-y-1\">\n                    {diff.modifiedFiles.map((file) => (\n                      <div key={file.path} className=\"flex items-center justify-between text-xs\">\n                        <span className=\"font-mono\">{file.path}</span>\n                        <div className=\"flex items-center gap-2 text-xs\">\n                          <span className=\"text-green-600\">+{file.additions}</span>\n                          <span className=\"text-red-600\">-{file.deletions}</span>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n              \n              {diff.addedFiles.length > 0 && (\n                <div>\n                  <h4 className=\"text-sm font-medium mb-2\">Added Files</h4>\n                  <div className=\"space-y-1\">\n                    {diff.addedFiles.map((file) => (\n                      <div key={file} className=\"text-xs font-mono text-green-600\">\n                        + {file}\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n              \n              {diff.deletedFiles.length > 0 && (\n                <div>\n                  <h4 className=\"text-sm font-medium mb-2\">Deleted Files</h4>\n                  <div className=\"space-y-1\">\n                    {diff.deletedFiles.map((file) => (\n                      <div key={file} className=\"text-xs font-mono text-red-600\">\n                        - {file}\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n            </div>\n          )}\n          \n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                setShowDiffDialog(false);\n                setDiff(null);\n                setCompareCheckpoint(null);\n              }}\n            >\n              Close\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}; \n"
  },
  {
    "path": "src/components/TokenCounter.tsx",
    "content": "import React from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Hash } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface TokenCounterProps {\n  /**\n   * Total number of tokens\n   */\n  tokens: number;\n  /**\n   * Whether to show the counter\n   */\n  show?: boolean;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * TokenCounter component - Displays a floating token count\n * \n * @example\n * <TokenCounter tokens={1234} show={true} />\n */\nexport const TokenCounter: React.FC<TokenCounterProps> = ({\n  tokens,\n  show = true,\n  className,\n}) => {\n  if (!show || tokens === 0) return null;\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, scale: 0.8 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={{ opacity: 0, scale: 0.8 }}\n      className={cn(\n        \"fixed bottom-20 right-4 z-30\",\n        \"bg-background/90 backdrop-blur-sm\",\n        \"border border-border rounded-full\",\n        \"px-3 py-1.5 shadow-lg\",\n        className\n      )}\n    >\n      <div className=\"flex items-center gap-1.5 text-xs\">\n        <Hash className=\"h-3 w-3 text-muted-foreground\" />\n        <span className=\"font-mono\">{tokens.toLocaleString()}</span>\n        <span className=\"text-muted-foreground\">tokens</span>\n      </div>\n    </motion.div>\n  );\n}; "
  },
  {
    "path": "src/components/ToolWidgets.new.tsx",
    "content": "// This file re-exports all widgets from the widgets directory\n// It maintains backward compatibility with the original ToolWidgets.tsx\n\nexport * from './widgets';"
  },
  {
    "path": "src/components/ToolWidgets.tsx",
    "content": "import React, { useState } from \"react\";\nimport { \n  CheckCircle2, \n  Circle, \n  Clock,\n  FolderOpen,\n  FileText,\n  Search,\n  Terminal,\n  FileEdit,\n  Code,\n  ChevronRight,\n  Maximize2,\n  GitBranch,\n  X,\n  Info,\n  AlertCircle,\n  Settings,\n  Fingerprint,\n  Cpu,\n  FolderSearch,\n  List,\n  LogOut,\n  Edit3,\n  FilePlus,\n  Book,\n  BookOpen,\n  Globe,\n  ListChecks,\n  ListPlus,\n  Globe2,\n  Package,\n  ChevronDown,\n  Package2,\n  Wrench,\n  CheckSquare,\n  type LucideIcon,\n  Sparkles,\n  Bot,\n  Zap,\n  FileCode,\n  Folder,\n  ChevronUp,\n  BarChart3,\n  Download,\n  LayoutGrid,\n  LayoutList,\n  Activity,\n  Hash,\n} from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { getClaudeSyntaxTheme } from \"@/lib/claudeSyntaxTheme\";\nimport { useTheme } from \"@/hooks\";\nimport { Button } from \"@/components/ui/button\";\nimport { createPortal } from \"react-dom\";\nimport * as Diff from 'diff';\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { detectLinks, makeLinksClickable } from \"@/lib/linkDetector\";\nimport ReactMarkdown from \"react-markdown\";\nimport { open } from \"@tauri-apps/plugin-shell\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Input } from \"@/components/ui/input\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\n/**\n * Widget for TodoWrite tool - displays a beautiful TODO list\n */\nexport const TodoWidget: React.FC<{ todos: any[]; result?: any }> = ({ todos, result: _result }) => {\n  const statusIcons = {\n    completed: <CheckCircle2 className=\"h-4 w-4 text-green-500\" />,\n    in_progress: <Clock className=\"h-4 w-4 text-blue-500 animate-pulse\" />,\n    pending: <Circle className=\"h-4 w-4 text-muted-foreground\" />\n  };\n\n  const priorityColors = {\n    high: \"bg-red-500/10 text-red-500 border-red-500/20\",\n    medium: \"bg-yellow-500/10 text-yellow-500 border-yellow-500/20\",\n    low: \"bg-green-500/10 text-green-500 border-green-500/20\"\n  };\n\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 mb-3\">\n        <FileEdit className=\"h-4 w-4 text-primary\" />\n        <span className=\"text-sm font-medium\">Todo List</span>\n      </div>\n      <div className=\"space-y-2\">\n        {todos.map((todo, idx) => (\n          <div\n            key={todo.id || idx}\n            className={cn(\n              \"flex items-start gap-3 p-3 rounded-lg border bg-card/50\",\n              todo.status === \"completed\" && \"opacity-60\"\n            )}\n          >\n            <div className=\"mt-0.5\">\n              {statusIcons[todo.status as keyof typeof statusIcons] || statusIcons.pending}\n            </div>\n            <div className=\"flex-1 space-y-1\">\n              <p className={cn(\n                \"text-sm\",\n                todo.status === \"completed\" && \"line-through\"\n              )}>\n                {todo.content}\n              </p>\n              {todo.priority && (\n                <Badge \n                  variant=\"outline\" \n                  className={cn(\"text-xs\", priorityColors[todo.priority as keyof typeof priorityColors])}\n                >\n                  {todo.priority}\n                </Badge>\n              )}\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for LS (List Directory) tool\n */\nexport const LSWidget: React.FC<{ path: string; result?: any }> = ({ path, result }) => {\n  // If we have a result, show it using the LSResultWidget\n  if (result) {\n    let resultContent = '';\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n    \n    return (\n      <div className=\"space-y-2\">\n        <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n          <FolderOpen className=\"h-4 w-4 text-primary\" />\n          <span className=\"text-sm\">Directory contents for:</span>\n          <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded\">\n            {path}\n          </code>\n        </div>\n        {resultContent && <LSResultWidget content={resultContent} />}\n      </div>\n    );\n  }\n  \n  return (\n    <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n      <FolderOpen className=\"h-4 w-4 text-primary\" />\n      <span className=\"text-sm\">Listing directory:</span>\n      <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded\">\n        {path}\n      </code>\n      {!result && (\n        <div className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n          <div className=\"h-2 w-2 bg-blue-500 rounded-full animate-pulse\" />\n          <span>Loading...</span>\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**\n * Widget for LS tool result - displays directory tree structure\n */\nexport const LSResultWidget: React.FC<{ content: string }> = ({ content }) => {\n  const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());\n  \n  // Parse the directory tree structure\n  const parseDirectoryTree = (rawContent: string) => {\n    const lines = rawContent.split('\\n');\n    const entries: Array<{\n      path: string;\n      name: string;\n      type: 'file' | 'directory';\n      level: number;\n    }> = [];\n    \n    let currentPath: string[] = [];\n    \n    for (const line of lines) {\n      // Skip NOTE section and everything after it\n      if (line.startsWith('NOTE:')) {\n        break;\n      }\n      \n      // Skip empty lines\n      if (!line.trim()) continue;\n      \n      // Calculate indentation level\n      const indent = line.match(/^(\\s*)/)?.[1] || '';\n      const level = Math.floor(indent.length / 2);\n      \n      // Extract the entry name\n      const entryMatch = line.match(/^\\s*-\\s+(.+?)(\\/$)?$/);\n      if (!entryMatch) continue;\n      \n      const fullName = entryMatch[1];\n      const isDirectory = line.trim().endsWith('/');\n      const name = isDirectory ? fullName : fullName;\n      \n      // Update current path based on level\n      currentPath = currentPath.slice(0, level);\n      currentPath.push(name);\n      \n      entries.push({\n        path: currentPath.join('/'),\n        name,\n        type: isDirectory ? 'directory' : 'file',\n        level,\n      });\n    }\n    \n    return entries;\n  };\n  \n  const entries = parseDirectoryTree(content);\n  \n  const toggleDirectory = (path: string) => {\n    setExpandedDirs(prev => {\n      const next = new Set(prev);\n      if (next.has(path)) {\n        next.delete(path);\n      } else {\n        next.add(path);\n      }\n      return next;\n    });\n  };\n  \n  // Group entries by parent for collapsible display\n  const getChildren = (parentPath: string, parentLevel: number) => {\n    return entries.filter(e => {\n      if (e.level !== parentLevel + 1) return false;\n      const parentParts = parentPath.split('/').filter(Boolean);\n      const entryParts = e.path.split('/').filter(Boolean);\n      \n      // Check if this entry is a direct child of the parent\n      if (entryParts.length !== parentParts.length + 1) return false;\n      \n      // Check if all parent parts match\n      for (let i = 0; i < parentParts.length; i++) {\n        if (parentParts[i] !== entryParts[i]) return false;\n      }\n      \n      return true;\n    });\n  };\n  \n  const renderEntry = (entry: typeof entries[0], isRoot = false) => {\n    const hasChildren = entry.type === 'directory' && \n      entries.some(e => e.path.startsWith(entry.path + '/') && e.level === entry.level + 1);\n    const isExpanded = expandedDirs.has(entry.path) || isRoot;\n    \n    const getIcon = () => {\n      if (entry.type === 'directory') {\n        return isExpanded ? \n          <FolderOpen className=\"h-3.5 w-3.5 text-blue-500\" /> : \n          <Folder className=\"h-3.5 w-3.5 text-blue-500\" />;\n      }\n      \n      // File type icons based on extension\n      const ext = entry.name.split('.').pop()?.toLowerCase();\n      switch (ext) {\n        case 'rs':\n          return <FileCode className=\"h-3.5 w-3.5 text-orange-500\" />;\n        case 'toml':\n        case 'yaml':\n        case 'yml':\n        case 'json':\n          return <FileText className=\"h-3.5 w-3.5 text-yellow-500\" />;\n        case 'md':\n          return <FileText className=\"h-3.5 w-3.5 text-blue-400\" />;\n        case 'js':\n        case 'jsx':\n        case 'ts':\n        case 'tsx':\n          return <FileCode className=\"h-3.5 w-3.5 text-yellow-400\" />;\n        case 'py':\n          return <FileCode className=\"h-3.5 w-3.5 text-blue-500\" />;\n        case 'go':\n          return <FileCode className=\"h-3.5 w-3.5 text-cyan-500\" />;\n        case 'sh':\n        case 'bash':\n          return <Terminal className=\"h-3.5 w-3.5 text-green-500\" />;\n        default:\n          return <FileText className=\"h-3.5 w-3.5 text-muted-foreground\" />;\n      }\n    };\n    \n    return (\n      <div key={entry.path}>\n        <div \n          className={cn(\n            \"flex items-center gap-2 py-1 px-2 rounded hover:bg-muted/50 transition-colors cursor-pointer\",\n            !isRoot && \"ml-4\"\n          )}\n          onClick={() => entry.type === 'directory' && hasChildren && toggleDirectory(entry.path)}\n        >\n          {entry.type === 'directory' && hasChildren && (\n            <ChevronRight className={cn(\n              \"h-3 w-3 text-muted-foreground transition-transform\",\n              isExpanded && \"rotate-90\"\n            )} />\n          )}\n          {(!hasChildren || entry.type !== 'directory') && (\n            <div className=\"w-3\" />\n          )}\n          {getIcon()}\n          <span className=\"text-sm font-mono\">{entry.name}</span>\n        </div>\n        \n        {entry.type === 'directory' && hasChildren && isExpanded && (\n          <div className=\"ml-2\">\n            {getChildren(entry.path, entry.level).map(child => renderEntry(child))}\n          </div>\n        )}\n      </div>\n    );\n  };\n  \n  // Get root entries\n  const rootEntries = entries.filter(e => e.level === 0);\n  \n  return (\n    <div className=\"rounded-lg border bg-muted/20 p-3\">\n      <div className=\"space-y-1\">\n        {rootEntries.map(entry => renderEntry(entry, true))}\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for Read tool\n */\nexport const ReadWidget: React.FC<{ filePath: string; result?: any }> = ({ filePath, result }) => {\n  // If we have a result, show it using the ReadResultWidget\n  if (result) {\n    let resultContent = '';\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n    \n    return (\n      <div className=\"space-y-2\">\n        <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n          <FileText className=\"h-4 w-4 text-primary\" />\n          <span className=\"text-sm\">File content:</span>\n          <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded flex-1 truncate\">\n            {filePath}\n          </code>\n        </div>\n        {resultContent && <ReadResultWidget content={resultContent} filePath={filePath} />}\n      </div>\n    );\n  }\n  \n  return (\n    <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n      <FileText className=\"h-4 w-4 text-primary\" />\n      <span className=\"text-sm\">Reading file:</span>\n      <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded flex-1 truncate\">\n        {filePath}\n      </code>\n      {!result && (\n        <div className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n          <div className=\"h-2 w-2 bg-blue-500 rounded-full animate-pulse\" />\n          <span>Loading...</span>\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**\n * Widget for Read tool result - shows file content with line numbers\n */\nexport const ReadResultWidget: React.FC<{ content: string; filePath?: string }> = ({ content, filePath }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n  const { theme } = useTheme();\n  const syntaxTheme = getClaudeSyntaxTheme(theme);\n  \n  // Extract file extension for syntax highlighting\n  const getLanguage = (path?: string) => {\n    if (!path) return \"text\";\n    const ext = path.split('.').pop()?.toLowerCase();\n    const languageMap: Record<string, string> = {\n      ts: \"typescript\",\n      tsx: \"tsx\",\n      js: \"javascript\",\n      jsx: \"jsx\",\n      py: \"python\",\n      rs: \"rust\",\n      go: \"go\",\n      java: \"java\",\n      cpp: \"cpp\",\n      c: \"c\",\n      cs: \"csharp\",\n      php: \"php\",\n      rb: \"ruby\",\n      swift: \"swift\",\n      kt: \"kotlin\",\n      scala: \"scala\",\n      sh: \"bash\",\n      bash: \"bash\",\n      zsh: \"bash\",\n      yaml: \"yaml\",\n      yml: \"yaml\",\n      json: \"json\",\n      xml: \"xml\",\n      html: \"html\",\n      css: \"css\",\n      scss: \"scss\",\n      sass: \"sass\",\n      less: \"less\",\n      sql: \"sql\",\n      md: \"markdown\",\n      toml: \"ini\",\n      ini: \"ini\",\n      dockerfile: \"dockerfile\",\n      makefile: \"makefile\"\n    };\n    return languageMap[ext || \"\"] || \"text\";\n  };\n\n  // Parse content to separate line numbers from code\n  const parseContent = (rawContent: string) => {\n    const lines = rawContent.split('\\n');\n    const codeLines: string[] = [];\n    let minLineNumber = Infinity;\n\n    // First, determine if the content is likely a numbered list from the 'read' tool.\n    // It is if more than half the non-empty lines match the expected format.\n    const nonEmptyLines = lines.filter(line => line.trim() !== '');\n    if (nonEmptyLines.length === 0) {\n      return { codeContent: rawContent, startLineNumber: 1 };\n    }\n    const parsableLines = nonEmptyLines.filter(line => /^\\s*\\d+→/.test(line)).length;\n    const isLikelyNumbered = (parsableLines / nonEmptyLines.length) > 0.5;\n\n    if (!isLikelyNumbered) {\n      return { codeContent: rawContent, startLineNumber: 1 };\n    }\n    \n    // If it's a numbered list, parse it strictly.\n    for (const line of lines) {\n      // Remove leading whitespace before parsing\n      const trimmedLine = line.trimStart();\n      const match = trimmedLine.match(/^(\\d+)→(.*)$/);\n      if (match) {\n        const lineNum = parseInt(match[1], 10);\n        if (minLineNumber === Infinity) {\n          minLineNumber = lineNum;\n        }\n        // Preserve the code content exactly as it appears after the arrow\n        codeLines.push(match[2]);\n      } else if (line.trim() === '') {\n        // Preserve empty lines\n        codeLines.push('');\n      } else {\n        // If a line in a numbered block does not match, it's a formatting anomaly.\n        // Render it as a blank line to avoid showing the raw, un-parsed string.\n        codeLines.push('');\n      }\n    }\n    \n    // Remove trailing empty lines\n    while (codeLines.length > 0 && codeLines[codeLines.length - 1] === '') {\n      codeLines.pop();\n    }\n    \n    return {\n      codeContent: codeLines.join('\\n'),\n      startLineNumber: minLineNumber === Infinity ? 1 : minLineNumber\n    };\n  };\n\n  const language = getLanguage(filePath);\n  const { codeContent, startLineNumber } = parseContent(content);\n  const lineCount = content.split('\\n').filter(line => line.trim()).length;\n  const isLargeFile = lineCount > 20;\n\n  return (\n    <div className=\"rounded-lg overflow-hidden border bg-background w-full\">\n      <div className=\"px-4 py-2 border-b bg-muted/50 flex items-center justify-between\">\n        <div className=\"flex items-center gap-2\">\n          <FileText className=\"h-3.5 w-3.5 text-muted-foreground\" />\n          <span className=\"text-xs font-mono text-muted-foreground\">\n            {filePath || \"File content\"}\n          </span>\n          {isLargeFile && (\n            <span className=\"text-xs text-muted-foreground\">\n              ({lineCount} lines)\n            </span>\n          )}\n        </div>\n        {isLargeFile && (\n          <button\n            onClick={() => setIsExpanded(!isExpanded)}\n            className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            <ChevronRight className={cn(\"h-3 w-3 transition-transform\", isExpanded && \"rotate-90\")} />\n            {isExpanded ? \"Collapse\" : \"Expand\"}\n          </button>\n        )}\n      </div>\n      \n      {(!isLargeFile || isExpanded) && (\n        <div className=\"relative overflow-x-auto\">\n          <SyntaxHighlighter\n            language={language}\n            style={syntaxTheme}\n            showLineNumbers\n            startingLineNumber={startLineNumber}\n            wrapLongLines={false}\n            customStyle={{\n              margin: 0,\n              background: 'transparent',\n              lineHeight: '1.6'\n            }}\n            codeTagProps={{\n              style: {\n                fontSize: '0.75rem'\n              }\n            }}\n            lineNumberStyle={{\n              minWidth: \"3.5rem\",\n              paddingRight: \"1rem\",\n              textAlign: \"right\",\n              opacity: 0.5,\n            }}\n          >\n            {codeContent}\n          </SyntaxHighlighter>\n        </div>\n      )}\n      \n      {isLargeFile && !isExpanded && (\n        <div className=\"px-4 py-3 text-xs text-muted-foreground text-center bg-muted/30\">\n          Click \"Expand\" to view the full file\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**\n * Widget for Glob tool\n */\nexport const GlobWidget: React.FC<{ pattern: string; result?: any }> = ({ pattern, result }) => {\n  // Extract result content if available\n  let resultContent = '';\n  let isError = false;\n  \n  if (result) {\n    isError = result.is_error || false;\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n  }\n  \n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n        <Search className=\"h-4 w-4 text-primary\" />\n        <span className=\"text-sm\">Searching for pattern:</span>\n        <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded\">\n          {pattern}\n        </code>\n        {!result && (\n          <div className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n            <div className=\"h-2 w-2 bg-blue-500 rounded-full animate-pulse\" />\n            <span>Searching...</span>\n          </div>\n        )}\n      </div>\n      \n      {/* Show result if available */}\n      {result && (\n        <div className={cn(\n          \"p-3 rounded-md border text-xs font-mono whitespace-pre-wrap overflow-x-auto\",\n          isError \n            ? \"border-red-500/20 bg-red-500/5 text-red-400\" \n            : \"border-green-500/20 bg-green-500/5 text-green-300\"\n        )}>\n          {resultContent || (isError ? \"Search failed\" : \"No matches found\")}\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**\n * Widget for Bash tool\n */\nexport const BashWidget: React.FC<{ \n  command: string; \n  description?: string;\n  result?: any;\n}> = ({ command, description, result }) => {\n  // Extract result content if available\n  let resultContent = '';\n  let isError = false;\n  \n  if (result) {\n    isError = result.is_error || false;\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n  }\n  \n  return (\n    <div className=\"rounded-lg border bg-background overflow-hidden\">\n      <div className=\"px-4 py-2 bg-muted/50 flex items-center gap-2 border-b\">\n        <Terminal className=\"h-3.5 w-3.5 text-green-500\" />\n        <span className=\"text-xs font-mono text-muted-foreground\">Terminal</span>\n        {description && (\n          <>\n            <ChevronRight className=\"h-3 w-3 text-muted-foreground\" />\n            <span className=\"text-xs text-muted-foreground\">{description}</span>\n          </>\n        )}\n        {/* Show loading indicator when no result yet */}\n        {!result && (\n          <div className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n            <div className=\"h-2 w-2 bg-green-500 rounded-full animate-pulse\" />\n            <span>Running...</span>\n          </div>\n        )}\n      </div>\n      <div className=\"p-4 space-y-3\">\n        <code className=\"text-xs font-mono text-green-400 block\">\n          $ {command}\n        </code>\n        \n        {/* Show result if available */}\n        {result && (\n          <div className={cn(\n            \"mt-3 p-3 rounded-md border text-xs font-mono whitespace-pre-wrap overflow-x-auto\",\n            isError \n              ? \"border-red-500/20 bg-red-500/5 text-red-400\" \n              : \"border-green-500/20 bg-green-500/5 text-green-300\"\n          )}>\n            {resultContent || (isError ? \"Command failed\" : \"Command completed\")}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for Write tool\n */\nexport const WriteWidget: React.FC<{ filePath: string; content: string; result?: any }> = ({ filePath, content, result: _result }) => {\n  const [isMaximized, setIsMaximized] = useState(false);\n  const { theme } = useTheme();\n  const syntaxTheme = getClaudeSyntaxTheme(theme);\n  \n  // Extract file extension for syntax highlighting\n  const getLanguage = (path: string) => {\n    const ext = path.split('.').pop()?.toLowerCase();\n    const languageMap: Record<string, string> = {\n      ts: \"typescript\",\n      tsx: \"tsx\",\n      js: \"javascript\",\n      jsx: \"jsx\",\n      py: \"python\",\n      rs: \"rust\",\n      go: \"go\",\n      java: \"java\",\n      cpp: \"cpp\",\n      c: \"c\",\n      cs: \"csharp\",\n      php: \"php\",\n      rb: \"ruby\",\n      swift: \"swift\",\n      kt: \"kotlin\",\n      scala: \"scala\",\n      sh: \"bash\",\n      bash: \"bash\",\n      zsh: \"bash\",\n      yaml: \"yaml\",\n      yml: \"yaml\",\n      json: \"json\",\n      xml: \"xml\",\n      html: \"html\",\n      css: \"css\",\n      scss: \"scss\",\n      sass: \"sass\",\n      less: \"less\",\n      sql: \"sql\",\n      md: \"markdown\",\n      toml: \"ini\",\n      ini: \"ini\",\n      dockerfile: \"dockerfile\",\n      makefile: \"makefile\"\n    };\n    return languageMap[ext || \"\"] || \"text\";\n  };\n\n  const language = getLanguage(filePath);\n  const isLargeContent = content.length > 1000;\n  const displayContent = isLargeContent ? content.substring(0, 1000) + \"\\n...\" : content;\n\n  // Maximized view as a modal\n  const MaximizedView = () => {\n    if (!isMaximized) return null;\n    \n    return createPortal(\n      <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n        {/* Backdrop with blur */}\n        <div \n          className=\"absolute inset-0 bg-black/60 backdrop-blur-sm\"\n          onClick={() => setIsMaximized(false)}\n        />\n        \n        {/* Modal content */}\n        <div className=\"relative w-[90vw] h-[90vh] max-w-7xl bg-background rounded-lg border shadow-2xl overflow-hidden flex flex-col\">\n          {/* Header */}\n          <div className=\"px-6 py-4 border-b bg-background flex items-center justify-between\">\n            <div className=\"flex items-center gap-3\">\n              <FileText className=\"h-4 w-4 text-muted-foreground\" />\n              <span className=\"text-sm font-mono text-muted-foreground\">{filePath}</span>\n            </div>\n            <Button \n              variant=\"ghost\" \n              size=\"icon\" \n              className=\"h-8 w-8\"\n              onClick={() => setIsMaximized(false)}\n            >\n              <X className=\"h-4 w-4\" />\n            </Button>\n          </div>\n          \n          {/* Code content */}\n          <div className=\"flex-1 overflow-auto\">\n            <SyntaxHighlighter\n              language={language}\n              style={syntaxTheme}\n              customStyle={{\n                margin: 0,\n                padding: '1.5rem',\n                background: 'transparent',\n                fontSize: '0.75rem',\n                lineHeight: '1.5',\n                height: '100%'\n              }}\n              showLineNumbers\n            >\n              {content}\n            </SyntaxHighlighter>\n          </div>\n        </div>\n      </div>,\n      document.body\n    );\n  };\n\n  const CodePreview = ({ codeContent, truncated }: { codeContent: string; truncated: boolean }) => (\n    <div \n      className=\"rounded-lg border bg-background overflow-hidden w-full\"\n      style={{ \n        height: truncated ? '440px' : 'auto', \n        maxHeight: truncated ? '440px' : undefined,\n        display: 'flex', \n        flexDirection: 'column' \n      }}\n    >\n      <div className=\"px-4 py-2 border-b bg-background flex items-center justify-between sticky top-0 z-10\">\n        <span className=\"text-xs font-mono text-muted-foreground\">Preview</span>\n        {isLargeContent && truncated && (\n          <div className=\"flex items-center gap-2\">\n            <Badge variant=\"outline\" className=\"text-xs whitespace-nowrap\">\n              Truncated to 1000 chars\n            </Badge>\n            <Button \n              variant=\"ghost\" \n              size=\"icon\" \n              className=\"h-6 w-6\"\n              onClick={() => setIsMaximized(true)}\n            >\n              <Maximize2 className=\"h-3 w-3\" />\n            </Button>\n          </div>\n        )}\n      </div>\n      <div className=\"overflow-auto flex-1\">\n        <SyntaxHighlighter\n          language={language}\n          style={syntaxTheme}\n          customStyle={{\n            margin: 0,\n            padding: '1rem',\n            background: 'transparent',\n            fontSize: '0.75rem',\n            lineHeight: '1.5',\n            overflowX: 'auto'\n          }}\n          wrapLongLines={false}\n        >\n          {codeContent}\n        </SyntaxHighlighter>\n      </div>\n    </div>\n  );\n\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n        <FileEdit className=\"h-4 w-4 text-primary\" />\n        <span className=\"text-sm\">Writing to file:</span>\n        <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded flex-1 truncate\">\n          {filePath}\n        </code>\n      </div>\n      <CodePreview codeContent={displayContent} truncated={true} />\n      <MaximizedView />\n    </div>\n  );\n};\n\n/**\n * Widget for Grep tool\n */\nexport const GrepWidget: React.FC<{ \n  pattern: string; \n  include?: string; \n  path?: string;\n  exclude?: string;\n  result?: any;\n}> = ({ pattern, include, path, exclude, result }) => {\n  const [isExpanded, setIsExpanded] = useState(true);\n  \n  // Extract result content if available\n  let resultContent = '';\n  let isError = false;\n  \n  if (result) {\n    isError = result.is_error || false;\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n  }\n  \n  // Parse grep results to extract file paths and matches\n  const parseGrepResults = (content: string) => {\n    const lines = content.split('\\n').filter(line => line.trim());\n    const results: Array<{\n      file: string;\n      lineNumber: number;\n      content: string;\n    }> = [];\n    \n    lines.forEach(line => {\n      // Common grep output format: filename:lineNumber:content\n      const match = line.match(/^(.+?):(\\d+):(.*)$/);\n      if (match) {\n        results.push({\n          file: match[1],\n          lineNumber: parseInt(match[2], 10),\n          content: match[3]\n        });\n      }\n    });\n    \n    return results;\n  };\n  \n  const grepResults = result && !isError ? parseGrepResults(resultContent) : [];\n  \n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 p-3 rounded-lg bg-gradient-to-r from-emerald-500/10 to-teal-500/10 border border-emerald-500/20\">\n        <Search className=\"h-4 w-4 text-emerald-500\" />\n        <span className=\"text-sm font-medium\">Searching with grep</span>\n        {!result && (\n          <div className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n            <div className=\"h-2 w-2 bg-emerald-500 rounded-full animate-pulse\" />\n            <span>Searching...</span>\n          </div>\n        )}\n      </div>\n      \n      {/* Search Parameters */}\n      <div className=\"rounded-lg border bg-muted/20 p-3 space-y-2\">\n        <div className=\"grid gap-2\">\n          {/* Pattern with regex highlighting */}\n          <div className=\"flex items-start gap-3\">\n            <div className=\"flex items-center gap-1.5 min-w-[80px]\">\n              <Code className=\"h-3 w-3 text-emerald-500\" />\n              <span className=\"text-xs font-medium text-muted-foreground\">Pattern</span>\n            </div>\n            <code className=\"flex-1 font-mono text-sm bg-emerald-500/10 border border-emerald-500/20 px-3 py-1.5 rounded-md text-emerald-600 dark:text-emerald-400\">\n              {pattern}\n            </code>\n          </div>\n          \n          {/* Path */}\n          {path && (\n            <div className=\"flex items-start gap-3\">\n              <div className=\"flex items-center gap-1.5 min-w-[80px]\">\n                <FolderOpen className=\"h-3 w-3 text-muted-foreground\" />\n                <span className=\"text-xs font-medium text-muted-foreground\">Path</span>\n              </div>\n              <code className=\"flex-1 font-mono text-xs bg-muted px-2 py-1 rounded truncate\">\n                {path}\n              </code>\n            </div>\n          )}\n          \n          {/* Include/Exclude patterns in a row */}\n          {(include || exclude) && (\n            <div className=\"flex gap-4\">\n              {include && (\n                <div className=\"flex items-center gap-2 flex-1\">\n                  <div className=\"flex items-center gap-1.5\">\n                    <FilePlus className=\"h-3 w-3 text-green-500\" />\n                    <span className=\"text-xs font-medium text-muted-foreground\">Include</span>\n                  </div>\n                  <code className=\"font-mono text-xs bg-green-500/10 border border-green-500/20 px-2 py-0.5 rounded text-green-600 dark:text-green-400\">\n                    {include}\n                  </code>\n                </div>\n              )}\n              \n              {exclude && (\n                <div className=\"flex items-center gap-2 flex-1\">\n                  <div className=\"flex items-center gap-1.5\">\n                    <X className=\"h-3 w-3 text-red-500\" />\n                    <span className=\"text-xs font-medium text-muted-foreground\">Exclude</span>\n                  </div>\n                  <code className=\"font-mono text-xs bg-red-500/10 border border-red-500/20 px-2 py-0.5 rounded text-red-600 dark:text-red-400\">\n                    {exclude}\n                  </code>\n                </div>\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n      \n      {/* Results */}\n      {result && (\n        <div className=\"space-y-2\">\n          {isError ? (\n            <div className=\"flex items-center gap-3 p-4 rounded-lg bg-red-500/10 border border-red-500/20\">\n              <AlertCircle className=\"h-5 w-5 text-red-500 flex-shrink-0\" />\n              <div className=\"text-sm text-red-600 dark:text-red-400\">\n                {resultContent || \"Search failed\"}\n              </div>\n            </div>\n          ) : grepResults.length > 0 ? (\n            <>\n              <button\n                onClick={() => setIsExpanded(!isExpanded)}\n                className=\"flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors\"\n              >\n                {isExpanded ? (\n                  <ChevronDown className=\"h-3.5 w-3.5\" />\n                ) : (\n                  <ChevronRight className=\"h-3.5 w-3.5\" />\n                )}\n                <span>{grepResults.length} matches found</span>\n              </button>\n              \n              {isExpanded && (\n                <div className=\"rounded-lg border bg-background overflow-hidden\">\n                  <div className=\"max-h-[400px] overflow-y-auto\">\n                    {grepResults.map((match, idx) => {\n                      const fileName = match.file.split('/').pop() || match.file;\n                      const dirPath = match.file.substring(0, match.file.lastIndexOf('/'));\n                      \n                      return (\n                        <div \n                          key={idx} \n                          className={cn(\n                            \"flex items-start gap-3 p-3 border-b border-border hover:bg-muted/50 transition-colors\",\n                            idx === grepResults.length - 1 && \"border-b-0\"\n                          )}\n                        >\n                          <div className=\"flex items-center gap-2 min-w-[60px]\">\n                            <FileText className=\"h-3.5 w-3.5 text-emerald-500\" />\n                            <span className=\"text-xs font-mono text-emerald-400\">\n                              {match.lineNumber}\n                            </span>\n                          </div>\n                          \n                          <div className=\"flex-1 space-y-1 min-w-0\">\n                            <div className=\"flex items-center gap-2\">\n                              <span className=\"text-xs font-medium text-blue-400 truncate\">\n                                {fileName}\n                              </span>\n                              {dirPath && (\n                                <span className=\"text-xs text-muted-foreground truncate\">\n                                  {dirPath}\n                                </span>\n                              )}\n                            </div>\n                            <code className=\"text-xs font-mono text-zinc-300 block whitespace-pre-wrap break-all\">\n                              {match.content.trim()}\n                            </code>\n                          </div>\n                        </div>\n                      );\n                    })}\n                  </div>\n                </div>\n              )}\n            </>\n          ) : (\n            <div className=\"flex items-center gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20\">\n              <Info className=\"h-5 w-5 text-amber-500 flex-shrink-0\" />\n              <div className=\"text-sm text-amber-600 dark:text-amber-400\">\n                No matches found for the given pattern.\n              </div>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst getLanguage = (path: string) => {\n  const ext = path.split('.').pop()?.toLowerCase();\n  const languageMap: Record<string, string> = {\n    ts: \"typescript\",\n    tsx: \"tsx\",\n    js: \"javascript\",\n    jsx: \"jsx\",\n    py: \"python\",\n    rs: \"rust\",\n    go: \"go\",\n    java: \"java\",\n    cpp: \"cpp\",\n    c: \"c\",\n    cs: \"csharp\",\n    php: \"php\",\n    rb: \"ruby\",\n    swift: \"swift\",\n    kt: \"kotlin\",\n    scala: \"scala\",\n    sh: \"bash\",\n    bash: \"bash\",\n    zsh: \"bash\",\n    yaml: \"yaml\",\n    yml: \"yaml\",\n    json: \"json\",\n    xml: \"xml\",\n    html: \"html\",\n    css: \"css\",\n    scss: \"scss\",\n    sass: \"sass\",\n    less: \"less\",\n    sql: \"sql\",\n    md: \"markdown\",\n    toml: \"ini\",\n    ini: \"ini\",\n    dockerfile: \"dockerfile\",\n    makefile: \"makefile\"\n  };\n  return languageMap[ext || \"\"] || \"text\";\n};\n\n/**\n * Widget for Edit tool - shows the edit operation\n */\nexport const EditWidget: React.FC<{ \n  file_path: string; \n  old_string: string; \n  new_string: string;\n  result?: any;\n}> = ({ file_path, old_string, new_string, result: _result }) => {\n  const { theme } = useTheme();\n  const syntaxTheme = getClaudeSyntaxTheme(theme);\n\n  const diffResult = Diff.diffLines(old_string || '', new_string || '', { \n    newlineIsToken: true,\n    ignoreWhitespace: false \n  });\n  const language = getLanguage(file_path);\n\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 mb-2\">\n        <FileEdit className=\"h-4 w-4 text-primary\" />\n        <span className=\"text-sm font-medium\">Applying Edit to:</span>\n        <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded flex-1 truncate\">\n          {file_path}\n        </code>\n      </div>\n\n      <div className=\"rounded-lg border bg-background overflow-hidden text-xs font-mono\">\n        <div className=\"max-h-[440px] overflow-y-auto overflow-x-auto\">\n          {diffResult.map((part, index) => {\n            const partClass = part.added \n              ? 'bg-green-950/20' \n              : part.removed \n              ? 'bg-red-950/20'\n              : '';\n            \n            if (!part.added && !part.removed && part.count && part.count > 8) {\n              return (\n                <div key={index} className=\"px-4 py-1 bg-muted border-y border-border text-center text-muted-foreground text-xs\">\n                  ... {part.count} unchanged lines ...\n                </div>\n              );\n            }\n            \n            const value = part.value.endsWith('\\n') ? part.value.slice(0, -1) : part.value;\n\n            return (\n              <div key={index} className={cn(partClass, \"flex\")}>\n                <div className=\"w-8 select-none text-center flex-shrink-0\">\n                  {part.added ? <span className=\"text-green-400\">+</span> : part.removed ? <span className=\"text-red-400\">-</span> : null}\n                </div>\n                <div className=\"flex-1\">\n                  <SyntaxHighlighter\n                    language={language}\n                    style={syntaxTheme}\n                    PreTag=\"div\"\n                    wrapLongLines={false}\n                    customStyle={{\n                      margin: 0,\n                      padding: 0,\n                      background: 'transparent',\n                    }}\n                    codeTagProps={{\n                      style: {\n                        fontSize: '0.75rem',\n                        lineHeight: '1.6',\n                      }\n                    }}\n                  >\n                    {value}\n                  </SyntaxHighlighter>\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for Edit tool result - shows a diff view\n */\nexport const EditResultWidget: React.FC<{ content: string }> = ({ content }) => {\n  const { theme } = useTheme();\n  const syntaxTheme = getClaudeSyntaxTheme(theme);\n  \n  // Parse the content to extract file path and code snippet\n  const lines = content.split('\\n');\n  let filePath = '';\n  const codeLines: { lineNumber: string; code: string }[] = [];\n  let inCodeBlock = false;\n  \n  for (const rawLine of lines) {\n    const line = rawLine.replace(/\\r$/, '');\n    if (line.includes('The file') && line.includes('has been updated')) {\n      const match = line.match(/The file (.+) has been updated/);\n      if (match) {\n        filePath = match[1];\n      }\n    } else if (/^\\s*\\d+/.test(line)) {\n      inCodeBlock = true;\n      const lineMatch = line.match(/^\\s*(\\d+)\\t?(.*)$/);\n      if (lineMatch) {\n        const [, lineNum, codePart] = lineMatch;\n        codeLines.push({\n          lineNumber: lineNum,\n          code: codePart,\n        });\n      }\n    } else if (inCodeBlock) {\n      // Allow non-numbered lines inside a code block (for empty lines)\n      codeLines.push({ lineNumber: '', code: line });\n    }\n  }\n\n  const codeContent = codeLines.map(l => l.code).join('\\n');\n  const firstNumberedLine = codeLines.find(l => l.lineNumber !== '');\n  const startLineNumber = firstNumberedLine ? parseInt(firstNumberedLine.lineNumber) : 1;\n  const language = getLanguage(filePath);\n\n  return (\n    <div className=\"rounded-lg border bg-background overflow-hidden\">\n      <div className=\"px-4 py-2 border-b bg-emerald-950/30 flex items-center gap-2\">\n        <GitBranch className=\"h-3.5 w-3.5 text-emerald-500\" />\n        <span className=\"text-xs font-mono text-emerald-400\">Edit Result</span>\n        {filePath && (\n          <>\n            <ChevronRight className=\"h-3 w-3 text-muted-foreground\" />\n            <span className=\"text-xs font-mono text-muted-foreground\">{filePath}</span>\n          </>\n        )}\n      </div>\n      <div className=\"overflow-x-auto max-h-[440px]\">\n        <SyntaxHighlighter\n          language={language}\n          style={syntaxTheme}\n          showLineNumbers\n          startingLineNumber={startLineNumber}\n          wrapLongLines={false}\n          customStyle={{\n            margin: 0,\n            background: 'transparent',\n            lineHeight: '1.6'\n          }}\n          codeTagProps={{\n            style: {\n              fontSize: '0.75rem'\n            }\n          }}\n          lineNumberStyle={{\n            minWidth: \"3.5rem\",\n            paddingRight: \"1rem\",\n            textAlign: \"right\",\n            opacity: 0.5,\n          }}\n        >\n          {codeContent}\n        </SyntaxHighlighter>\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for MCP (Model Context Protocol) tools\n */\nexport const MCPWidget: React.FC<{ \n  toolName: string; \n  input?: any;\n  result?: any;\n}> = ({ toolName, input, result: _result }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n  const { theme } = useTheme();\n  const syntaxTheme = getClaudeSyntaxTheme(theme);\n  \n  // Parse the tool name to extract components\n  // Format: mcp__namespace__method\n  const parts = toolName.split('__');\n  const namespace = parts[1] || '';\n  const method = parts[2] || '';\n  \n  // Format namespace for display (handle kebab-case and snake_case)\n  const formatNamespace = (ns: string) => {\n    return ns\n      .replace(/-/g, ' ')\n      .replace(/_/g, ' ')\n      .split(' ')\n      .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n      .join(' ');\n  };\n  \n  // Format method name\n  const formatMethod = (m: string) => {\n    return m\n      .replace(/_/g, ' ')\n      .split(' ')\n      .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n      .join(' ');\n  };\n  \n  const hasInput = input && Object.keys(input).length > 0;\n  const inputString = hasInput ? JSON.stringify(input, null, 2) : '';\n  const isLargeInput = inputString.length > 200;\n  \n  // Count tokens approximation (very rough estimate)\n  const estimateTokens = (str: string) => {\n    // Rough approximation: ~4 characters per token\n    return Math.ceil(str.length / 4);\n  };\n  \n  const inputTokens = hasInput ? estimateTokens(inputString) : 0;\n\n  return (\n    <div className=\"rounded-lg border border-violet-500/20 bg-gradient-to-br from-violet-500/5 to-purple-500/5 overflow-hidden\">\n      {/* Header */}\n      <div className=\"px-4 py-3 bg-gradient-to-r from-violet-500/10 to-purple-500/10 border-b border-violet-500/20\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-2\">\n            <div className=\"relative\">\n              <Package2 className=\"h-4 w-4 text-violet-500\" />\n              <Sparkles className=\"h-2.5 w-2.5 text-violet-400 absolute -top-1 -right-1\" />\n            </div>\n            <span className=\"text-sm font-medium text-violet-600 dark:text-violet-400\">MCP Tool</span>\n          </div>\n          {hasInput && (\n            <div className=\"flex items-center gap-2\">\n              <Badge \n                variant=\"outline\" \n                className=\"text-xs border-violet-500/30 text-violet-600 dark:text-violet-400\"\n              >\n                ~{inputTokens} tokens\n              </Badge>\n              {isLargeInput && (\n                <button\n                  onClick={() => setIsExpanded(!isExpanded)}\n                  className=\"text-violet-500 hover:text-violet-600 transition-colors\"\n                >\n                  {isExpanded ? (\n                    <ChevronUp className=\"h-3.5 w-3.5\" />\n                  ) : (\n                    <ChevronDown className=\"h-3.5 w-3.5\" />\n                  )}\n                </button>\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n      \n      {/* Tool Path */}\n      <div className=\"px-4 py-3 space-y-3\">\n        <div className=\"flex items-center gap-2 text-sm\">\n          <span className=\"text-violet-500 font-medium\">MCP</span>\n          <ChevronRight className=\"h-3.5 w-3.5 text-muted-foreground\" />\n          <span className=\"text-purple-600 dark:text-purple-400 font-medium\">\n            {formatNamespace(namespace)}\n          </span>\n          <ChevronRight className=\"h-3.5 w-3.5 text-muted-foreground\" />\n          <div className=\"flex items-center gap-1.5\">\n            <Zap className=\"h-3.5 w-3.5 text-violet-500\" />\n            <code className=\"text-sm font-mono font-semibold text-foreground\">\n              {formatMethod(method)}\n              <span className=\"text-muted-foreground\">()</span>\n            </code>\n          </div>\n        </div>\n        \n        {/* Input Parameters */}\n        {hasInput && (\n          <div className={cn(\n            \"transition-all duration-200\",\n            !isExpanded && isLargeInput && \"max-h-[200px]\"\n          )}>\n            <div className=\"relative\">\n              <div className={cn(\n                \"rounded-lg border bg-background/50 overflow-hidden\",\n                !isExpanded && isLargeInput && \"max-h-[200px]\"\n              )}>\n                <div className=\"px-3 py-2 border-b bg-muted/50 flex items-center gap-2\">\n                  <Code className=\"h-3 w-3 text-violet-500\" />\n                  <span className=\"text-xs font-mono text-muted-foreground\">Parameters</span>\n                </div>\n                <div className={cn(\n                  \"overflow-auto\",\n                  !isExpanded && isLargeInput && \"max-h-[150px]\"\n                )}>\n                  <SyntaxHighlighter\n                    language=\"json\"\n                    style={syntaxTheme}\n                    customStyle={{\n                      margin: 0,\n                      padding: '0.75rem',\n                      background: 'transparent',\n                      fontSize: '0.75rem',\n                      lineHeight: '1.5',\n                    }}\n                    wrapLongLines={false}\n                  >\n                    {inputString}\n                  </SyntaxHighlighter>\n                </div>\n              </div>\n              \n              {/* Gradient fade for collapsed view */}\n              {!isExpanded && isLargeInput && (\n                <div className=\"absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-background/80 to-transparent pointer-events-none\" />\n              )}\n            </div>\n            \n            {/* Expand hint */}\n            {!isExpanded && isLargeInput && (\n              <div className=\"text-center mt-2\">\n                <button\n                  onClick={() => setIsExpanded(true)}\n                  className=\"text-xs text-violet-500 hover:text-violet-600 transition-colors inline-flex items-center gap-1\"\n                >\n                  <ChevronDown className=\"h-3 w-3\" />\n                  Show full parameters\n                </button>\n              </div>\n            )}\n          </div>\n        )}\n        \n        {/* No input message */}\n        {!hasInput && (\n          <div className=\"text-xs text-muted-foreground italic px-2\">\n            No parameters required\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for user commands (e.g., model, clear)\n */\nexport const CommandWidget: React.FC<{ \n  commandName: string;\n  commandMessage: string;\n  commandArgs?: string;\n}> = ({ commandName, commandMessage, commandArgs }) => {\n  return (\n    <div className=\"rounded-lg border bg-background/50 overflow-hidden\">\n      <div className=\"px-4 py-2 border-b bg-muted/50 flex items-center gap-2\">\n        <Terminal className=\"h-3.5 w-3.5 text-blue-500\" />\n        <span className=\"text-xs font-mono text-blue-400\">Command</span>\n      </div>\n      <div className=\"p-3 space-y-1\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-xs text-muted-foreground\">$</span>\n          <code className=\"text-sm font-mono text-foreground\">{commandName}</code>\n          {commandArgs && (\n            <code className=\"text-sm font-mono text-muted-foreground\">{commandArgs}</code>\n          )}\n        </div>\n        {commandMessage && commandMessage !== commandName && (\n          <div className=\"text-xs text-muted-foreground ml-4\">{commandMessage}</div>\n        )}\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for command output/stdout\n */\nexport const CommandOutputWidget: React.FC<{ \n  output: string;\n  onLinkDetected?: (url: string) => void;\n}> = ({ output, onLinkDetected }) => {\n  // Check for links on mount and when output changes\n  React.useEffect(() => {\n    if (output && onLinkDetected) {\n      const links = detectLinks(output);\n      if (links.length > 0) {\n        // Notify about the first detected link\n        onLinkDetected(links[0].fullUrl);\n      }\n    }\n  }, [output, onLinkDetected]);\n\n  // Parse ANSI codes for basic styling\n  const parseAnsiToReact = (text: string) => {\n    // Simple ANSI parsing - handles bold (\\u001b[1m) and reset (\\u001b[22m)\n    const parts = text.split(/(\\u001b\\[\\d+m)/);\n    let isBold = false;\n    const elements: React.ReactNode[] = [];\n    \n    parts.forEach((part, idx) => {\n      if (part === '\\u001b[1m') {\n        isBold = true;\n        return;\n      } else if (part === '\\u001b[22m') {\n        isBold = false;\n        return;\n      } else if (part.match(/\\u001b\\[\\d+m/)) {\n        // Ignore other ANSI codes for now\n        return;\n      }\n      \n      if (!part) return;\n      \n      // Make links clickable within this part\n      const linkElements = makeLinksClickable(part, (url) => {\n        onLinkDetected?.(url);\n      });\n      \n      if (isBold) {\n        elements.push(\n          <span key={idx} className=\"font-bold\">\n            {linkElements}\n        </span>\n      );\n      } else {\n        elements.push(...linkElements);\n      }\n    });\n    \n    return elements;\n  };\n\n  return (\n    <div className=\"rounded-lg border bg-background/50 overflow-hidden\">\n      <div className=\"px-4 py-2 bg-muted/50 flex items-center gap-2\">\n        <ChevronRight className=\"h-3 w-3 text-green-500\" />\n        <span className=\"text-xs font-mono text-green-400\">Output</span>\n      </div>\n      <div className=\"p-3\">\n        <pre className=\"text-sm font-mono text-zinc-300 whitespace-pre-wrap\">\n          {output ? parseAnsiToReact(output) : <span className=\"text-zinc-500 italic\">No output</span>}\n        </pre>\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for AI-generated summaries\n */\nexport const SummaryWidget: React.FC<{ \n  summary: string;\n  leafUuid?: string;\n}> = ({ summary, leafUuid }) => {\n  return (\n    <div className=\"rounded-lg border border-blue-500/20 bg-blue-500/5 overflow-hidden\">\n      <div className=\"px-4 py-3 flex items-start gap-3\">\n        <div className=\"mt-0.5\">\n          <div className=\"h-8 w-8 rounded-full bg-blue-500/10 flex items-center justify-center\">\n            <Info className=\"h-4 w-4 text-blue-500\" />\n          </div>\n        </div>\n        <div className=\"flex-1 space-y-1\">\n          <div className=\"text-xs font-medium text-blue-600 dark:text-blue-400\">AI Summary</div>\n          <p className=\"text-sm text-foreground\">{summary}</p>\n          {leafUuid && (\n            <div className=\"text-xs text-muted-foreground mt-2\">\n              ID: <code className=\"font-mono\">{leafUuid.slice(0, 8)}...</code>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for displaying MultiEdit tool usage\n */\nexport const MultiEditWidget: React.FC<{\n  file_path: string;\n  edits: Array<{ old_string: string; new_string: string }>;\n  result?: any;\n}> = ({ file_path, edits, result: _result }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n  const language = getLanguage(file_path);\n  const { theme } = useTheme();\n  const syntaxTheme = getClaudeSyntaxTheme(theme);\n  \n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 mb-2\">\n        <FileEdit className=\"h-4 w-4 text-muted-foreground\" />\n        <span className=\"text-sm font-medium\">Using tool: MultiEdit</span>\n      </div>\n      <div className=\"ml-6 space-y-2\">\n        <div className=\"flex items-center gap-2\">\n          <FileText className=\"h-3 w-3 text-blue-500\" />\n          <code className=\"text-xs font-mono text-blue-500\">{file_path}</code>\n        </div>\n        \n        <div className=\"space-y-1\">\n          <button\n            onClick={() => setIsExpanded(!isExpanded)}\n            className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            <ChevronRight className={cn(\"h-3 w-3 transition-transform\", isExpanded && \"rotate-90\")} />\n            {edits.length} edit{edits.length !== 1 ? 's' : ''}\n          </button>\n          \n          {isExpanded && (\n            <div className=\"space-y-3 mt-3\">\n              {edits.map((edit, index) => {\n                const diffResult = Diff.diffLines(edit.old_string || '', edit.new_string || '', { \n                  newlineIsToken: true,\n                  ignoreWhitespace: false \n                });\n                \n                return (\n                  <div key={index} className=\"space-y-1\">\n                    <div className=\"text-xs font-medium text-muted-foreground\">Edit {index + 1}</div>\n                    <div className=\"rounded-lg border bg-background overflow-hidden text-xs font-mono\">\n                      <div className=\"max-h-[300px] overflow-y-auto overflow-x-auto\">\n                        {diffResult.map((part, partIndex) => {\n                          const partClass = part.added \n                            ? 'bg-green-950/20' \n                            : part.removed \n                            ? 'bg-red-950/20'\n                            : '';\n                          \n                          if (!part.added && !part.removed && part.count && part.count > 8) {\n                            return (\n                              <div key={partIndex} className=\"px-4 py-1 bg-muted border-y border-border text-center text-muted-foreground text-xs\">\n                                ... {part.count} unchanged lines ...\n                              </div>\n                            );\n                          }\n                          \n                          const value = part.value.endsWith('\\n') ? part.value.slice(0, -1) : part.value;\n\n                          return (\n                            <div key={partIndex} className={cn(partClass, \"flex\")}>\n                              <div className=\"w-8 select-none text-center flex-shrink-0\">\n                                {part.added ? <span className=\"text-green-400\">+</span> : part.removed ? <span className=\"text-red-400\">-</span> : null}\n                              </div>\n                              <div className=\"flex-1\">\n                                <SyntaxHighlighter\n                                  language={language}\n                                  style={syntaxTheme}\n                                  PreTag=\"div\"\n                                  wrapLongLines={false}\n                                  customStyle={{\n                                    margin: 0,\n                                    padding: 0,\n                                    background: 'transparent',\n                                  }}\n                                  codeTagProps={{\n                                    style: {\n                                      fontSize: '0.75rem',\n                                      lineHeight: '1.6',\n                                    }\n                                  }}\n                                >\n                                  {value}\n                                </SyntaxHighlighter>\n                              </div>\n                            </div>\n                          );\n                        })}\n                      </div>\n                    </div>\n                  </div>\n                );\n              })}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for displaying MultiEdit tool results with diffs\n */\nexport const MultiEditResultWidget: React.FC<{ \n  content: string;\n  edits?: Array<{ old_string: string; new_string: string }>;\n}> = ({ content, edits }) => {\n  // If we have the edits array, show a nice diff view\n  if (edits && edits.length > 0) {\n    return (\n      <div className=\"space-y-3\">\n        <div className=\"flex items-center gap-2 px-3 py-2 bg-green-500/10 rounded-t-md border-b border-green-500/20\">\n          <GitBranch className=\"h-4 w-4 text-green-500\" />\n          <span className=\"text-sm font-medium text-green-600 dark:text-green-400\">\n            {edits.length} Changes Applied\n          </span>\n        </div>\n        \n        <div className=\"space-y-4\">\n          {edits.map((edit, index) => {\n            // Split the strings into lines for diff display\n            const oldLines = edit.old_string.split('\\n');\n            const newLines = edit.new_string.split('\\n');\n            \n            return (\n              <div key={index} className=\"border border-border/50 rounded-md overflow-hidden\">\n                <div className=\"px-3 py-1 bg-muted/50 border-b border-border/50\">\n                  <span className=\"text-xs font-medium text-muted-foreground\">Change {index + 1}</span>\n                </div>\n                \n                <div className=\"font-mono text-xs\">\n                  {/* Show removed lines */}\n                  {oldLines.map((line, lineIndex) => (\n                    <div\n                      key={`old-${lineIndex}`}\n                      className=\"flex bg-red-500/10 border-l-4 border-red-500\"\n                    >\n                      <span className=\"w-12 px-2 py-1 text-red-600 dark:text-red-400 select-none text-right bg-red-500/10\">\n                        -{lineIndex + 1}\n                      </span>\n                      <pre className=\"flex-1 px-3 py-1 text-red-700 dark:text-red-300 overflow-x-auto\">\n                        <code>{line || ' '}</code>\n                      </pre>\n                    </div>\n                  ))}\n                  \n                  {/* Show added lines */}\n                  {newLines.map((line, lineIndex) => (\n                    <div\n                      key={`new-${lineIndex}`}\n                      className=\"flex bg-green-500/10 border-l-4 border-green-500\"\n                    >\n                      <span className=\"w-12 px-2 py-1 text-green-600 dark:text-green-400 select-none text-right bg-green-500/10\">\n                        +{lineIndex + 1}\n                      </span>\n                      <pre className=\"flex-1 px-3 py-1 text-green-700 dark:text-green-300 overflow-x-auto\">\n                        <code>{line || ' '}</code>\n                      </pre>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    );\n  }\n  \n  // Fallback to simple content display\n  return (\n    <div className=\"p-3 bg-muted/50 rounded-md border\">\n      <pre className=\"text-xs font-mono whitespace-pre-wrap\">{content}</pre>\n    </div>\n  );\n};\n\n/**\n * Widget for displaying system reminders (instead of raw XML)\n */\nexport const SystemReminderWidget: React.FC<{ message: string }> = ({ message }) => {\n  // Extract icon based on message content\n  let icon = <Info className=\"h-4 w-4\" />;\n  let colorClass = \"border-blue-500/20 bg-blue-500/5 text-blue-600\";\n  \n  if (message.toLowerCase().includes(\"warning\")) {\n    icon = <AlertCircle className=\"h-4 w-4\" />;\n    colorClass = \"border-yellow-500/20 bg-yellow-500/5 text-yellow-600\";\n  } else if (message.toLowerCase().includes(\"error\")) {\n    icon = <AlertCircle className=\"h-4 w-4\" />;\n    colorClass = \"border-destructive/20 bg-destructive/5 text-destructive\";\n  }\n  \n  return (\n    <div className={cn(\"flex items-start gap-2 p-3 rounded-md border\", colorClass)}>\n      <div className=\"mt-0.5\">{icon}</div>\n      <div className=\"flex-1 text-sm\">{message}</div>\n    </div>\n  );\n};\n\n/**\n * Widget for displaying system initialization information in a visually appealing way\n * Separates regular tools from MCP tools and provides icons for each tool type\n */\nexport const SystemInitializedWidget: React.FC<{\n  sessionId?: string;\n  model?: string;\n  cwd?: string;\n  tools?: string[];\n}> = ({ sessionId, model, cwd, tools = [] }) => {\n  const [mcpExpanded, setMcpExpanded] = useState(false);\n  \n  // Separate regular tools from MCP tools\n  const regularTools = tools.filter(tool => !tool.startsWith('mcp__'));\n  const mcpTools = tools.filter(tool => tool.startsWith('mcp__'));\n  \n  // Tool icon mapping for regular tools\n  const toolIcons: Record<string, LucideIcon> = {\n    'task': CheckSquare,\n    'bash': Terminal,\n    'glob': FolderSearch,\n    'grep': Search,\n    'ls': List,\n    'exit_plan_mode': LogOut,\n    'read': FileText,\n    'edit': Edit3,\n    'multiedit': Edit3,\n    'write': FilePlus,\n    'notebookread': Book,\n    'notebookedit': BookOpen,\n    'webfetch': Globe,\n    'todoread': ListChecks,\n    'todowrite': ListPlus,\n    'websearch': Globe2,\n  };\n  \n  // Get icon for a tool, fallback to Wrench\n  const getToolIcon = (toolName: string) => {\n    const normalizedName = toolName.toLowerCase();\n    return toolIcons[normalizedName] || Wrench;\n  };\n  \n  // Format MCP tool name (remove mcp__ prefix and format underscores)\n  const formatMcpToolName = (toolName: string) => {\n    // Remove mcp__ prefix\n    const withoutPrefix = toolName.replace(/^mcp__/, '');\n    // Split by double underscores first (provider separator)\n    const parts = withoutPrefix.split('__');\n    if (parts.length >= 2) {\n      // Format provider name and method name separately\n      const provider = parts[0].replace(/_/g, ' ').replace(/-/g, ' ')\n        .split(' ')\n        .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n        .join(' ');\n      const method = parts.slice(1).join('__').replace(/_/g, ' ')\n        .split(' ')\n        .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n        .join(' ');\n      return { provider, method };\n    }\n    // Fallback formatting\n    return {\n      provider: 'MCP',\n      method: withoutPrefix.replace(/_/g, ' ')\n        .split(' ')\n        .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n        .join(' ')\n    };\n  };\n  \n  // Group MCP tools by provider\n  const mcpToolsByProvider = mcpTools.reduce((acc, tool) => {\n    const { provider } = formatMcpToolName(tool);\n    if (!acc[provider]) {\n      acc[provider] = [];\n    }\n    acc[provider].push(tool);\n    return acc;\n  }, {} as Record<string, string[]>);\n  \n  return (\n    <Card className=\"border-blue-500/20 bg-blue-500/5\">\n      <CardContent className=\"p-4\">\n        <div className=\"flex items-start gap-3\">\n          <Settings className=\"h-5 w-5 text-blue-500 mt-0.5\" />\n          <div className=\"flex-1 space-y-4\">\n            <h4 className=\"font-semibold text-sm\">System Initialized</h4>\n            \n            {/* Session Info */}\n            <div className=\"space-y-2\">\n              {sessionId && (\n                <div className=\"flex items-center gap-2 text-xs\">\n                  <Fingerprint className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                  <span className=\"text-muted-foreground\">Session ID:</span>\n                  <code className=\"font-mono text-xs bg-muted px-1.5 py-0.5 rounded\">\n                    {sessionId}\n                  </code>\n                </div>\n              )}\n              \n              {model && (\n                <div className=\"flex items-center gap-2 text-xs\">\n                  <Cpu className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                  <span className=\"text-muted-foreground\">Model:</span>\n                  <code className=\"font-mono text-xs bg-muted px-1.5 py-0.5 rounded\">\n                    {model}\n                  </code>\n                </div>\n              )}\n              \n              {cwd && (\n                <div className=\"flex items-center gap-2 text-xs\">\n                  <FolderOpen className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                  <span className=\"text-muted-foreground\">Working Directory:</span>\n                  <code className=\"font-mono text-xs bg-muted px-1.5 py-0.5 rounded break-all\">\n                    {cwd}\n                  </code>\n                </div>\n              )}\n            </div>\n            \n            {/* Regular Tools */}\n            {regularTools.length > 0 && (\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center gap-2\">\n                  <Wrench className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                  <span className=\"text-xs font-medium text-muted-foreground\">\n                    Available Tools ({regularTools.length})\n                  </span>\n                </div>\n                <div className=\"flex flex-wrap gap-1.5\">\n                  {regularTools.map((tool, idx) => {\n                    const Icon = getToolIcon(tool);\n                    return (\n                      <Badge \n                        key={idx} \n                        variant=\"secondary\" \n                        className=\"text-xs py-0.5 px-2 flex items-center gap-1\"\n                      >\n                        <Icon className=\"h-3 w-3\" />\n                        {tool}\n                      </Badge>\n                    );\n                  })}\n                </div>\n              </div>\n            )}\n            \n            {/* MCP Tools */}\n            {mcpTools.length > 0 && (\n              <div className=\"space-y-2\">\n                <button\n                  onClick={() => setMcpExpanded(!mcpExpanded)}\n                  className=\"flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors\"\n                >\n                  <Package className=\"h-3.5 w-3.5\" />\n                  <span>MCP Services ({mcpTools.length})</span>\n                  <ChevronDown className={cn(\n                    \"h-3 w-3 transition-transform\",\n                    mcpExpanded && \"rotate-180\"\n                  )} />\n                </button>\n                \n                {mcpExpanded && (\n                  <div className=\"ml-5 space-y-3\">\n                    {Object.entries(mcpToolsByProvider).map(([provider, providerTools]) => (\n                      <div key={provider} className=\"space-y-1.5\">\n                        <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n                          <Package2 className=\"h-3 w-3\" />\n                          <span className=\"font-medium\">{provider}</span>\n                          <span className=\"text-muted-foreground/60\">({providerTools.length})</span>\n                        </div>\n                        <div className=\"ml-4 flex flex-wrap gap-1\">\n                          {providerTools.map((tool, idx) => {\n                            const { method } = formatMcpToolName(tool);\n                            return (\n                              <Badge \n                                key={idx} \n                                variant=\"outline\" \n                                className=\"text-xs py-0 px-1.5 font-normal\"\n                              >\n                                {method}\n                              </Badge>\n                            );\n                          })}\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                )}\n              </div>\n            )}\n            \n            {/* Show message if no tools */}\n            {tools.length === 0 && (\n              <div className=\"text-xs text-muted-foreground italic\">\n                No tools available\n              </div>\n            )}\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n};\n\n/**\n * Widget for Task tool - displays sub-agent task information\n */\nexport const TaskWidget: React.FC<{ \n  description?: string; \n  prompt?: string;\n  result?: any;\n}> = ({ description, prompt, result: _result }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n  \n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 mb-2\">\n        <div className=\"relative\">\n          <Bot className=\"h-4 w-4 text-purple-500\" />\n          <Sparkles className=\"h-2.5 w-2.5 text-purple-400 absolute -top-1 -right-1\" />\n        </div>\n        <span className=\"text-sm font-medium\">Spawning Sub-Agent Task</span>\n      </div>\n      \n      <div className=\"ml-6 space-y-3\">\n        {description && (\n          <div className=\"rounded-lg border border-purple-500/20 bg-purple-500/5 p-3\">\n            <div className=\"flex items-center gap-2 mb-1\">\n              <Zap className=\"h-3.5 w-3.5 text-purple-500\" />\n              <span className=\"text-xs font-medium text-purple-600 dark:text-purple-400\">Task Description</span>\n            </div>\n            <p className=\"text-sm text-foreground ml-5\">{description}</p>\n          </div>\n        )}\n        \n        {prompt && (\n          <div className=\"space-y-2\">\n            <button\n              onClick={() => setIsExpanded(!isExpanded)}\n              className=\"flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              <ChevronRight className={cn(\"h-3 w-3 transition-transform\", isExpanded && \"rotate-90\")} />\n              <span>Task Instructions</span>\n            </button>\n            \n            {isExpanded && (\n              <div className=\"rounded-lg border bg-muted/30 p-3\">\n                <pre className=\"text-xs font-mono text-muted-foreground whitespace-pre-wrap\">\n                  {prompt}\n                </pre>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\n/**\n * Widget for WebSearch tool - displays web search query and results\n */\nexport const WebSearchWidget: React.FC<{ \n  query: string; \n  result?: any;\n}> = ({ query, result }) => {\n  const [expandedSections, setExpandedSections] = useState<Set<number>>(new Set());\n  \n  // Parse the result to extract all links sections and build a structured representation\n  const parseSearchResult = (resultContent: string) => {\n    const sections: Array<{\n      type: 'text' | 'links';\n      content: string | Array<{ title: string; url: string }>;\n    }> = [];\n    \n    // Split by \"Links: [\" to find all link sections\n    const parts = resultContent.split(/Links:\\s*\\[/);\n    \n    // First part is always text (or empty)\n    if (parts[0]) {\n      sections.push({ type: 'text', content: parts[0].trim() });\n    }\n    \n    // Process each links section\n    parts.slice(1).forEach(part => {\n      try {\n        // Find the closing bracket\n        const closingIndex = part.indexOf(']');\n        if (closingIndex === -1) return;\n        \n        const linksJson = '[' + part.substring(0, closingIndex + 1);\n        const remainingText = part.substring(closingIndex + 1).trim();\n        \n        // Parse the JSON array\n        const links = JSON.parse(linksJson);\n        sections.push({ type: 'links', content: links });\n        \n        // Add any remaining text\n        if (remainingText) {\n          sections.push({ type: 'text', content: remainingText });\n        }\n      } catch (e) {\n        // If parsing fails, treat it as text\n        sections.push({ type: 'text', content: 'Links: [' + part });\n      }\n    });\n    \n    return sections;\n  };\n  \n  const toggleSection = (index: number) => {\n    const newExpanded = new Set(expandedSections);\n    if (newExpanded.has(index)) {\n      newExpanded.delete(index);\n    } else {\n      newExpanded.add(index);\n    }\n    setExpandedSections(newExpanded);\n  };\n  \n  // Extract result content if available\n  let searchResults: {\n    sections: Array<{\n      type: 'text' | 'links';\n      content: string | Array<{ title: string; url: string }>;\n    }>;\n    noResults: boolean;\n  } = { sections: [], noResults: false };\n  \n  if (result) {\n    let resultContent = '';\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n    \n    searchResults.noResults = resultContent.toLowerCase().includes('no links found') || \n                               resultContent.toLowerCase().includes('no results');\n    searchResults.sections = parseSearchResult(resultContent);\n  }\n  \n  const handleLinkClick = async (url: string) => {\n    try {\n      await open(url);\n    } catch (error) {\n      console.error('Failed to open URL:', error);\n    }\n  };\n  \n  return (\n    <div className=\"flex flex-col gap-2\">\n      {/* Subtle Search Query Header */}\n      <div className=\"flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-500/5 border border-blue-500/10\">\n        <Globe className=\"h-4 w-4 text-blue-500/70\" />\n        <span className=\"text-xs font-medium uppercase tracking-wider text-blue-600/70 dark:text-blue-400/70\">Web Search</span>\n        <span className=\"text-sm text-muted-foreground/80 flex-1 truncate\">{query}</span>\n      </div>\n      \n      {/* Results */}\n      {result && (\n        <div className=\"rounded-lg border bg-background/50 backdrop-blur-sm overflow-hidden\">\n          {!searchResults.sections.length ? (\n            <div className=\"px-3 py-2 flex items-center gap-2 text-muted-foreground\">\n              <div className=\"animate-pulse flex items-center gap-1\">\n                <div className=\"h-1 w-1 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.3s]\"></div>\n                <div className=\"h-1 w-1 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.15s]\"></div>\n                <div className=\"h-1 w-1 bg-blue-500 rounded-full animate-bounce\"></div>\n              </div>\n              <span className=\"text-sm\">Searching...</span>\n            </div>\n          ) : searchResults.noResults ? (\n            <div className=\"px-3 py-2\">\n              <div className=\"flex items-center gap-2 text-muted-foreground\">\n                <AlertCircle className=\"h-4 w-4\" />\n                <span className=\"text-sm\">No results found</span>\n              </div>\n            </div>\n          ) : (\n            <div className=\"p-3 space-y-3\">\n              {searchResults.sections.map((section, idx) => {\n                if (section.type === 'text') {\n                  return (\n                    <div key={idx} className=\"prose prose-sm dark:prose-invert max-w-none\">\n                      <ReactMarkdown>{section.content as string}</ReactMarkdown>\n                    </div>\n                  );\n                } else if (section.type === 'links' && Array.isArray(section.content)) {\n                  const links = section.content;\n                  const isExpanded = expandedSections.has(idx);\n                  \n                  return (\n                    <div key={idx} className=\"space-y-1.5\">\n                      {/* Toggle Button */}\n                      <button\n                        onClick={() => toggleSection(idx)}\n                        className=\"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n                      >\n                        {isExpanded ? (\n                          <ChevronDown className=\"h-3 w-3\" />\n                        ) : (\n                          <ChevronRight className=\"h-3 w-3\" />\n                        )}\n                        <span>{links.length} result{links.length !== 1 ? 's' : ''}</span>\n                      </button>\n                      \n                      {/* Links Display */}\n                      {isExpanded ? (\n                        /* Expanded Card View */\n                        <div className=\"grid gap-1.5 ml-4\">\n                          {links.map((link, linkIdx) => (\n                            <button\n                              key={linkIdx}\n                              onClick={() => handleLinkClick(link.url)}\n                              className=\"group flex flex-col gap-0.5 p-2.5 rounded-md border bg-card/30 hover:bg-card/50 hover:border-blue-500/30 transition-all text-left\"\n                            >\n                              <div className=\"flex items-start gap-2\">\n                                <Globe2 className=\"h-3.5 w-3.5 text-blue-500/70 mt-0.5 flex-shrink-0\" />\n                                <div className=\"flex-1 min-w-0\">\n                                  <div className=\"text-sm font-medium group-hover:text-blue-500 transition-colors line-clamp-2\">\n                                    {link.title}\n                                  </div>\n                                  <div className=\"text-xs text-muted-foreground mt-0.5 truncate\">\n                                    {link.url}\n                                  </div>\n                                </div>\n                              </div>\n                            </button>\n                          ))}\n                        </div>\n                      ) : (\n                        /* Collapsed Pills View */\n                        <div className=\"flex flex-wrap gap-1.5 ml-4\">\n                          {links.map((link, linkIdx) => (\n                            <button\n                              key={linkIdx}\n                              onClick={(e) => {\n                                e.stopPropagation();\n                                handleLinkClick(link.url);\n                              }}\n                              className=\"group inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium bg-blue-500/5 hover:bg-blue-500/10 border border-blue-500/10 hover:border-blue-500/20 transition-all\"\n                            >\n                              <Globe2 className=\"h-3 w-3 text-blue-500/70\" />\n                              <span className=\"truncate max-w-[180px] text-foreground/70 group-hover:text-foreground/90\">\n                                {link.title}\n                              </span>\n                            </button>\n                          ))}\n                        </div>\n                      )}\n                    </div>\n                  );\n                }\n                return null;\n              })}\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**\n * Widget for displaying AI thinking/reasoning content\n * Collapsible and closed by default\n */\nexport const ThinkingWidget: React.FC<{ \n  thinking: string;\n  signature?: string;\n}> = ({ thinking }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n  \n  // Strip whitespace from thinking content\n  const trimmedThinking = thinking.trim();\n  \n  return (\n    <div className=\"rounded-lg border border-gray-500/20 bg-gray-500/5 overflow-hidden\">\n      <button\n        onClick={() => setIsExpanded(!isExpanded)}\n        className=\"w-full px-4 py-3 flex items-center justify-between hover:bg-gray-500/10 transition-colors\"\n      >\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative\">\n            <Bot className=\"h-4 w-4 text-gray-500\" />\n            <Sparkles className=\"h-2.5 w-2.5 text-gray-400 absolute -top-1 -right-1 animate-pulse\" />\n          </div>\n          <span className=\"text-sm font-medium text-gray-600 dark:text-gray-400 italic\">\n            Thinking...\n          </span>\n        </div>\n        <ChevronRight className={cn(\n          \"h-4 w-4 text-gray-500 transition-transform\",\n          isExpanded && \"rotate-90\"\n        )} />\n      </button>\n      \n      {isExpanded && (\n        <div className=\"px-4 pb-4 pt-2 border-t border-gray-500/20\">\n          <pre className=\"text-xs font-mono text-gray-600 dark:text-gray-400 whitespace-pre-wrap bg-gray-500/5 p-3 rounded-lg italic\">\n            {trimmedThinking}\n          </pre>\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**\n * Widget for WebFetch tool - displays URL fetching with optional prompts\n */\nexport const WebFetchWidget: React.FC<{ \n  url: string;\n  prompt?: string;\n  result?: any;\n}> = ({ url, prompt, result }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [showFullContent, setShowFullContent] = useState(false);\n  \n  // Extract result content if available\n  let fetchedContent = '';\n  let isLoading = !result;\n  let hasError = false;\n  \n  if (result) {\n    if (typeof result.content === 'string') {\n      fetchedContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        fetchedContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        fetchedContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        fetchedContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n    \n    // Check if there's an error\n    hasError = result.is_error || \n               fetchedContent.toLowerCase().includes('error') ||\n               fetchedContent.toLowerCase().includes('failed');\n  }\n  \n  // Truncate content for preview\n  const maxPreviewLength = 500;\n  const isTruncated = fetchedContent.length > maxPreviewLength;\n  const previewContent = isTruncated && !showFullContent\n    ? fetchedContent.substring(0, maxPreviewLength) + '...'\n    : fetchedContent;\n  \n  // Extract domain from URL for display\n  const getDomain = (urlString: string) => {\n    try {\n      const urlObj = new URL(urlString);\n      return urlObj.hostname;\n    } catch {\n      return urlString;\n    }\n  };\n  \n  const handleUrlClick = async () => {\n    try {\n      await open(url);\n    } catch (error) {\n      console.error('Failed to open URL:', error);\n    }\n  };\n  \n  return (\n    <div className=\"flex flex-col gap-2\">\n      {/* Header with URL and optional prompt */}\n      <div className=\"space-y-2\">\n        {/* URL Display */}\n        <div className=\"flex items-center gap-2 px-3 py-2 rounded-lg bg-purple-500/5 border border-purple-500/10\">\n          <Globe className=\"h-4 w-4 text-purple-500/70\" />\n          <span className=\"text-xs font-medium uppercase tracking-wider text-purple-600/70 dark:text-purple-400/70\">Fetching</span>\n          <button\n            onClick={handleUrlClick}\n            className=\"text-sm text-foreground/80 hover:text-foreground flex-1 truncate text-left hover:underline decoration-purple-500/50\"\n          >\n            {url}\n          </button>\n        </div>\n        \n        {/* Prompt Display */}\n        {prompt && (\n          <div className=\"ml-6 space-y-1\">\n            <button\n              onClick={() => setIsExpanded(!isExpanded)}\n              className=\"flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              <ChevronRight className={cn(\"h-3 w-3 transition-transform\", isExpanded && \"rotate-90\")} />\n              <Info className=\"h-3 w-3\" />\n              <span>Analysis Prompt</span>\n            </button>\n            \n            {isExpanded && (\n              <div className=\"rounded-lg border bg-muted/30 p-3 ml-4\">\n                <p className=\"text-sm text-foreground/90\">\n                  {prompt}\n                </p>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n      \n      {/* Results */}\n      {isLoading ? (\n        <div className=\"rounded-lg border bg-background/50 backdrop-blur-sm overflow-hidden\">\n          <div className=\"px-3 py-2 flex items-center gap-2 text-muted-foreground\">\n            <div className=\"animate-pulse flex items-center gap-1\">\n              <div className=\"h-1 w-1 bg-purple-500 rounded-full animate-bounce [animation-delay:-0.3s]\"></div>\n              <div className=\"h-1 w-1 bg-purple-500 rounded-full animate-bounce [animation-delay:-0.15s]\"></div>\n              <div className=\"h-1 w-1 bg-purple-500 rounded-full animate-bounce\"></div>\n            </div>\n            <span className=\"text-sm\">Fetching content from {getDomain(url)}...</span>\n          </div>\n        </div>\n      ) : fetchedContent ? (\n        <div className=\"rounded-lg border bg-background/50 backdrop-blur-sm overflow-hidden\">\n          {hasError ? (\n            <div className=\"px-3 py-2\">\n              <div className=\"flex items-center gap-2 text-destructive\">\n                <AlertCircle className=\"h-4 w-4\" />\n                <span className=\"text-sm font-medium\">Failed to fetch content</span>\n              </div>\n              <pre className=\"mt-2 text-xs font-mono text-muted-foreground whitespace-pre-wrap\">\n                {fetchedContent}\n              </pre>\n            </div>\n          ) : (\n            <div className=\"p-3 space-y-2\">\n              {/* Content Header */}\n              <div className=\"flex items-center justify-between\">\n                <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n                  <FileText className=\"h-3.5 w-3.5\" />\n                  <span>Content from {getDomain(url)}</span>\n                </div>\n                {isTruncated && (\n                  <button\n                    onClick={() => setShowFullContent(!showFullContent)}\n                    className=\"text-xs text-purple-500 hover:text-purple-600 transition-colors flex items-center gap-1\"\n                  >\n                    {showFullContent ? (\n                      <>\n                        <ChevronUp className=\"h-3 w-3\" />\n                        Show less\n                      </>\n                    ) : (\n                      <>\n                        <ChevronDown className=\"h-3 w-3\" />\n                        Show full content\n                      </>\n                    )}\n                  </button>\n                )}\n              </div>\n              \n              {/* Fetched Content */}\n              <div className=\"relative\">\n                <div className={cn(\n                  \"rounded-lg bg-muted/30 p-3 overflow-hidden\",\n                  !showFullContent && isTruncated && \"max-h-[300px]\"\n                )}>\n                  <pre className=\"text-sm font-mono text-foreground/90 whitespace-pre-wrap\">\n                    {previewContent}\n                  </pre>\n                  {!showFullContent && isTruncated && (\n                    <div className=\"absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-muted/30 to-transparent pointer-events-none\" />\n                  )}\n                </div>\n              </div>\n            </div>\n          )}\n        </div>\n      ) : (\n        <div className=\"rounded-lg border bg-background/50 backdrop-blur-sm overflow-hidden\">\n          <div className=\"px-3 py-2\">\n            <div className=\"flex items-center gap-2 text-muted-foreground\">\n              <Info className=\"h-4 w-4\" />\n              <span className=\"text-sm\">No content returned</span>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**\n * Widget for TodoRead tool - displays todos with advanced viewing capabilities\n */\nexport const TodoReadWidget: React.FC<{ todos?: any[]; result?: any }> = ({ todos: inputTodos, result }) => {\n  // Extract todos from result if not directly provided\n  let todos: any[] = inputTodos || [];\n  if (!todos.length && result) {\n    if (typeof result === 'object' && Array.isArray(result.todos)) {\n      todos = result.todos;\n    } else if (typeof result.content === 'string') {\n      try {\n        const parsed = JSON.parse(result.content);\n        if (Array.isArray(parsed)) todos = parsed;\n        else if (parsed.todos) todos = parsed.todos;\n      } catch (e) {\n        // Not JSON, ignore\n      }\n    }\n  }\n\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [statusFilter, setStatusFilter] = useState<string>(\"all\");\n  const [viewMode, setViewMode] = useState<\"list\" | \"board\" | \"timeline\" | \"stats\">(\"list\");\n  const [expandedTodos, setExpandedTodos] = useState<Set<string>>(new Set());\n\n  // Status icons and colors\n  const statusConfig = {\n    completed: {\n      icon: <CheckCircle2 className=\"h-4 w-4\" />,\n      color: \"text-green-500\",\n      bgColor: \"bg-green-500/10\",\n      borderColor: \"border-green-500/20\",\n      label: \"Completed\"\n    },\n    in_progress: {\n      icon: <Clock className=\"h-4 w-4 animate-pulse\" />,\n      color: \"text-blue-500\",\n      bgColor: \"bg-blue-500/10\",\n      borderColor: \"border-blue-500/20\",\n      label: \"In Progress\"\n    },\n    pending: {\n      icon: <Circle className=\"h-4 w-4\" />,\n      color: \"text-muted-foreground\",\n      bgColor: \"bg-muted/50\",\n      borderColor: \"border-muted\",\n      label: \"Pending\"\n    },\n    cancelled: {\n      icon: <X className=\"h-4 w-4\" />,\n      color: \"text-red-500\",\n      bgColor: \"bg-red-500/10\",\n      borderColor: \"border-red-500/20\",\n      label: \"Cancelled\"\n    }\n  };\n\n  // Filter todos based on search and status\n  const filteredTodos = todos.filter(todo => {\n    const matchesSearch = !searchQuery || \n      todo.content.toLowerCase().includes(searchQuery.toLowerCase()) ||\n      (todo.id && todo.id.toLowerCase().includes(searchQuery.toLowerCase()));\n    \n    const matchesStatus = statusFilter === \"all\" || todo.status === statusFilter;\n    \n    return matchesSearch && matchesStatus;\n  });\n\n  // Calculate statistics\n  const stats = {\n    total: todos.length,\n    completed: todos.filter(t => t.status === \"completed\").length,\n    inProgress: todos.filter(t => t.status === \"in_progress\").length,\n    pending: todos.filter(t => t.status === \"pending\").length,\n    cancelled: todos.filter(t => t.status === \"cancelled\").length,\n    completionRate: todos.length > 0 \n      ? Math.round((todos.filter(t => t.status === \"completed\").length / todos.length) * 100)\n      : 0\n  };\n\n  // Group todos by status for board view\n  const todosByStatus = {\n    pending: filteredTodos.filter(t => t.status === \"pending\"),\n    in_progress: filteredTodos.filter(t => t.status === \"in_progress\"),\n    completed: filteredTodos.filter(t => t.status === \"completed\"),\n    cancelled: filteredTodos.filter(t => t.status === \"cancelled\")\n  };\n\n  // Toggle expanded state for a todo\n  const toggleExpanded = (todoId: string) => {\n    setExpandedTodos(prev => {\n      const next = new Set(prev);\n      if (next.has(todoId)) {\n        next.delete(todoId);\n      } else {\n        next.add(todoId);\n      }\n      return next;\n    });\n  };\n\n  // Export todos as JSON\n  const exportAsJson = () => {\n    const dataStr = JSON.stringify(todos, null, 2);\n    const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);\n    const exportFileDefaultName = 'todos.json';\n    const linkElement = document.createElement('a');\n    linkElement.setAttribute('href', dataUri);\n    linkElement.setAttribute('download', exportFileDefaultName);\n    linkElement.click();\n  };\n\n  // Export todos as Markdown\n  const exportAsMarkdown = () => {\n    let markdown = \"# Todo List\\n\\n\";\n    markdown += `**Total**: ${stats.total} | **Completed**: ${stats.completed} | **In Progress**: ${stats.inProgress} | **Pending**: ${stats.pending}\\n\\n`;\n    \n    const statusGroups = [\"pending\", \"in_progress\", \"completed\", \"cancelled\"];\n    statusGroups.forEach(status => {\n      const todosInStatus = todos.filter(t => t.status === status);\n      if (todosInStatus.length > 0) {\n        markdown += `## ${statusConfig[status as keyof typeof statusConfig]?.label || status}\\n\\n`;\n        todosInStatus.forEach(todo => {\n          const checkbox = todo.status === \"completed\" ? \"[x]\" : \"[ ]\";\n          markdown += `- ${checkbox} ${todo.content}${todo.id ? ` (${todo.id})` : \"\"}\\n`;\n          if (todo.dependencies?.length > 0) {\n            markdown += `  - Dependencies: ${todo.dependencies.join(\", \")}\\n`;\n          }\n        });\n        markdown += \"\\n\";\n      }\n    });\n    \n    const dataUri = 'data:text/markdown;charset=utf-8,'+ encodeURIComponent(markdown);\n    const linkElement = document.createElement('a');\n    linkElement.setAttribute('href', dataUri);\n    linkElement.setAttribute('download', 'todos.md');\n    linkElement.click();\n  };\n\n  // Render todo card\n  const TodoCard = ({ todo, isExpanded }: { todo: any; isExpanded: boolean }) => {\n    const config = statusConfig[todo.status as keyof typeof statusConfig] || statusConfig.pending;\n    \n    return (\n      <motion.div\n        layout\n        initial={{ opacity: 0, y: 20 }}\n        animate={{ opacity: 1, y: 0 }}\n        exit={{ opacity: 0, y: -20 }}\n        className={cn(\n          \"group rounded-lg border p-4 transition-all hover:shadow-md cursor-pointer\",\n          config.bgColor,\n          config.borderColor,\n          todo.status === \"completed\" && \"opacity-75\"\n        )}\n        onClick={() => todo.id && toggleExpanded(todo.id)}\n      >\n        <div className=\"flex items-start gap-3\">\n          <div className={cn(\"mt-0.5\", config.color)}>\n            {config.icon}\n          </div>\n          <div className=\"flex-1 space-y-2\">\n            <p className={cn(\n              \"text-sm\",\n              todo.status === \"completed\" && \"line-through\"\n            )}>\n              {todo.content}\n            </p>\n            \n            {/* Todo metadata */}\n            <div className=\"flex flex-wrap items-center gap-2 text-xs text-muted-foreground\">\n              {todo.id && (\n                <div className=\"flex items-center gap-1\">\n                  <Hash className=\"h-3 w-3\" />\n                  <span className=\"font-mono\">{todo.id}</span>\n                </div>\n              )}\n              {todo.dependencies?.length > 0 && (\n                <div className=\"flex items-center gap-1\">\n                  <GitBranch className=\"h-3 w-3\" />\n                  <span>{todo.dependencies.length} deps</span>\n                </div>\n              )}\n            </div>\n            \n            {/* Expanded details */}\n            <AnimatePresence>\n              {isExpanded && todo.dependencies?.length > 0 && (\n                <motion.div\n                  initial={{ height: 0, opacity: 0 }}\n                  animate={{ height: \"auto\", opacity: 1 }}\n                  exit={{ height: 0, opacity: 0 }}\n                  className=\"overflow-hidden\"\n                >\n                  <div className=\"pt-2 mt-2 border-t space-y-1\">\n                    <span className=\"text-xs font-medium text-muted-foreground\">Dependencies:</span>\n                    <div className=\"flex flex-wrap gap-1\">\n                      {todo.dependencies.map((dep: string) => (\n                        <Badge\n                          key={dep}\n                          variant=\"outline\"\n                          className=\"text-xs font-mono\"\n                        >\n                          {dep}\n                        </Badge>\n                      ))}\n                    </div>\n                  </div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n        </div>\n      </motion.div>\n    );\n  };\n\n  // Render statistics view\n  const StatsView = () => (\n    <div className=\"space-y-4\">\n      {/* Overall Progress */}\n      <Card className=\"p-4\">\n        <div className=\"flex items-center justify-between mb-3\">\n          <h4 className=\"text-sm font-medium\">Overall Progress</h4>\n          <span className=\"text-2xl font-bold text-primary\">{stats.completionRate}%</span>\n        </div>\n        <div className=\"w-full bg-muted rounded-full h-3 overflow-hidden\">\n          <motion.div\n            initial={{ width: 0 }}\n            animate={{ width: `${stats.completionRate}%` }}\n            transition={{ duration: 0.5, ease: \"easeOut\" }}\n            className=\"h-full bg-gradient-to-r from-primary to-primary/80\"\n          />\n        </div>\n      </Card>\n\n      {/* Status Breakdown */}\n      <div className=\"grid grid-cols-2 gap-3\">\n        {Object.entries(statusConfig).map(([status, config]) => {\n          const count = stats[status as keyof typeof stats] || 0;\n          const percentage = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0;\n          \n          return (\n            <Card key={status} className={cn(\"p-4\", config.bgColor)}>\n              <div className=\"flex items-center gap-3\">\n                <div className={config.color}>{config.icon}</div>\n                <div className=\"flex-1\">\n                  <p className=\"text-xs text-muted-foreground\">{config.label}</p>\n                  <p className=\"text-lg font-semibold\">{count}</p>\n                  <p className=\"text-xs text-muted-foreground\">{percentage}%</p>\n                </div>\n              </div>\n            </Card>\n          );\n        })}\n      </div>\n\n      {/* Activity Chart */}\n      <Card className=\"p-4\">\n        <div className=\"flex items-center gap-2 mb-3\">\n          <Activity className=\"h-4 w-4 text-primary\" />\n          <h4 className=\"text-sm font-medium\">Activity Overview</h4>\n        </div>\n        <div className=\"space-y-2\">\n          {Object.entries(statusConfig).map(([status, config]) => {\n            const count = stats[status as keyof typeof stats] || 0;\n            const percentage = stats.total > 0 ? (count / stats.total) * 100 : 0;\n            \n            return (\n              <div key={status} className=\"flex items-center gap-3\">\n                <span className=\"text-xs w-20 text-right\">{config.label}</span>\n                <div className=\"flex-1 bg-muted rounded-full h-2 overflow-hidden\">\n                  <motion.div\n                    initial={{ width: 0 }}\n                    animate={{ width: `${percentage}%` }}\n                    transition={{ duration: 0.5, delay: 0.1 }}\n                    className={cn(\"h-full\", config.bgColor)}\n                  />\n                </div>\n                <span className=\"text-xs w-12 text-left\">{count}</span>\n              </div>\n            );\n          })}\n        </div>\n      </Card>\n    </div>\n  );\n\n  // Render board view\n  const BoardView = () => (\n    <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">\n      {Object.entries(todosByStatus).map(([status, todos]) => {\n        const config = statusConfig[status as keyof typeof statusConfig];\n        \n        return (\n          <div key={status} className=\"space-y-3\">\n            <div className=\"flex items-center gap-2 pb-2 border-b\">\n              <div className={config.color}>{config.icon}</div>\n              <h3 className=\"text-sm font-medium\">{config.label}</h3>\n              <Badge variant=\"secondary\" className=\"ml-auto text-xs\">\n                {todos.length}\n              </Badge>\n            </div>\n            <div className=\"space-y-2\">\n              {todos.map(todo => (\n                <TodoCard \n                  key={todo.id || todos.indexOf(todo)} \n                  todo={todo} \n                  isExpanded={expandedTodos.has(todo.id)}\n                />\n              ))}\n              {todos.length === 0 && (\n                <p className=\"text-xs text-muted-foreground text-center py-4\">\n                  No todos\n                </p>\n              )}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n\n  // Render timeline view\n  const TimelineView = () => {\n    // Group todos by their dependencies to create a timeline\n    const rootTodos = todos.filter(t => !t.dependencies || t.dependencies.length === 0);\n    const rendered = new Set<string>();\n    \n    const renderTodoWithDependents = (todo: any, level = 0) => {\n      if (rendered.has(todo.id)) return null;\n      rendered.add(todo.id);\n      \n      const dependents = todos.filter(t => \n        t.dependencies?.includes(todo.id) && !rendered.has(t.id)\n      );\n      \n      return (\n        <div key={todo.id} className=\"relative\">\n          {level > 0 && (\n            <div className=\"absolute left-6 top-0 w-px h-6 bg-border\" />\n          )}\n          <div className={cn(\"flex gap-4\", level > 0 && \"ml-12\")}>\n            <div className=\"relative\">\n              <div className={cn(\n                \"w-3 h-3 rounded-full border-2 bg-background\",\n                statusConfig[todo.status as keyof typeof statusConfig]?.borderColor\n              )} />\n              {dependents.length > 0 && (\n                <div className=\"absolute left-1/2 top-3 w-px h-full bg-border -translate-x-1/2\" />\n              )}\n            </div>\n            <div className=\"flex-1 pb-6\">\n              <TodoCard \n                todo={todo} \n                isExpanded={expandedTodos.has(todo.id)}\n              />\n            </div>\n          </div>\n          {dependents.map(dep => renderTodoWithDependents(dep, level + 1))}\n        </div>\n      );\n    };\n    \n    return (\n      <div className=\"space-y-4\">\n        {rootTodos.map(todo => renderTodoWithDependents(todo))}\n        {todos.filter(t => !rendered.has(t.id)).map(todo => renderTodoWithDependents(todo))}\n      </div>\n    );\n  };\n\n  return (\n    <div className=\"space-y-4\">\n      {/* Header */}\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-3\">\n          <ListChecks className=\"h-5 w-5 text-primary\" />\n          <div>\n            <h3 className=\"text-sm font-medium\">Todo Overview</h3>\n            <p className=\"text-xs text-muted-foreground\">\n              {stats.total} total • {stats.completed} completed • {stats.completionRate}% done\n            </p>\n          </div>\n        </div>\n        \n        {/* Export Options */}\n        <div className=\"flex items-center gap-2\">\n          <Button\n            size=\"sm\"\n            variant=\"outline\"\n            className=\"h-7 text-xs\"\n            onClick={exportAsJson}\n          >\n            <Download className=\"h-3 w-3 mr-1\" />\n            JSON\n          </Button>\n          <Button\n            size=\"sm\"\n            variant=\"outline\"\n            className=\"h-7 text-xs\"\n            onClick={exportAsMarkdown}\n          >\n            <Download className=\"h-3 w-3 mr-1\" />\n            Markdown\n          </Button>\n        </div>\n      </div>\n\n      {/* Search and Filters */}\n      <div className=\"flex flex-col sm:flex-row gap-2\">\n        <div className=\"relative flex-1\">\n          <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n          <Input\n            type=\"text\"\n            placeholder=\"Search todos...\"\n            value={searchQuery}\n            onChange={(e) => setSearchQuery(e.target.value)}\n            className=\"pl-9 h-9\"\n          />\n        </div>\n        \n        <div className=\"flex gap-2\">\n          <div className=\"flex gap-1 p-1 bg-muted rounded-md\">\n            {[\"all\", \"pending\", \"in_progress\", \"completed\", \"cancelled\"].map(status => (\n              <Button\n                key={status}\n                size=\"sm\"\n                variant={statusFilter === status ? \"default\" : \"ghost\"}\n                className=\"h-7 px-2 text-xs\"\n                onClick={() => setStatusFilter(status)}\n              >\n                {status === \"all\" ? \"All\" : statusConfig[status as keyof typeof statusConfig]?.label}\n                {status === \"all\" && (\n                  <Badge variant=\"secondary\" className=\"ml-1 h-4 px-1 text-xs\">\n                    {stats.total}\n                  </Badge>\n                )}\n              </Button>\n            ))}\n          </div>\n        </div>\n      </div>\n\n      {/* View Mode Tabs */}\n      <Tabs value={viewMode} onValueChange={(v) => setViewMode(v as typeof viewMode)}>\n        <TabsList className=\"grid w-full grid-cols-4\">\n          <TabsTrigger value=\"list\" className=\"text-xs\">\n            <LayoutList className=\"h-4 w-4 mr-1\" />\n            List\n          </TabsTrigger>\n          <TabsTrigger value=\"board\" className=\"text-xs\">\n            <LayoutGrid className=\"h-4 w-4 mr-1\" />\n            Board\n          </TabsTrigger>\n          <TabsTrigger value=\"timeline\" className=\"text-xs\">\n            <GitBranch className=\"h-4 w-4 mr-1\" />\n            Timeline\n          </TabsTrigger>\n          <TabsTrigger value=\"stats\" className=\"text-xs\">\n            <BarChart3 className=\"h-4 w-4 mr-1\" />\n            Stats\n          </TabsTrigger>\n        </TabsList>\n\n        <TabsContent value=\"list\" className=\"mt-4\">\n          <div className=\"space-y-2\">\n            <AnimatePresence mode=\"popLayout\">\n              {filteredTodos.map(todo => (\n                <TodoCard \n                  key={todo.id || filteredTodos.indexOf(todo)} \n                  todo={todo} \n                  isExpanded={expandedTodos.has(todo.id)}\n                />\n              ))}\n            </AnimatePresence>\n            {filteredTodos.length === 0 && (\n              <div className=\"text-center py-8 text-sm text-muted-foreground\">\n                {searchQuery || statusFilter !== \"all\" \n                  ? \"No todos match your filters\" \n                  : \"No todos available\"}\n              </div>\n            )}\n          </div>\n        </TabsContent>\n\n        <TabsContent value=\"board\" className=\"mt-4\">\n          <BoardView />\n        </TabsContent>\n\n        <TabsContent value=\"timeline\" className=\"mt-4\">\n          <TimelineView />\n        </TabsContent>\n\n        <TabsContent value=\"stats\" className=\"mt-4\">\n          <StatsView />\n        </TabsContent>\n      </Tabs>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/Topbar.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Circle, ExternalLink } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Popover } from \"@/components/ui/popover\";\nimport { api, type ClaudeVersionStatus } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\ninterface TopbarProps {\n  /**\n   * Callback when CLAUDE.md is clicked\n   */\n  onClaudeClick: () => void;\n  /**\n   * Callback when Settings is clicked\n   */\n  onSettingsClick: () => void;\n  /**\n   * Callback when Usage Dashboard is clicked\n   */\n  onUsageClick: () => void;\n  /**\n   * Callback when MCP is clicked\n   */\n  onMCPClick: () => void;\n  /**\n   * Callback when Info is clicked\n   */\n  onInfoClick: () => void;\n  /**\n   * Callback when Agents is clicked\n   */\n  onAgentsClick?: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * Topbar component with status indicator and navigation buttons\n * \n * @example\n * <Topbar\n *   onClaudeClick={() => setView('editor')}\n *   onSettingsClick={() => setView('settings')}\n *   onUsageClick={() => setView('usage-dashboard')}\n *   onMCPClick={() => setView('mcp')}\n * />\n */\nexport const Topbar: React.FC<TopbarProps> = ({\n  onSettingsClick,\n  className,\n}) => {\n  const [versionStatus, setVersionStatus] = useState<ClaudeVersionStatus | null>(null);\n  const [checking, setChecking] = useState(true);\n  \n  // Check Claude version on mount\n  useEffect(() => {\n    checkVersion();\n  }, []);\n  \n  const checkVersion = async () => {\n    try {\n      setChecking(true);\n      const status = await api.checkClaudeVersion();\n      setVersionStatus(status);\n      \n      // If Claude is not installed and the error indicates it wasn't found\n      if (!status.is_installed && status.output.includes(\"No such file or directory\")) {\n        // Emit an event that can be caught by the parent\n        window.dispatchEvent(new CustomEvent('claude-not-found'));\n      }\n    } catch (err) {\n      console.error(\"Failed to check Claude version:\", err);\n      setVersionStatus({\n        is_installed: false,\n        output: \"Failed to check version\",\n      });\n    } finally {\n      setChecking(false);\n    }\n  };\n  \n  const StatusIndicator = () => {\n    if (checking) {\n      return (\n        <div className=\"flex items-center space-x-2 text-xs\">\n          <Circle className=\"h-3 w-3 animate-pulse text-muted-foreground\" />\n          <span className=\"text-muted-foreground\">Checking...</span>\n        </div>\n      );\n    }\n    \n    if (!versionStatus) return null;\n    \n    const statusContent = (\n      <Button\n        variant=\"ghost\"\n        size=\"sm\"\n        className=\"h-auto py-1 px-2 hover:bg-accent\"\n        onClick={onSettingsClick}\n      >\n        <div className=\"flex items-center space-x-2 text-xs\">\n          <Circle\n            className={cn(\n              \"h-3 w-3\",\n              versionStatus.is_installed \n                ? \"fill-green-500 text-green-500\" \n                : \"fill-red-500 text-red-500\"\n            )}\n          />\n          <span>\n            {versionStatus.is_installed && versionStatus.version\n              ? `Claude Code v${versionStatus.version}`\n              : \"Claude Code\"}\n          </span>\n        </div>\n      </Button>\n    );\n    \n    if (!versionStatus.is_installed) {\n      return (\n        <Popover\n          trigger={statusContent}\n          content={\n            <div className=\"space-y-3 max-w-xs\">\n              <p className=\"text-sm font-medium\">Claude Code not found</p>\n              <div className=\"rounded-md bg-muted p-3\">\n                <pre className=\"text-xs font-mono whitespace-pre-wrap\">\n                  {versionStatus.output}\n                </pre>\n              </div>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                className=\"w-full\"\n                onClick={onSettingsClick}\n              >\n                Select Claude Installation\n              </Button>\n              <a\n                href=\"https://www.anthropic.com/claude-code\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"flex items-center space-x-1 text-xs text-primary hover:underline\"\n              >\n                <span>Install Claude Code</span>\n                <ExternalLink className=\"h-3 w-3\" />\n              </a>\n            </div>\n          }\n          align=\"start\"\n        />\n      );\n    }\n    \n    return statusContent;\n  };\n  \n  return (\n    <motion.div\n      initial={{ opacity: 0, y: -20 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.3 }}\n      className={cn(\n        \"flex items-center justify-between px-4 py-3 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\",\n        className\n      )}\n    >\n      {/* Status Indicator */}\n      <StatusIndicator />\n      \n      {/* Spacer - Navigation moved to titlebar */}\n      <div></div>\n    </motion.div>\n  );\n}; "
  },
  {
    "path": "src/components/UsageDashboard.original.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { api, type UsageStats, type ProjectUsage } from \"@/lib/api\";\nimport { \n  Calendar, \n  Filter,\n  Loader2,\n  Briefcase\n} from \"lucide-react\";\n\ninterface UsageDashboardProps {\n  /**\n   * Callback when back button is clicked\n   */\n  onBack: () => void;\n}\n\n/**\n * UsageDashboard component - Displays Claude API usage statistics and costs\n * \n * @example\n * <UsageDashboard onBack={() => setView('welcome')} />\n */\nexport const UsageDashboard: React.FC<UsageDashboardProps> = ({ }) => {\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [stats, setStats] = useState<UsageStats | null>(null);\n  const [sessionStats, setSessionStats] = useState<ProjectUsage[] | null>(null);\n  const [selectedDateRange, setSelectedDateRange] = useState<\"all\" | \"7d\" | \"30d\">(\"7d\");\n  const [activeTab, setActiveTab] = useState(\"overview\");\n\n  useEffect(() => {\n    loadUsageStats();\n  }, [selectedDateRange]);\n\n  const loadUsageStats = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n\n      let statsData: UsageStats;\n      let sessionData: ProjectUsage[];\n      \n      if (selectedDateRange === \"all\") {\n        statsData = await api.getUsageStats();\n        sessionData = await api.getSessionStats();\n      } else {\n        const endDate = new Date();\n        const startDate = new Date();\n        const days = selectedDateRange === \"7d\" ? 7 : 30;\n        startDate.setDate(startDate.getDate() - days);\n        \n        const formatDateForApi = (date: Date) => {\n            const year = date.getFullYear();\n            const month = String(date.getMonth() + 1).padStart(2, '0');\n            const day = String(date.getDate()).padStart(2, '0');\n            return `${year}${month}${day}`;\n        }\n\n        statsData = await api.getUsageByDateRange(\n          startDate.toISOString(),\n          endDate.toISOString()\n        );\n        sessionData = await api.getSessionStats(\n            formatDateForApi(startDate),\n            formatDateForApi(endDate),\n            'desc'\n        );\n      }\n      \n      setStats(statsData);\n      setSessionStats(sessionData);\n    } catch (err) {\n      console.error(\"Failed to load usage stats:\", err);\n      setError(\"Failed to load usage statistics. Please try again.\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const formatCurrency = (amount: number): string => {\n    return new Intl.NumberFormat('en-US', {\n      style: 'currency',\n      currency: 'USD',\n      minimumFractionDigits: 2,\n      maximumFractionDigits: 4\n    }).format(amount);\n  };\n\n  const formatNumber = (num: number): string => {\n    return new Intl.NumberFormat('en-US').format(num);\n  };\n\n  const formatTokens = (num: number): string => {\n    if (num >= 1_000_000) {\n      return `${(num / 1_000_000).toFixed(2)}M`;\n    } else if (num >= 1_000) {\n      return `${(num / 1_000).toFixed(1)}K`;\n    }\n    return formatNumber(num);\n  };\n\n  const getModelDisplayName = (model: string): string => {\n    const modelMap: Record<string, string> = {\n      \"claude-4-opus\": \"Opus 4\",\n      \"claude-4-sonnet\": \"Sonnet 4\",\n      \"claude-3.5-sonnet\": \"Sonnet 3.5\",\n      \"claude-3-opus\": \"Opus 3\",\n    };\n    return modelMap[model] || model;\n  };\n\n  // Helper for model color (unused in this original version)\n\n  return (\n    <div className=\"h-full overflow-y-auto\">\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <h1 className=\"text-heading-1\">Usage Dashboard</h1>\n              <p className=\"mt-1 text-body-small text-muted-foreground\">\n                Track your Claude Code usage and costs\n              </p>\n            </div>\n            {/* Date Range Filter */}\n            <div className=\"flex items-center space-x-2\">\n              <Filter className=\"h-4 w-4 text-muted-foreground\" />\n              <div className=\"flex space-x-1\">\n                {([\"7d\", \"30d\", \"all\"] as const).map((range) => (\n                  <Button\n                    key={range}\n                    variant={selectedDateRange === range ? \"default\" : \"outline\"}\n                    size=\"sm\"\n                    onClick={() => setSelectedDateRange(range)}\n                  >\n                    {range === \"all\" ? \"All Time\" : range === \"7d\" ? \"Last 7 Days\" : \"Last 30 Days\"}\n                  </Button>\n                ))}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Content */}\n        <div className=\"flex-1 overflow-y-auto p-6\">\n          {loading ? (\n            <div className=\"flex items-center justify-center h-64\">\n              <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n            </div>\n          ) : error ? (\n            <motion.div\n              initial={{ opacity: 0, y: -10 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -10 }}\n              className=\"mb-4 p-3 rounded-lg bg-destructive/10 border border-destructive/50 text-body-small text-destructive\">\n              {error}\n              <Button onClick={loadUsageStats} size=\"sm\" className=\"ml-4\">\n                Try Again\n              </Button>\n            </motion.div>\n          ) : stats ? (\n            <div className=\"space-y-6\">\n            {/* Summary Cards */}\n            <div className=\"grid grid-cols-1 md:grid-cols-4 gap-4\">\n              {/* Total Cost Card */}\n              <Card className=\"p-4 shimmer-hover\">\n                <div>\n                  <p className=\"text-caption text-muted-foreground\">Total Cost</p>\n                  <p className=\"text-display-2 mt-1\">\n                    {formatCurrency(stats.total_cost)}\n                  </p>\n                </div>\n              </Card>\n\n              {/* Total Sessions Card */}\n              <Card className=\"p-4 shimmer-hover\">\n                <div>\n                  <p className=\"text-caption text-muted-foreground\">Total Sessions</p>\n                  <p className=\"text-display-2 mt-1\">\n                    {formatNumber(stats.total_sessions)}\n                  </p>\n                </div>\n              </Card>\n\n              {/* Total Tokens Card */}\n              <Card className=\"p-4 shimmer-hover\">\n                <div>\n                  <p className=\"text-caption text-muted-foreground\">Total Tokens</p>\n                  <p className=\"text-display-2 mt-1\">\n                    {formatTokens(stats.total_tokens)}\n                  </p>\n                </div>\n              </Card>\n\n              {/* Average Cost per Session Card */}\n              <Card className=\"p-4 shimmer-hover\">\n                <div>\n                  <p className=\"text-caption text-muted-foreground\">Avg Cost/Session</p>\n                  <p className=\"text-display-2 mt-1\">\n                    {formatCurrency(\n                      stats.total_sessions > 0 \n                        ? stats.total_cost / stats.total_sessions \n                        : 0\n                    )}\n                  </p>\n                </div>\n              </Card>\n            </div>\n\n            {/* Tabs for different views */}\n            <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n              <TabsList className=\"grid grid-cols-5 w-full mb-6 h-auto p-1\">\n                <TabsTrigger value=\"overview\" className=\"py-2.5 px-3\">Overview</TabsTrigger>\n                <TabsTrigger value=\"models\" className=\"py-2.5 px-3\">By Model</TabsTrigger>\n                <TabsTrigger value=\"projects\" className=\"py-2.5 px-3\">By Project</TabsTrigger>\n                <TabsTrigger value=\"sessions\" className=\"py-2.5 px-3\">By Session</TabsTrigger>\n                <TabsTrigger value=\"timeline\" className=\"py-2.5 px-3\">Timeline</TabsTrigger>\n              </TabsList>\n\n              {/* Overview Tab */}\n              <TabsContent value=\"overview\" className=\"space-y-6 mt-6\">\n                <Card className=\"p-6\">\n                  <h3 className=\"text-label mb-4\">Token Breakdown</h3>\n                  <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n                    <div>\n                      <p className=\"text-caption text-muted-foreground\">Input Tokens</p>\n                      <p className=\"text-heading-4\">{formatTokens(stats.total_input_tokens)}</p>\n                    </div>\n                    <div>\n                      <p className=\"text-caption text-muted-foreground\">Output Tokens</p>\n                      <p className=\"text-heading-4\">{formatTokens(stats.total_output_tokens)}</p>\n                    </div>\n                    <div>\n                      <p className=\"text-caption text-muted-foreground\">Cache Write</p>\n                      <p className=\"text-heading-4\">{formatTokens(stats.total_cache_creation_tokens)}</p>\n                    </div>\n                    <div>\n                      <p className=\"text-caption text-muted-foreground\">Cache Read</p>\n                      <p className=\"text-heading-4\">{formatTokens(stats.total_cache_read_tokens)}</p>\n                    </div>\n                  </div>\n                </Card>\n\n                {/* Quick Stats */}\n                <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                  <Card className=\"p-6\">\n                    <h3 className=\"text-label mb-4\">Most Used Models</h3>\n                    <div className=\"space-y-3\">\n                      {stats.by_model.slice(0, 3).map((model) => (\n                        <div key={model.model} className=\"flex items-center justify-between\">\n                          <div className=\"flex items-center space-x-2\">\n                            <Badge variant=\"outline\" className=\"text-caption\">\n                              {getModelDisplayName(model.model)}\n                            </Badge>\n                            <span className=\"text-caption text-muted-foreground\">\n                              {model.session_count} sessions\n                            </span>\n                          </div>\n                          <span className=\"text-body-small font-medium\">\n                            {formatCurrency(model.total_cost)}\n                          </span>\n                        </div>\n                      ))}\n                    </div>\n                  </Card>\n\n                  <Card className=\"p-6\">\n                    <h3 className=\"text-label mb-4\">Top Projects</h3>\n                    <div className=\"space-y-3\">\n                      {stats.by_project.slice(0, 3).map((project) => (\n                        <div key={project.project_path} className=\"flex items-center justify-between\">\n                          <div className=\"flex flex-col\">\n                            <span className=\"text-body-small font-medium truncate max-w-[200px]\" title={project.project_path}>\n                              {project.project_path}\n                            </span>\n                            <span className=\"text-caption text-muted-foreground\">\n                              {project.session_count} sessions\n                            </span>\n                          </div>\n                          <span className=\"text-body-small font-medium\">\n                            {formatCurrency(project.total_cost)}\n                          </span>\n                        </div>\n                      ))}\n                    </div>\n                  </Card>\n                </div>\n              </TabsContent>\n\n              {/* Models Tab */}\n              <TabsContent value=\"models\" className=\"space-y-6 mt-6\">\n                <Card className=\"p-6\">\n                  <h3 className=\"text-sm font-semibold mb-4\">Usage by Model</h3>\n                  <div className=\"space-y-4\">\n                    {stats.by_model.map((model) => (\n                      <div key={model.model} className=\"space-y-2\">\n                        <div className=\"flex items-center justify-between\">\n                          <div className=\"flex items-center space-x-3\">\n                            <Badge \n                              variant=\"outline\" \n                              className=\"text-xs\"\n                            >\n                              {getModelDisplayName(model.model)}\n                            </Badge>\n                            <span className=\"text-sm text-muted-foreground\">\n                              {model.session_count} sessions\n                            </span>\n                          </div>\n                          <span className=\"text-sm font-semibold\">\n                            {formatCurrency(model.total_cost)}\n                          </span>\n                        </div>\n                        <div className=\"grid grid-cols-4 gap-2 text-xs\">\n                          <div>\n                            <span className=\"text-muted-foreground\">Input: </span>\n                            <span className=\"font-medium\">{formatTokens(model.input_tokens)}</span>\n                          </div>\n                          <div>\n                            <span className=\"text-muted-foreground\">Output: </span>\n                            <span className=\"font-medium\">{formatTokens(model.output_tokens)}</span>\n                          </div>\n                          <div>\n                            <span className=\"text-muted-foreground\">Cache W: </span>\n                            <span className=\"font-medium\">{formatTokens(model.cache_creation_tokens)}</span>\n                          </div>\n                          <div>\n                            <span className=\"text-muted-foreground\">Cache R: </span>\n                            <span className=\"font-medium\">{formatTokens(model.cache_read_tokens)}</span>\n                          </div>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </Card>\n              </TabsContent>\n\n              {/* Projects Tab */}\n              <TabsContent value=\"projects\" className=\"space-y-6 mt-6\">\n                <Card className=\"p-6\">\n                  <h3 className=\"text-sm font-semibold mb-4\">Usage by Project</h3>\n                  <div className=\"space-y-3\">\n                    {stats.by_project.map((project) => (\n                      <div key={project.project_path} className=\"flex items-center justify-between py-2 border-b border-border last:border-0\">\n                        <div className=\"flex flex-col truncate\">\n                          <span className=\"text-sm font-medium truncate\" title={project.project_path}>\n                            {project.project_path}\n                          </span>\n                          <div className=\"flex items-center space-x-3 mt-1\">\n                            <span className=\"text-caption text-muted-foreground\">\n                              {project.session_count} sessions\n                            </span>\n                            <span className=\"text-caption text-muted-foreground\">\n                              {formatTokens(project.total_tokens)} tokens\n                            </span>\n                          </div>\n                        </div>\n                        <div className=\"text-right\">\n                          <p className=\"text-sm font-semibold\">{formatCurrency(project.total_cost)}</p>\n                          <p className=\"text-xs text-muted-foreground\">\n                            {formatCurrency(project.total_cost / project.session_count)}/session\n                          </p>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </Card>\n              </TabsContent>\n\n              {/* Sessions Tab */}\n              <TabsContent value=\"sessions\" className=\"space-y-6 mt-6\">\n                  <Card className=\"p-6\">\n                      <h3 className=\"text-sm font-semibold mb-4\">Usage by Session</h3>\n                      <div className=\"space-y-3\">\n                          {sessionStats?.map((session) => (\n                              <div key={`${session.project_path}-${session.project_name}`} className=\"flex items-center justify-between py-2 border-b border-border last:border-0\">\n                                  <div className=\"flex flex-col\">\n                                      <div className=\"flex items-center space-x-2\">\n                                        <Briefcase className=\"h-4 w-4 text-muted-foreground\" />\n                                        <span className=\"text-xs font-mono text-muted-foreground truncate max-w-[200px]\" title={session.project_path}>\n                                            {session.project_path.split('/').slice(-2).join('/')}\n                                        </span>\n                                      </div>\n                                      <span className=\"text-sm font-medium mt-1\">\n                                          {session.project_name}\n                                      </span>\n                                  </div>\n                                  <div className=\"text-right\">\n                                      <p className=\"text-sm font-semibold\">{formatCurrency(session.total_cost)}</p>\n                                      <p className=\"text-xs text-muted-foreground\">\n                                          {new Date(session.last_used).toLocaleDateString()}\n                                      </p>\n                                  </div>\n                              </div>\n                          ))}\n                      </div>\n                  </Card>\n              </TabsContent>\n\n              {/* Timeline Tab */}\n              <TabsContent value=\"timeline\" className=\"space-y-6 mt-6\">\n                <Card className=\"p-6\">\n                  <h3 className=\"text-sm font-semibold mb-6 flex items-center space-x-2\">\n                    <Calendar className=\"h-4 w-4\" />\n                    <span>Daily Usage</span>\n                  </h3>\n                  {stats.by_date.length > 0 ? (() => {\n                    const maxCost = Math.max(...stats.by_date.map(d => d.total_cost), 0);\n                    const halfMaxCost = maxCost / 2;\n\n                    return (\n                      <div className=\"relative pl-8 pr-4\">\n                        {/* Y-axis labels */}\n                        <div className=\"absolute left-0 top-0 bottom-8 flex flex-col justify-between text-xs text-muted-foreground\">\n                          <span>{formatCurrency(maxCost)}</span>\n                          <span>{formatCurrency(halfMaxCost)}</span>\n                          <span>{formatCurrency(0)}</span>\n                        </div>\n                        \n                        {/* Chart container */}\n                        <div className=\"flex items-end space-x-2 h-64 border-l border-b border-border pl-4\">\n                          {stats.by_date.slice().reverse().map((day) => {\n                            const heightPercent = maxCost > 0 ? (day.total_cost / maxCost) * 100 : 0;\n                            const date = new Date(day.date.replace(/-/g, '/'));\n                            const formattedDate = date.toLocaleDateString('en-US', {\n                              weekday: 'short',\n                              month: 'short',\n                              day: 'numeric'\n                            });\n                            \n                            return (\n                              <div key={day.date} className=\"flex-1 h-full flex flex-col items-center justify-end group relative\">\n                                {/* Tooltip */}\n                                <div className=\"absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10\">\n                                  <div className=\"bg-background border border-border rounded-lg shadow-lg p-3 whitespace-nowrap\">\n                                    <p className=\"text-sm font-semibold\">{formattedDate}</p>\n                                    <p className=\"text-sm text-muted-foreground mt-1\">\n                                      Cost: {formatCurrency(day.total_cost)}\n                                    </p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                      {formatTokens(day.total_tokens)} tokens\n                                    </p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                      {day.models_used.length} model{day.models_used.length !== 1 ? 's' : ''}\n                                    </p>\n                                  </div>\n                                  <div className=\"absolute top-full left-1/2 transform -translate-x-1/2 -mt-1\">\n                                    <div className=\"border-4 border-transparent border-t-border\"></div>\n                                  </div>\n                                </div>\n                                \n                                {/* Bar */}\n                                <div \n                                  className=\"w-full bg-[#d97757] hover:opacity-80 transition-opacity rounded-t cursor-pointer\"\n                                  style={{ height: `${heightPercent}%` }}\n                                />\n                                \n                                {/* X-axis label – absolutely positioned below the bar so it doesn't affect bar height */}\n                                <div\n                                  className=\"absolute left-1/2 top-full mt-1 -translate-x-1/2 text-xs text-muted-foreground -rotate-45 origin-top-left whitespace-nowrap pointer-events-none\"\n                                >\n                                  {date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n                                </div>\n                              </div>\n                            );\n                          })}\n                        </div>\n                        \n                        {/* X-axis label */}\n                        <div className=\"mt-8 text-center text-xs text-muted-foreground\">\n                          Daily Usage Over Time\n                        </div>\n                      </div>\n                    )\n                  })() : (\n                    <div className=\"text-center py-8 text-sm text-muted-foreground\">\n                      No usage data available for the selected period\n                    </div>\n                  )}\n                </Card>\n              </TabsContent>\n            </Tabs>\n          </div>\n        ) : null}\n        </div>\n      </div>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/UsageDashboard.tsx",
    "content": "import React, { useState, useEffect, useMemo, useCallback } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { api, type UsageStats, type ProjectUsage } from \"@/lib/api\";\nimport { \n  Calendar, \n  Filter,\n  Loader2,\n  Briefcase,\n  ChevronLeft,\n  ChevronRight\n} from \"lucide-react\";\n\ninterface UsageDashboardProps {\n  /**\n   * Callback when back button is clicked\n   */\n  onBack: () => void;\n}\n\n// Cache for storing fetched data\nconst dataCache = new Map<string, { data: any; timestamp: number }>();\nconst CACHE_DURATION = 10 * 60 * 1000; // 10 minutes cache - increased for better performance\n\n/**\n * Optimized UsageDashboard component with caching and progressive loading\n */\nexport const UsageDashboard: React.FC<UsageDashboardProps> = ({ }) => {\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [stats, setStats] = useState<UsageStats | null>(null);\n  const [sessionStats, setSessionStats] = useState<ProjectUsage[] | null>(null);\n  const [selectedDateRange, setSelectedDateRange] = useState<\"all\" | \"7d\" | \"30d\">(\"7d\");\n  const [activeTab, setActiveTab] = useState(\"overview\");\n  const [hasLoadedTabs, setHasLoadedTabs] = useState<Set<string>>(new Set([\"overview\"]));\n  \n  // Pagination states\n  const [projectsPage, setProjectsPage] = useState(1);\n  const [sessionsPage, setSessionsPage] = useState(1);\n  const ITEMS_PER_PAGE = 10;\n\n  // Memoized formatters to prevent recreation on each render\n  const formatCurrency = useMemo(() => (amount: number): string => {\n    return new Intl.NumberFormat('en-US', {\n      style: 'currency',\n      currency: 'USD',\n      minimumFractionDigits: 2,\n      maximumFractionDigits: 2\n    }).format(amount);\n  }, []);\n\n  const formatNumber = useMemo(() => (num: number): string => {\n    return new Intl.NumberFormat('en-US').format(num);\n  }, []);\n\n  const formatTokens = useMemo(() => (num: number): string => {\n    if (num >= 1_000_000) {\n      return `${(num / 1_000_000).toFixed(2)}M`;\n    } else if (num >= 1_000) {\n      return `${(num / 1_000).toFixed(1)}K`;\n    }\n    return formatNumber(num);\n  }, [formatNumber]);\n\n  const getModelDisplayName = useCallback((model: string): string => {\n    const modelMap: Record<string, string> = {\n      \"claude-4-opus\": \"Opus 4\",\n      \"claude-4-sonnet\": \"Sonnet 4\",\n      \"claude-3.5-sonnet\": \"Sonnet 3.5\",\n      \"claude-3-opus\": \"Opus 3\",\n    };\n    return modelMap[model] || model;\n  }, []);\n\n  // Function to get cached data or null\n  const getCachedData = useCallback((key: string) => {\n    const cached = dataCache.get(key);\n    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {\n      return cached.data;\n    }\n    return null;\n  }, []);\n\n  // Function to set cached data\n  const setCachedData = useCallback((key: string, data: any) => {\n    dataCache.set(key, { data, timestamp: Date.now() });\n  }, []);\n\n  const loadUsageStats = useCallback(async () => {\n    const cacheKey = `usage-${selectedDateRange}`;\n    \n    // Check cache first\n    const cachedStats = getCachedData(`${cacheKey}-stats`);\n    const cachedSessions = getCachedData(`${cacheKey}-sessions`);\n    \n    if (cachedStats && cachedSessions) {\n      setStats(cachedStats);\n      setSessionStats(cachedSessions);\n      setLoading(false);\n      return;\n    }\n\n    try {\n      // Don't show loading spinner if we have cached data for a different range\n      if (!stats && !sessionStats) {\n        setLoading(true);\n      }\n      setError(null);\n\n      let statsData: UsageStats;\n      let sessionData: ProjectUsage[] = [];\n      \n      if (selectedDateRange === \"all\") {\n        // Fetch both in parallel for all time\n        const [statsResult, sessionResult] = await Promise.all([\n          api.getUsageStats(),\n          api.getSessionStats()\n        ]);\n        statsData = statsResult;\n        sessionData = sessionResult;\n      } else {\n        const endDate = new Date();\n        const startDate = new Date();\n        const days = selectedDateRange === \"7d\" ? 7 : 30;\n        startDate.setDate(startDate.getDate() - days);\n        \n        const formatDateForApi = (date: Date) => {\n          const year = date.getFullYear();\n          const month = String(date.getMonth() + 1).padStart(2, '0');\n          const day = String(date.getDate()).padStart(2, '0');\n          return `${year}${month}${day}`;\n        }\n\n        // Fetch both in parallel for better performance\n        const [statsResult, sessionResult] = await Promise.all([\n          api.getUsageByDateRange(\n            startDate.toISOString(),\n            endDate.toISOString()\n          ),\n          api.getSessionStats(\n            formatDateForApi(startDate),\n            formatDateForApi(endDate),\n            'desc'\n          )\n        ]);\n        \n        statsData = statsResult;\n        sessionData = sessionResult;\n      }\n      \n      // Update state\n      setStats(statsData);\n      setSessionStats(sessionData);\n      \n      // Cache the data\n      setCachedData(`${cacheKey}-stats`, statsData);\n      setCachedData(`${cacheKey}-sessions`, sessionData);\n    } catch (err: any) {\n      console.error(\"Failed to load usage stats:\", err);\n      setError(\"Failed to load usage statistics. Please try again.\");\n    } finally {\n      setLoading(false);\n    }\n  }, [selectedDateRange, getCachedData, setCachedData, stats, sessionStats]);\n\n  // Load data on mount and when date range changes\n  useEffect(() => {\n    // Reset pagination when date range changes\n    setProjectsPage(1);\n    setSessionsPage(1);\n    loadUsageStats();\n  }, [loadUsageStats])\n\n  // Preload adjacent tabs when idle\n  useEffect(() => {\n    if (!stats || loading) return;\n    \n    const tabOrder = [\"overview\", \"models\", \"projects\", \"sessions\", \"timeline\"];\n    const currentIndex = tabOrder.indexOf(activeTab);\n    \n    // Use requestIdleCallback if available, otherwise setTimeout\n    const schedulePreload = (callback: () => void) => {\n      if ('requestIdleCallback' in window) {\n        requestIdleCallback(callback, { timeout: 2000 });\n      } else {\n        setTimeout(callback, 100);\n      }\n    };\n    \n    // Preload adjacent tabs\n    schedulePreload(() => {\n      if (currentIndex > 0) {\n        setHasLoadedTabs(prev => new Set([...prev, tabOrder[currentIndex - 1]]));\n      }\n      if (currentIndex < tabOrder.length - 1) {\n        setHasLoadedTabs(prev => new Set([...prev, tabOrder[currentIndex + 1]]));\n      }\n    });\n  }, [activeTab, stats, loading])\n\n  // Memoize expensive computations\n  const summaryCards = useMemo(() => {\n    if (!stats) return null;\n    \n    return (\n      <div className=\"grid grid-cols-1 md:grid-cols-4 gap-4\">\n        <Card className=\"p-4 shimmer-hover\">\n          <div>\n            <p className=\"text-caption text-muted-foreground\">Total Cost</p>\n            <p className=\"text-display-2 mt-1\">\n              {formatCurrency(stats.total_cost)}\n            </p>\n          </div>\n        </Card>\n\n        <Card className=\"p-4 shimmer-hover\">\n          <div>\n            <p className=\"text-caption text-muted-foreground\">Total Sessions</p>\n            <p className=\"text-display-2 mt-1\">\n              {formatNumber(stats.total_sessions)}\n            </p>\n          </div>\n        </Card>\n\n        <Card className=\"p-4 shimmer-hover\">\n          <div>\n            <p className=\"text-caption text-muted-foreground\">Total Tokens</p>\n            <p className=\"text-display-2 mt-1\">\n              {formatTokens(stats.total_tokens)}\n            </p>\n          </div>\n        </Card>\n\n        <Card className=\"p-4 shimmer-hover\">\n          <div>\n            <p className=\"text-caption text-muted-foreground\">Avg Cost/Session</p>\n            <p className=\"text-display-2 mt-1\">\n              {formatCurrency(\n                stats.total_sessions > 0 \n                  ? stats.total_cost / stats.total_sessions \n                  : 0\n              )}\n            </p>\n          </div>\n        </Card>\n      </div>\n    );\n  }, [stats, formatCurrency, formatNumber, formatTokens]);\n\n  // Memoize the most used models section\n  const mostUsedModels = useMemo(() => {\n    if (!stats?.by_model) return null;\n    \n    return stats.by_model.slice(0, 3).map((model) => (\n      <div key={model.model} className=\"flex items-center justify-between\">\n        <div className=\"flex items-center space-x-2\">\n          <Badge variant=\"outline\" className=\"text-caption\">\n            {getModelDisplayName(model.model)}\n          </Badge>\n          <span className=\"text-caption text-muted-foreground\">\n            {model.session_count} sessions\n          </span>\n        </div>\n        <span className=\"text-body-small font-medium\">\n          {formatCurrency(model.total_cost)}\n        </span>\n      </div>\n    ));\n  }, [stats, formatCurrency, getModelDisplayName]);\n\n  // Memoize top projects section\n  const topProjects = useMemo(() => {\n    if (!stats?.by_project) return null;\n    \n    return stats.by_project.slice(0, 3).map((project) => (\n      <div key={project.project_path} className=\"flex items-center justify-between\">\n        <div className=\"flex flex-col\">\n          <span className=\"text-body-small font-medium truncate max-w-[200px]\" title={project.project_path}>\n            {project.project_path}\n          </span>\n          <span className=\"text-caption text-muted-foreground\">\n            {project.session_count} sessions\n          </span>\n        </div>\n        <span className=\"text-body-small font-medium\">\n          {formatCurrency(project.total_cost)}\n        </span>\n      </div>\n    ));\n  }, [stats, formatCurrency]);\n\n  // Memoize timeline chart data\n  const timelineChartData = useMemo(() => {\n    if (!stats?.by_date || stats.by_date.length === 0) return null;\n    \n    const maxCost = Math.max(...stats.by_date.map(d => d.total_cost), 0);\n    const halfMaxCost = maxCost / 2;\n    const reversedData = stats.by_date.slice().reverse();\n    \n    return {\n      maxCost,\n      halfMaxCost,\n      reversedData,\n      bars: reversedData.map(day => ({\n        ...day,\n        heightPercent: maxCost > 0 ? (day.total_cost / maxCost) * 100 : 0,\n        date: new Date(day.date.replace(/-/g, '/')),\n      }))\n    };\n  }, [stats?.by_date]);\n\n  return (\n    <div className=\"h-full overflow-y-auto\">\n      <div className=\"max-w-6xl mx-auto flex flex-col h-full\">\n        {/* Header */}\n        <div className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <h1 className=\"text-heading-1\">Usage Dashboard</h1>\n              <p className=\"mt-1 text-body-small text-muted-foreground\">\n                Track your Claude Code usage and costs\n              </p>\n            </div>\n            {/* Date Range Filter */}\n            <div className=\"flex items-center space-x-2\">\n              <Filter className=\"h-4 w-4 text-muted-foreground\" />\n              <div className=\"flex space-x-1\">\n                {([\"7d\", \"30d\", \"all\"] as const).map((range) => (\n                  <Button\n                    key={range}\n                    variant={selectedDateRange === range ? \"default\" : \"outline\"}\n                    size=\"sm\"\n                    onClick={() => setSelectedDateRange(range)}\n                    disabled={loading}\n                  >\n                    {range === \"all\" ? \"All Time\" : range === \"7d\" ? \"Last 7 Days\" : \"Last 30 Days\"}\n                  </Button>\n                ))}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Content */}\n        <div className=\"flex-1 overflow-y-auto p-6\">\n          {loading ? (\n            <div className=\"flex items-center justify-center h-64\">\n              <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n            </div>\n          ) : error ? (\n            <div className=\"mb-4 p-3 rounded-lg bg-destructive/10 border border-destructive/50 text-body-small text-destructive\">\n              {error}\n              <Button onClick={() => loadUsageStats()} size=\"sm\" className=\"ml-4\">\n                Try Again\n              </Button>\n            </div>\n          ) : stats ? (\n            <div className=\"space-y-6\">\n              {/* Summary Cards */}\n              {summaryCards}\n\n              {/* Tabs for different views */}\n              <Tabs value={activeTab} onValueChange={(value) => {\n                setActiveTab(value);\n                setHasLoadedTabs(prev => new Set([...prev, value]));\n              }} className=\"w-full\">\n                <TabsList className=\"grid grid-cols-5 w-full mb-6 h-auto p-1\">\n                  <TabsTrigger value=\"overview\" className=\"py-2.5 px-3\">Overview</TabsTrigger>\n                  <TabsTrigger value=\"models\" className=\"py-2.5 px-3\">By Model</TabsTrigger>\n                  <TabsTrigger value=\"projects\" className=\"py-2.5 px-3\">By Project</TabsTrigger>\n                  <TabsTrigger value=\"sessions\" className=\"py-2.5 px-3\">By Session</TabsTrigger>\n                  <TabsTrigger value=\"timeline\" className=\"py-2.5 px-3\">Timeline</TabsTrigger>\n                </TabsList>\n\n                {/* Overview Tab */}\n                <TabsContent value=\"overview\" className=\"space-y-6 mt-6\">\n                  <Card className=\"p-6\">\n                    <h3 className=\"text-label mb-4\">Token Breakdown</h3>\n                    <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n                      <div>\n                        <p className=\"text-caption text-muted-foreground\">Input Tokens</p>\n                        <p className=\"text-heading-4\">{formatTokens(stats.total_input_tokens)}</p>\n                      </div>\n                      <div>\n                        <p className=\"text-caption text-muted-foreground\">Output Tokens</p>\n                        <p className=\"text-heading-4\">{formatTokens(stats.total_output_tokens)}</p>\n                      </div>\n                      <div>\n                        <p className=\"text-caption text-muted-foreground\">Cache Write</p>\n                        <p className=\"text-heading-4\">{formatTokens(stats.total_cache_creation_tokens)}</p>\n                      </div>\n                      <div>\n                        <p className=\"text-caption text-muted-foreground\">Cache Read</p>\n                        <p className=\"text-heading-4\">{formatTokens(stats.total_cache_read_tokens)}</p>\n                      </div>\n                    </div>\n                  </Card>\n\n                  {/* Quick Stats */}\n                  <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                    <Card className=\"p-6\">\n                      <h3 className=\"text-label mb-4\">Most Used Models</h3>\n                      <div className=\"space-y-3\">\n                        {mostUsedModels}\n                      </div>\n                    </Card>\n\n                    <Card className=\"p-6\">\n                      <h3 className=\"text-label mb-4\">Top Projects</h3>\n                      <div className=\"space-y-3\">\n                        {topProjects}\n                      </div>\n                    </Card>\n                  </div>\n                </TabsContent>\n\n                {/* Models Tab - Lazy render and cache */}\n                <TabsContent value=\"models\" className=\"space-y-6 mt-6\">\n                  {hasLoadedTabs.has(\"models\") && stats && (\n                    <div style={{ display: activeTab === \"models\" ? \"block\" : \"none\" }}>\n                      <Card className=\"p-6\">\n                        <h3 className=\"text-sm font-semibold mb-4\">Usage by Model</h3>\n                        <div className=\"space-y-4\">\n                          {stats.by_model.map((model) => (\n                          <div key={model.model} className=\"space-y-2\">\n                            <div className=\"flex items-center justify-between\">\n                              <div className=\"flex items-center space-x-3\">\n                                <Badge \n                                  variant=\"outline\" \n                                  className=\"text-xs\"\n                                >\n                                  {getModelDisplayName(model.model)}\n                                </Badge>\n                                <span className=\"text-sm text-muted-foreground\">\n                                  {model.session_count} sessions\n                                </span>\n                              </div>\n                              <span className=\"text-sm font-semibold\">\n                                {formatCurrency(model.total_cost)}\n                              </span>\n                            </div>\n                            <div className=\"grid grid-cols-4 gap-2 text-xs\">\n                              <div>\n                                <span className=\"text-muted-foreground\">Input: </span>\n                                <span className=\"font-medium\">{formatTokens(model.input_tokens)}</span>\n                              </div>\n                              <div>\n                                <span className=\"text-muted-foreground\">Output: </span>\n                                <span className=\"font-medium\">{formatTokens(model.output_tokens)}</span>\n                              </div>\n                              <div>\n                                <span className=\"text-muted-foreground\">Cache W: </span>\n                                <span className=\"font-medium\">{formatTokens(model.cache_creation_tokens)}</span>\n                              </div>\n                              <div>\n                                <span className=\"text-muted-foreground\">Cache R: </span>\n                                <span className=\"font-medium\">{formatTokens(model.cache_read_tokens)}</span>\n                              </div>\n                            </div>\n                            </div>\n                          ))}\n                        </div>\n                      </Card>\n                    </div>\n                  )}\n                </TabsContent>\n\n                {/* Projects Tab - Lazy render and cache */}\n                <TabsContent value=\"projects\" className=\"space-y-6 mt-6\">\n                  {hasLoadedTabs.has(\"projects\") && stats && (\n                    <div style={{ display: activeTab === \"projects\" ? \"block\" : \"none\" }}>\n                      <Card className=\"p-6\">\n                      <div className=\"flex items-center justify-between mb-4\">\n                        <h3 className=\"text-sm font-semibold\">Usage by Project</h3>\n                        <span className=\"text-xs text-muted-foreground\">\n                          {stats.by_project.length} total projects\n                        </span>\n                      </div>\n                      <div className=\"space-y-3\">\n                        {(() => {\n                          const startIndex = (projectsPage - 1) * ITEMS_PER_PAGE;\n                          const endIndex = startIndex + ITEMS_PER_PAGE;\n                          const paginatedProjects = stats.by_project.slice(startIndex, endIndex);\n                          const totalPages = Math.ceil(stats.by_project.length / ITEMS_PER_PAGE);\n                          \n                          return (\n                            <>\n                              {paginatedProjects.map((project) => (\n                                <div key={project.project_path} className=\"flex items-center justify-between py-2 border-b border-border last:border-0\">\n                                  <div className=\"flex flex-col truncate\">\n                                    <span className=\"text-sm font-medium truncate\" title={project.project_path}>\n                                      {project.project_path}\n                                    </span>\n                                    <div className=\"flex items-center space-x-3 mt-1\">\n                                      <span className=\"text-caption text-muted-foreground\">\n                                        {project.session_count} sessions\n                                      </span>\n                                      <span className=\"text-caption text-muted-foreground\">\n                                        {formatTokens(project.total_tokens)} tokens\n                                      </span>\n                                    </div>\n                                  </div>\n                                  <div className=\"text-right\">\n                                    <p className=\"text-sm font-semibold\">{formatCurrency(project.total_cost)}</p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                      {formatCurrency(project.total_cost / project.session_count)}/session\n                                    </p>\n                                  </div>\n                                </div>\n                              ))}\n                              \n                              {/* Pagination Controls */}\n                              {totalPages > 1 && (\n                                <div className=\"flex items-center justify-between pt-4\">\n                                  <span className=\"text-xs text-muted-foreground\">\n                                    Showing {startIndex + 1}-{Math.min(endIndex, stats.by_project.length)} of {stats.by_project.length}\n                                  </span>\n                                  <div className=\"flex items-center gap-2\">\n                                    <Button\n                                      variant=\"outline\"\n                                      size=\"sm\"\n                                      onClick={() => setProjectsPage(prev => Math.max(1, prev - 1))}\n                                      disabled={projectsPage === 1}\n                                    >\n                                      <ChevronLeft className=\"h-4 w-4\" />\n                                    </Button>\n                                    <span className=\"text-sm\">\n                                      Page {projectsPage} of {totalPages}\n                                    </span>\n                                    <Button\n                                      variant=\"outline\"\n                                      size=\"sm\"\n                                      onClick={() => setProjectsPage(prev => Math.min(totalPages, prev + 1))}\n                                      disabled={projectsPage === totalPages}\n                                    >\n                                      <ChevronRight className=\"h-4 w-4\" />\n                                    </Button>\n                                  </div>\n                                </div>\n                              )}\n                            </>\n                          );\n                          })()}\n                        </div>\n                      </Card>\n                    </div>\n                  )}\n                </TabsContent>\n\n                {/* Sessions Tab - Lazy render and cache */}\n                <TabsContent value=\"sessions\" className=\"space-y-6 mt-6\">\n                  {hasLoadedTabs.has(\"sessions\") && (\n                    <div style={{ display: activeTab === \"sessions\" ? \"block\" : \"none\" }}>\n                      <Card className=\"p-6\">\n                      <div className=\"flex items-center justify-between mb-4\">\n                        <h3 className=\"text-sm font-semibold\">Usage by Session</h3>\n                        {sessionStats && sessionStats.length > 0 && (\n                          <span className=\"text-xs text-muted-foreground\">\n                            {sessionStats.length} total sessions\n                          </span>\n                        )}\n                      </div>\n                      <div className=\"space-y-3\">\n                        {sessionStats && sessionStats.length > 0 ? (() => {\n                          const startIndex = (sessionsPage - 1) * ITEMS_PER_PAGE;\n                          const endIndex = startIndex + ITEMS_PER_PAGE;\n                          const paginatedSessions = sessionStats.slice(startIndex, endIndex);\n                          const totalPages = Math.ceil(sessionStats.length / ITEMS_PER_PAGE);\n                          \n                          return (\n                            <>\n                              {paginatedSessions.map((session, index) => (\n                                <div key={`${session.project_path}-${session.project_name}-${startIndex + index}`} className=\"flex items-center justify-between py-2 border-b border-border last:border-0\">\n                                  <div className=\"flex flex-col\">\n                                    <div className=\"flex items-center space-x-2\">\n                                      <Briefcase className=\"h-4 w-4 text-muted-foreground\" />\n                                      <span className=\"text-xs font-mono text-muted-foreground truncate max-w-[200px]\" title={session.project_path}>\n                                        {session.project_path.split('/').slice(-2).join('/')}\n                                      </span>\n                                    </div>\n                                    <span className=\"text-sm font-medium mt-1\">\n                                      {session.project_name}\n                                    </span>\n                                  </div>\n                                  <div className=\"text-right\">\n                                    <p className=\"text-sm font-semibold\">{formatCurrency(session.total_cost)}</p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                      {session.last_used ? new Date(session.last_used).toLocaleDateString() : 'N/A'}\n                                    </p>\n                                  </div>\n                                </div>\n                              ))}\n                              \n                              {/* Pagination Controls */}\n                              {totalPages > 1 && (\n                                <div className=\"flex items-center justify-between pt-4\">\n                                  <span className=\"text-xs text-muted-foreground\">\n                                    Showing {startIndex + 1}-{Math.min(endIndex, sessionStats.length)} of {sessionStats.length}\n                                  </span>\n                                  <div className=\"flex items-center gap-2\">\n                                    <Button\n                                      variant=\"outline\"\n                                      size=\"sm\"\n                                      onClick={() => setSessionsPage(prev => Math.max(1, prev - 1))}\n                                      disabled={sessionsPage === 1}\n                                    >\n                                      <ChevronLeft className=\"h-4 w-4\" />\n                                    </Button>\n                                    <span className=\"text-sm\">\n                                      Page {sessionsPage} of {totalPages}\n                                    </span>\n                                    <Button\n                                      variant=\"outline\"\n                                      size=\"sm\"\n                                      onClick={() => setSessionsPage(prev => Math.min(totalPages, prev + 1))}\n                                      disabled={sessionsPage === totalPages}\n                                    >\n                                      <ChevronRight className=\"h-4 w-4\" />\n                                    </Button>\n                                  </div>\n                                </div>\n                              )}\n                            </>\n                          );\n                        })() : (\n                          <div className=\"text-center py-8 text-sm text-muted-foreground\">\n                            No session data available for the selected period\n                          </div>\n                          )}\n                        </div>\n                      </Card>\n                    </div>\n                  )}\n                </TabsContent>\n\n                {/* Timeline Tab - Lazy render and cache */}\n                <TabsContent value=\"timeline\" className=\"space-y-6 mt-6\">\n                  {hasLoadedTabs.has(\"timeline\") && stats && (\n                    <div style={{ display: activeTab === \"timeline\" ? \"block\" : \"none\" }}>\n                      <Card className=\"p-6\">\n                      <h3 className=\"text-sm font-semibold mb-6 flex items-center space-x-2\">\n                        <Calendar className=\"h-4 w-4\" />\n                        <span>Daily Usage</span>\n                      </h3>\n                      {timelineChartData ? (\n                        <div className=\"relative pl-8 pr-4\">\n                          {/* Y-axis labels */}\n                          <div className=\"absolute left-0 top-0 bottom-8 flex flex-col justify-between text-xs text-muted-foreground\">\n                            <span>{formatCurrency(timelineChartData.maxCost)}</span>\n                            <span>{formatCurrency(timelineChartData.halfMaxCost)}</span>\n                            <span>{formatCurrency(0)}</span>\n                          </div>\n                          \n                          {/* Chart container */}\n                          <div className=\"flex items-end space-x-2 h-64 border-l border-b border-border pl-4\">\n                            {timelineChartData.bars.map((day) => {\n                              const formattedDate = day.date.toLocaleDateString('en-US', {\n                                weekday: 'short',\n                                month: 'short',\n                                day: 'numeric'\n                              });\n                              \n                              return (\n                                <div key={day.date.toISOString()} className=\"flex-1 h-full flex flex-col items-center justify-end group relative\">\n                                  {/* Tooltip */}\n                                  <div className=\"absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10\">\n                                    <div className=\"bg-background border border-border rounded-lg shadow-lg p-3 whitespace-nowrap\">\n                                      <p className=\"text-sm font-semibold\">{formattedDate}</p>\n                                      <p className=\"text-sm text-muted-foreground mt-1\">\n                                        Cost: {formatCurrency(day.total_cost)}\n                                      </p>\n                                      <p className=\"text-xs text-muted-foreground\">\n                                        {formatTokens(day.total_tokens)} tokens\n                                      </p>\n                                      <p className=\"text-xs text-muted-foreground\">\n                                        {day.models_used.length} model{day.models_used.length !== 1 ? 's' : ''}\n                                      </p>\n                                    </div>\n                                    <div className=\"absolute top-full left-1/2 transform -translate-x-1/2 -mt-1\">\n                                      <div className=\"border-4 border-transparent border-t-border\"></div>\n                                    </div>\n                                  </div>\n                                  \n                                  {/* Bar */}\n                                  <div \n                                    className=\"w-full bg-primary hover:opacity-80 transition-opacity rounded-t cursor-pointer\"\n                                    style={{ height: `${day.heightPercent}%` }}\n                                  />\n                                  \n                                  {/* X-axis label – absolutely positioned below the bar */}\n                                  <div\n                                    className=\"absolute left-1/2 top-full mt-2 -translate-x-1/2 text-xs text-muted-foreground whitespace-nowrap pointer-events-none\"\n                                  >\n                                    {day.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n                                  </div>\n                                </div>\n                              );\n                            })}\n                          </div>\n                          \n                          {/* X-axis label */}\n                          <div className=\"mt-10 text-center text-xs text-muted-foreground\">\n                            Daily Usage Over Time\n                          </div>\n                        </div>\n                      ) : (\n                        <div className=\"text-center py-8 text-sm text-muted-foreground\">\n                          No usage data available for the selected period\n                        </div>\n                        )}\n                      </Card>\n                    </div>\n                  )}\n                </TabsContent>\n              </Tabs>\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n};"
  },
  {
    "path": "src/components/WebviewPreview.tsx",
    "content": "import React, { useState, useRef, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport {\n  ArrowLeft,\n  ArrowRight,\n  RefreshCw,\n  X,\n  Minimize2,\n  Maximize2,\n  Loader2,\n  AlertCircle,\n  Globe,\n  Home,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\ninterface WebviewPreviewProps {\n  /**\n   * Initial URL to load\n   */\n  initialUrl: string;\n  /**\n   * Callback when close is clicked\n   */\n  onClose: () => void;\n  /**\n   * Whether the webview is maximized\n   */\n  isMaximized?: boolean;\n  /**\n   * Callback when maximize/minimize is clicked\n   */\n  onToggleMaximize?: () => void;\n  /**\n   * Callback when URL changes\n   */\n  onUrlChange?: (url: string) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * WebviewPreview component - Browser-like webview with navigation controls\n * \n * @example\n * <WebviewPreview\n *   initialUrl=\"http://localhost:3000\"\n *   onClose={() => setShowPreview(false)}\n * />\n */\nconst WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({\n  initialUrl,\n  onClose,\n  isMaximized = false,\n  onToggleMaximize,\n  onUrlChange,\n  className,\n}) => {\n  const [currentUrl, setCurrentUrl] = useState(initialUrl);\n  const [inputUrl, setInputUrl] = useState(initialUrl);\n  const [isLoading, setIsLoading] = useState(false);\n  const [hasError, setHasError] = useState(false);\n  const [errorMessage, setErrorMessage] = useState(\"\");\n  // TODO: These will be implemented with actual webview navigation\n  // const [canGoBack, setCanGoBack] = useState(false);\n  // const [canGoForward, setCanGoForward] = useState(false);\n\n  // TODO: These will be used for actual Tauri webview implementation\n  // const webviewRef = useRef<WebviewWindow | null>(null);\n  const iframeRef = useRef<HTMLIFrameElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  // const previewId = useRef(`preview-${Date.now()}`);\n  const isIMEComposingRef = useRef(false);\n\n  // Handle ESC key to exit full screen\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape' && isMaximized && onToggleMaximize) {\n        onToggleMaximize();\n      }\n    };\n\n    document.addEventListener('keydown', handleKeyDown);\n    return () => document.removeEventListener('keydown', handleKeyDown);\n  }, [isMaximized, onToggleMaximize]);\n\n  // Debug: Log initial URL on mount\n  useEffect(() => {\n    console.log('[WebviewPreview] Component mounted with initialUrl:', initialUrl, 'isMaximized:', isMaximized);\n  }, []);\n\n  // Focus management for full screen mode\n  useEffect(() => {\n    if (isMaximized && containerRef.current) {\n      containerRef.current.focus();\n    }\n  }, [isMaximized]);\n\n  // For now, we'll use an iframe as a placeholder\n  // In the full implementation, this would create a Tauri webview window\n  useEffect(() => {\n    if (currentUrl) {\n      // This is where we'd create the actual webview\n      // For now, using iframe for demonstration\n      setIsLoading(true);\n      setHasError(false);\n      \n      // Simulate loading\n      const timer = setTimeout(() => {\n        setIsLoading(false);\n      }, 1000);\n\n      return () => clearTimeout(timer);\n    }\n  }, [currentUrl]);\n\n  const navigate = (url: string) => {\n    try {\n      // Validate URL\n      const urlObj = new URL(url.startsWith('http') ? url : `https://${url}`);\n      const finalUrl = urlObj.href;\n      \n      console.log('[WebviewPreview] Navigating to:', finalUrl);\n      setCurrentUrl(finalUrl);\n      setInputUrl(finalUrl);\n      setHasError(false);\n      onUrlChange?.(finalUrl);\n    } catch (err) {\n      setHasError(true);\n      setErrorMessage(\"Invalid URL\");\n    }\n  };\n\n  const handleNavigate = () => {\n    if (inputUrl.trim()) {\n      navigate(inputUrl);\n    }\n  };\n\n  const handleCompositionStart = () => {\n    isIMEComposingRef.current = true;\n  };\n\n  const handleCompositionEnd = () => {\n    setTimeout(() => {\n      isIMEComposingRef.current = false;\n    }, 0);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      if (e.nativeEvent.isComposing || isIMEComposingRef.current) {\n        return;\n      }\n      handleNavigate();\n    }\n  };\n\n  const handleGoBack = () => {\n    // In real implementation, this would call webview.goBack()\n    console.log(\"Go back\");\n  };\n\n  const handleGoForward = () => {\n    // In real implementation, this would call webview.goForward()\n    console.log(\"Go forward\");\n  };\n\n  const handleRefresh = () => {\n    setIsLoading(true);\n    // In real implementation, this would call webview.reload()\n    setTimeout(() => setIsLoading(false), 1000);\n  };\n\n  const handleGoHome = () => {\n    navigate(initialUrl);\n  };\n\n  return (\n    <div \n      ref={containerRef}\n      className={cn(\"flex flex-col h-full bg-background border-l\", className)}\n      tabIndex={-1}\n      role=\"region\"\n      aria-label=\"Web preview\"\n    >\n      {/* Browser Top Bar */}\n      <div className=\"border-b bg-muted/30 flex-shrink-0\">\n        {/* Title Bar */}\n        <div className=\"flex items-center justify-between px-3 py-2 border-b\">\n          <div className=\"flex items-center gap-2\">\n            <Globe className=\"h-4 w-4 text-muted-foreground\" />\n            <span className=\"text-sm font-medium\">Preview</span>\n            {isLoading && (\n              <Loader2 className=\"h-3 w-3 animate-spin text-muted-foreground\" />\n            )}\n          </div>\n          \n          <div className=\"flex items-center gap-1\">\n            {onToggleMaximize && (\n              <TooltipProvider>\n                <Tooltip>\n                  <TooltipTrigger asChild>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      onClick={onToggleMaximize}\n                      className=\"h-7 w-7\"\n                    >\n                      {isMaximized ? (\n                        <Minimize2 className=\"h-3.5 w-3.5\" />\n                      ) : (\n                        <Maximize2 className=\"h-3.5 w-3.5\" />\n                      )}\n                    </Button>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    {isMaximized ? \"Exit full screen (ESC)\" : \"Enter full screen\"}\n                  </TooltipContent>\n                </Tooltip>\n              </TooltipProvider>\n            )}\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={onClose}\n              className=\"h-7 w-7 hover:bg-destructive/10 hover:text-destructive\"\n            >\n              <X className=\"h-3.5 w-3.5\" />\n            </Button>\n          </div>\n        </div>\n        \n        {/* Navigation Bar */}\n        <div className=\"flex items-center gap-2 px-3 py-2\">\n          {/* Navigation Buttons */}\n          <div className=\"flex items-center gap-1\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={handleGoBack}\n              disabled={true} // TODO: Enable when implementing actual navigation\n              className=\"h-8 w-8\"\n            >\n              <ArrowLeft className=\"h-4 w-4\" />\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={handleGoForward}\n              disabled={true} // TODO: Enable when implementing actual navigation\n              className=\"h-8 w-8\"\n            >\n              <ArrowRight className=\"h-4 w-4\" />\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={handleRefresh}\n              disabled={isLoading}\n              className=\"h-8 w-8\"\n            >\n              <RefreshCw className={cn(\"h-4 w-4\", isLoading && \"animate-spin\")} />\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={handleGoHome}\n              className=\"h-8 w-8\"\n            >\n              <Home className=\"h-4 w-4\" />\n            </Button>\n          </div>\n          \n          {/* URL Bar */}\n          <div className=\"flex-1 relative\">\n            <Input\n              value={inputUrl}\n              onChange={(e) => setInputUrl(e.target.value)}\n              onKeyDown={handleKeyDown}\n              onCompositionStart={handleCompositionStart}\n              onCompositionEnd={handleCompositionEnd}\n              placeholder=\"Enter URL...\"\n              className=\"pr-10 h-8 text-sm font-mono\"\n            />\n            {inputUrl !== currentUrl && (\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                onClick={handleNavigate}\n                className=\"absolute right-1 top-1 h-6 w-6\"\n              >\n                <ArrowRight className=\"h-3 w-3\" />\n              </Button>\n            )}\n          </div>\n        </div>\n      </div>\n      \n      {/* Webview Content */}\n      <div className=\"flex-1 relative bg-background\" ref={contentRef}>\n        {/* Loading Overlay */}\n        <AnimatePresence>\n          {isLoading && (\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              className=\"absolute inset-0 bg-background/80 backdrop-blur-sm z-10 flex items-center justify-center\"\n            >\n              <div className=\"flex flex-col items-center gap-3\">\n                <Loader2 className=\"h-8 w-8 animate-spin text-primary\" />\n                <p className=\"text-sm text-muted-foreground\">Loading preview...</p>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n        \n        {/* Error State */}\n        {hasError ? (\n          <div className=\"flex flex-col items-center justify-center h-full p-8\">\n            <AlertCircle className=\"h-12 w-12 text-destructive mb-4\" />\n            <h3 className=\"text-lg font-semibold mb-2\">Failed to load preview</h3>\n            <p className=\"text-sm text-muted-foreground text-center mb-4\">\n              {errorMessage || \"The page could not be loaded. Please check the URL and try again.\"}\n            </p>\n            <Button onClick={handleRefresh} variant=\"outline\" size=\"sm\">\n              Try Again\n            </Button>\n          </div>\n        ) : currentUrl ? (\n          // Placeholder iframe - in real implementation, this would be a Tauri webview\n          <iframe\n            ref={iframeRef}\n            src={currentUrl}\n            className=\"absolute inset-0 w-full h-full border-0\"\n            title=\"Preview\"\n            sandbox=\"allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox\"\n            onLoad={() => setIsLoading(false)}\n            onError={() => {\n              setHasError(true);\n              setIsLoading(false);\n            }}\n          />\n        ) : (\n          // Empty state when no URL is provided\n          <div className=\"flex flex-col items-center justify-center h-full p-8 text-foreground\">\n            <Globe className=\"h-16 w-16 text-muted-foreground/50 mb-6\" />\n            <h3 className=\"text-xl font-semibold mb-3\">Enter a URL to preview</h3>\n            <p className=\"text-sm text-muted-foreground text-center mb-6 max-w-md\">\n              Enter a URL in the address bar above to preview a website.\n            </p>\n            <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n              <span>Try entering</span>\n              <code className=\"px-2 py-1 bg-muted/50 text-foreground rounded font-mono text-xs\">localhost:3000</code>\n              <span>or any other URL</span>\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport const WebviewPreview = React.memo(WebviewPreviewComponent); "
  },
  {
    "path": "src/components/claude-code-session/MessageList.tsx",
    "content": "import React, { useRef, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { useVirtualizer } from '@tanstack/react-virtual';\nimport { StreamMessage } from '../StreamMessage';\nimport { Terminal } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport type { ClaudeStreamMessage } from '../AgentExecution';\n\ninterface MessageListProps {\n  messages: ClaudeStreamMessage[];\n  projectPath: string;\n  isStreaming: boolean;\n  onLinkDetected?: (url: string) => void;\n  className?: string;\n}\n\nexport const MessageList: React.FC<MessageListProps> = React.memo(({\n  messages,\n  projectPath,\n  isStreaming,\n  onLinkDetected,\n  className\n}) => {\n  const scrollContainerRef = useRef<HTMLDivElement>(null);\n  const shouldAutoScrollRef = useRef(true);\n  const userHasScrolledRef = useRef(false);\n\n  // Virtual scrolling setup\n  const virtualizer = useVirtualizer({\n    count: messages.length,\n    getScrollElement: () => scrollContainerRef.current,\n    estimateSize: () => 100, // Estimated height of each message\n    overscan: 5,\n  });\n\n  // Auto-scroll to bottom when new messages arrive\n  useEffect(() => {\n    if (shouldAutoScrollRef.current && scrollContainerRef.current) {\n      const scrollElement = scrollContainerRef.current;\n      scrollElement.scrollTop = scrollElement.scrollHeight;\n    }\n  }, [messages]);\n\n  // Handle scroll events to detect user scrolling\n  const handleScroll = () => {\n    if (!scrollContainerRef.current) return;\n    \n    const scrollElement = scrollContainerRef.current;\n    const isAtBottom = \n      Math.abs(scrollElement.scrollHeight - scrollElement.scrollTop - scrollElement.clientHeight) < 50;\n    \n    if (!isAtBottom) {\n      userHasScrolledRef.current = true;\n      shouldAutoScrollRef.current = false;\n    } else if (userHasScrolledRef.current) {\n      shouldAutoScrollRef.current = true;\n      userHasScrolledRef.current = false;\n    }\n  };\n\n  // Reset auto-scroll when streaming stops\n  useEffect(() => {\n    if (!isStreaming) {\n      shouldAutoScrollRef.current = true;\n      userHasScrolledRef.current = false;\n    }\n  }, [isStreaming]);\n\n  if (messages.length === 0) {\n    return (\n      <div className={cn(\"flex-1 flex items-center justify-center\", className)}>\n        <motion.div\n          initial={{ opacity: 0, scale: 0.95 }}\n          animate={{ opacity: 1, scale: 1 }}\n          className=\"text-center space-y-4 max-w-md\"\n        >\n          <div className=\"h-16 w-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto\">\n            <Terminal className=\"h-8 w-8 text-primary\" />\n          </div>\n          <div>\n            <h3 className=\"text-lg font-semibold mb-2\">Ready to start coding</h3>\n            <p className=\"text-sm text-muted-foreground\">\n              {projectPath \n                ? \"Enter a prompt below to begin your Claude Code session\"\n                : \"Select a project folder to begin\"}\n            </p>\n          </div>\n        </motion.div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      ref={scrollContainerRef}\n      onScroll={handleScroll}\n      className={cn(\"flex-1 overflow-y-auto scroll-smooth\", className)}\n    >\n      <div\n        style={{\n          height: `${virtualizer.getTotalSize()}px`,\n          width: '100%',\n          position: 'relative',\n        }}\n      >\n        <AnimatePresence mode=\"popLayout\">\n          {virtualizer.getVirtualItems().map((virtualItem) => {\n            const message = messages[virtualItem.index];\n            const key = `msg-${virtualItem.index}-${message.type}`;\n            \n            return (\n              <motion.div\n                key={key}\n                initial={{ opacity: 0, y: 10 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, scale: 0.95 }}\n                transition={{ duration: 0.2 }}\n                style={{\n                  position: 'absolute',\n                  top: 0,\n                  left: 0,\n                  width: '100%',\n                  transform: `translateY(${virtualItem.start}px)`,\n                }}\n              >\n                <div className=\"px-4 py-2\">\n                  <StreamMessage \n                    message={message}\n                    streamMessages={messages}\n                    onLinkDetected={onLinkDetected}\n                  />\n                </div>\n              </motion.div>\n            );\n          })}\n        </AnimatePresence>\n      </div>\n\n      {/* Streaming indicator */}\n      {isStreaming && (\n        <motion.div\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          className=\"sticky bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-background to-transparent\"\n        >\n          <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n            <div className=\"h-2 w-2 bg-primary rounded-full animate-pulse\" />\n            <span>Claude is thinking...</span>\n          </div>\n        </motion.div>\n      )}\n    </div>\n  );\n});"
  },
  {
    "path": "src/components/claude-code-session/PromptQueue.tsx",
    "content": "import React from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { X, Clock, Sparkles, Zap } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\nimport { cn } from '@/lib/utils';\n\ninterface QueuedPrompt {\n  id: string;\n  prompt: string;\n  model: \"sonnet\" | \"opus\";\n}\n\ninterface PromptQueueProps {\n  queuedPrompts: QueuedPrompt[];\n  onRemove: (id: string) => void;\n  className?: string;\n}\n\nexport const PromptQueue: React.FC<PromptQueueProps> = React.memo(({\n  queuedPrompts,\n  onRemove,\n  className\n}) => {\n  if (queuedPrompts.length === 0) return null;\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, height: 0 }}\n      animate={{ opacity: 1, height: 'auto' }}\n      exit={{ opacity: 0, height: 0 }}\n      className={cn(\"border-t bg-muted/20\", className)}\n    >\n      <div className=\"px-4 py-3\">\n        <div className=\"flex items-center gap-2 mb-2\">\n          <Clock className=\"h-4 w-4 text-muted-foreground\" />\n          <span className=\"text-sm font-medium\">Queued Prompts</span>\n          <Badge variant=\"secondary\" className=\"text-xs\">\n            {queuedPrompts.length}\n          </Badge>\n        </div>\n        \n        <div className=\"space-y-2 max-h-32 overflow-y-auto\">\n          <AnimatePresence mode=\"popLayout\">\n            {queuedPrompts.map((queuedPrompt, index) => (\n              <motion.div\n                key={queuedPrompt.id}\n                initial={{ opacity: 0, x: -20 }}\n                animate={{ opacity: 1, x: 0 }}\n                exit={{ opacity: 0, x: 20 }}\n                transition={{ delay: index * 0.05 }}\n                className=\"flex items-start gap-2 p-2 rounded-md bg-background/50\"\n              >\n                <div className=\"flex-shrink-0 mt-0.5\">\n                  {queuedPrompt.model === \"opus\" ? (\n                    <Sparkles className=\"h-3.5 w-3.5 text-purple-500\" />\n                  ) : (\n                    <Zap className=\"h-3.5 w-3.5 text-amber-500\" />\n                  )}\n                </div>\n                \n                <div className=\"flex-1 min-w-0\">\n                  <p className=\"text-sm truncate\">{queuedPrompt.prompt}</p>\n                  <span className=\"text-xs text-muted-foreground\">\n                    {queuedPrompt.model === \"opus\" ? \"Opus\" : \"Sonnet\"}\n                  </span>\n                </div>\n                \n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  className=\"h-6 w-6 flex-shrink-0\"\n                  onClick={() => onRemove(queuedPrompt.id)}\n                >\n                  <X className=\"h-3 w-3\" />\n                </Button>\n              </motion.div>\n            ))}\n          </AnimatePresence>\n        </div>\n      </div>\n    </motion.div>\n  );\n});"
  },
  {
    "path": "src/components/claude-code-session/SessionHeader.tsx",
    "content": "import React from 'react';\nimport { motion } from 'framer-motion';\nimport { \n  ArrowLeft, \n  Terminal, \n  FolderOpen, \n  Copy, \n  GitBranch,\n  Settings,\n  Hash,\n  Command\n} from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Popover } from '@/components/ui/popover';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';\nimport { Badge } from '@/components/ui/badge';\nimport { cn } from '@/lib/utils';\n\ninterface SessionHeaderProps {\n  projectPath: string;\n  claudeSessionId: string | null;\n  totalTokens: number;\n  isStreaming: boolean;\n  hasMessages: boolean;\n  showTimeline: boolean;\n  copyPopoverOpen: boolean;\n  onBack: () => void;\n  onSelectPath: () => void;\n  onCopyAsJsonl: () => void;\n  onCopyAsMarkdown: () => void;\n  onToggleTimeline: () => void;\n  onProjectSettings?: () => void;\n  onSlashCommandsSettings?: () => void;\n  setCopyPopoverOpen: (open: boolean) => void;\n}\n\nexport const SessionHeader: React.FC<SessionHeaderProps> = React.memo(({\n  projectPath,\n  claudeSessionId,\n  totalTokens,\n  isStreaming,\n  hasMessages,\n  showTimeline,\n  copyPopoverOpen,\n  onBack,\n  onSelectPath,\n  onCopyAsJsonl,\n  onCopyAsMarkdown,\n  onToggleTimeline,\n  onProjectSettings,\n  onSlashCommandsSettings,\n  setCopyPopoverOpen\n}) => {\n  return (\n    <motion.div \n      initial={{ opacity: 0, y: -20 }}\n      animate={{ opacity: 1, y: 0 }}\n      className=\"bg-background/95 backdrop-blur-sm border-b px-4 py-3 sticky top-0 z-40\"\n    >\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-3\">\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={onBack}\n            className=\"h-8 w-8\"\n          >\n            <ArrowLeft className=\"h-4 w-4\" />\n          </Button>\n          \n          <div className=\"flex items-center gap-2\">\n            <Terminal className=\"h-5 w-5 text-primary\" />\n            <span className=\"font-semibold\">Claude Code Session</span>\n          </div>\n\n          \n          {!projectPath && (\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={onSelectPath}\n              className=\"flex items-center gap-2\"\n            >\n              <FolderOpen className=\"h-4 w-4\" />\n              Select Project\n            </Button>\n          )}\n        </div>\n\n        <div className=\"flex items-center gap-2\">\n          {claudeSessionId && (\n            <div className=\"flex items-center gap-2\">\n              <Badge variant=\"outline\" className=\"text-xs\">\n                <Hash className=\"h-3 w-3 mr-1\" />\n                {claudeSessionId.slice(0, 8)}\n              </Badge>\n              {totalTokens > 0 && (\n                <Badge variant=\"secondary\" className=\"text-xs\">\n                  {totalTokens.toLocaleString()} tokens\n                </Badge>\n              )}\n            </div>\n          )}\n\n          {hasMessages && !isStreaming && (\n            <Popover\n              open={copyPopoverOpen}\n              onOpenChange={setCopyPopoverOpen}\n              trigger={\n                <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\">\n                  <Copy className=\"h-4 w-4\" />\n                </Button>\n              }\n              content={\n                <div className=\"space-y-1\">\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"w-full justify-start\"\n                    onClick={onCopyAsJsonl}\n                  >\n                    Copy as JSONL\n                  </Button>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"w-full justify-start\"\n                    onClick={onCopyAsMarkdown}\n                  >\n                    Copy as Markdown\n                  </Button>\n                </div>\n              }\n              className=\"w-48 p-2\"\n            />\n          )}\n\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={onToggleTimeline}\n            className={cn(\n              \"h-8 w-8 transition-colors\",\n              showTimeline && \"bg-accent text-accent-foreground\"\n            )}\n          >\n            <GitBranch className=\"h-4 w-4\" />\n          </Button>\n\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\">\n                <Settings className=\"h-4 w-4\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\" className=\"w-48\">\n              {onProjectSettings && projectPath && (\n                <DropdownMenuItem onClick={onProjectSettings}>\n                  <Settings className=\"h-4 w-4 mr-2\" />\n                  Project Settings\n                </DropdownMenuItem>\n              )}\n              {onSlashCommandsSettings && projectPath && (\n                <DropdownMenuItem onClick={onSlashCommandsSettings}>\n                  <Command className=\"h-4 w-4 mr-2\" />\n                  Slash Commands\n                </DropdownMenuItem>\n              )}\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n      </div>\n    </motion.div>\n  );\n});"
  },
  {
    "path": "src/components/claude-code-session/useCheckpoints.ts",
    "content": "import { useState, useCallback } from 'react';\nimport { api } from '@/lib/api';\n\n// Local checkpoint format for UI display\ninterface Checkpoint {\n  id: string;\n  sessionId: string;\n  name: string;\n  createdAt: string;\n  messageCount: number;\n}\n\ninterface UseCheckpointsOptions {\n  sessionId: string | null;\n  projectId: string;\n  projectPath: string;\n  onToast?: (message: string, type: 'success' | 'error') => void;\n}\n\nexport function useCheckpoints({ sessionId, projectId, projectPath, onToast }: UseCheckpointsOptions) {\n  const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([]);\n  const [isLoadingCheckpoints, setIsLoadingCheckpoints] = useState(false);\n  const [timelineVersion, setTimelineVersion] = useState(0);\n  \n  const showToast = useCallback((message: string, type: 'success' | 'error' = 'success') => {\n    if (onToast) {\n      onToast(message, type);\n    }\n  }, [onToast]);\n\n  const loadCheckpoints = useCallback(async () => {\n    if (!sessionId) return;\n    \n    setIsLoadingCheckpoints(true);\n    try {\n      const result = await api.listCheckpoints(sessionId, projectId, projectPath);\n      // Map API Checkpoint type to local format if needed\n      const mappedCheckpoints = result.map(cp => ({\n        id: cp.id,\n        sessionId: cp.sessionId,\n        name: cp.description || `Checkpoint at ${cp.timestamp}`,\n        createdAt: cp.timestamp,\n        messageCount: cp.metadata.totalTokens\n      }));\n      setCheckpoints(mappedCheckpoints);\n      setTimelineVersion(prev => prev + 1);\n    } catch (error) {\n      console.error(\"Failed to load checkpoints:\", error);\n      showToast(\"Failed to load checkpoints\", 'error');\n    } finally {\n      setIsLoadingCheckpoints(false);\n    }\n  }, [sessionId, projectId, projectPath, showToast]);\n\n  const createCheckpoint = useCallback(async (name: string) => {\n    if (!sessionId) return;\n    \n    try {\n      await api.createCheckpoint(sessionId, projectId, projectPath, undefined, name);\n      await loadCheckpoints();\n      showToast(\"Checkpoint created successfully\", 'success');\n    } catch (error) {\n      console.error(\"Failed to create checkpoint:\", error);\n      showToast(\"Failed to create checkpoint\", 'error');\n      throw error;\n    }\n  }, [sessionId, projectId, projectPath, loadCheckpoints, showToast]);\n\n  const restoreCheckpoint = useCallback(async (checkpointId: string) => {\n    if (!sessionId) return;\n    \n    try {\n      await api.restoreCheckpoint(checkpointId, sessionId, projectId, projectPath);\n      showToast(\"Checkpoint restored successfully\", 'success');\n      // Return true to indicate success\n      return true;\n    } catch (error) {\n      console.error(\"Failed to restore checkpoint:\", error);\n      showToast(\"Failed to restore checkpoint\", 'error');\n      return false;\n    }\n  }, [sessionId, projectId, projectPath, showToast]);\n\n  const deleteCheckpoint = useCallback(async (_checkpointId: string) => {\n    if (!sessionId) return;\n    \n    try {\n      // API doesn't have deleteCheckpoint, using a placeholder\n      console.warn('deleteCheckpoint not implemented in API');\n      await loadCheckpoints();\n      showToast(\"Checkpoint deleted successfully\", 'success');\n    } catch (error) {\n      console.error(\"Failed to delete checkpoint:\", error);\n      showToast(\"Failed to delete checkpoint\", 'error');\n    }\n  }, [sessionId, loadCheckpoints, showToast]);\n\n  const forkCheckpoint = useCallback(async (checkpointId: string, newSessionName: string) => {\n    if (!sessionId) return null;\n    \n    try {\n      const forkedSession = await api.forkFromCheckpoint(checkpointId, sessionId, projectId, projectPath, newSessionName, 'Forked from checkpoint');\n      showToast(\"Session forked successfully\", 'success');\n      return forkedSession;\n    } catch (error) {\n      console.error(\"Failed to fork checkpoint:\", error);\n      showToast(\"Failed to fork session\", 'error');\n      return null;\n    }\n  }, [sessionId, projectId, projectPath, showToast]);\n\n  return {\n    checkpoints,\n    isLoadingCheckpoints,\n    timelineVersion,\n    loadCheckpoints,\n    createCheckpoint,\n    restoreCheckpoint,\n    deleteCheckpoint,\n    forkCheckpoint\n  };\n}"
  },
  {
    "path": "src/components/claude-code-session/useClaudeMessages.ts",
    "content": "import { useState, useCallback, useRef, useEffect } from 'react';\nimport { api } from '@/lib/api';\nimport { getEnvironmentInfo } from '@/lib/apiAdapter';\nimport type { ClaudeStreamMessage } from '../AgentExecution';\n\n// Conditional import for Tauri\nlet tauriListen: any;\ntry {\n  if (typeof window !== 'undefined' && window.__TAURI__) {\n    tauriListen = require('@tauri-apps/api/event').listen;\n  }\n} catch (e) {\n  console.log('[useClaudeMessages] Tauri event API not available, using web mode');\n}\n\ninterface UseClaudeMessagesOptions {\n  onSessionInfo?: (info: { sessionId: string; projectId: string }) => void;\n  onTokenUpdate?: (tokens: number) => void;\n  onStreamingChange?: (isStreaming: boolean, sessionId: string | null) => void;\n}\n\nexport function useClaudeMessages(options: UseClaudeMessagesOptions = {}) {\n  const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);\n  const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);\n  \n  const eventListenerRef = useRef<(() => void) | null>(null);\n  const accumulatedContentRef = useRef<{ [key: string]: string }>({});\n\n  const handleMessage = useCallback((message: ClaudeStreamMessage) => {\n    console.log('[TRACE] useClaudeMessages.handleMessage called with:', message);\n    \n    if ((message as any).type === \"start\") {\n      console.log('[TRACE] Start message detected - clearing accumulated content and setting streaming=true');\n      // Clear accumulated content for new stream\n      accumulatedContentRef.current = {};\n      setIsStreaming(true);\n      options.onStreamingChange?.(true, currentSessionId);\n    } else if ((message as any).type === \"partial\") {\n      console.log('[TRACE] Partial message detected');\n      if (message.tool_calls && message.tool_calls.length > 0) {\n        message.tool_calls.forEach((toolCall: any) => {\n          if (toolCall.content && toolCall.partial_tool_call_index !== undefined) {\n            const key = `tool-${toolCall.partial_tool_call_index}`;\n            if (!accumulatedContentRef.current[key]) {\n              accumulatedContentRef.current[key] = \"\";\n            }\n            accumulatedContentRef.current[key] += toolCall.content;\n            toolCall.accumulated_content = accumulatedContentRef.current[key];\n          }\n        });\n      }\n    } else if ((message as any).type === \"response\" && message.message?.usage) {\n      console.log('[TRACE] Response message with usage detected');\n      const totalTokens = (message.message.usage.input_tokens || 0) + \n                         (message.message.usage.output_tokens || 0);\n      console.log('[TRACE] Total tokens:', totalTokens);\n      options.onTokenUpdate?.(totalTokens);\n    } else if ((message as any).type === \"error\" || (message as any).type === \"response\") {\n      console.log('[TRACE] Error or response message detected - setting streaming=false');\n      setIsStreaming(false);\n      options.onStreamingChange?.(false, currentSessionId);\n    } else if ((message as any).type === \"output\") {\n      console.log('[TRACE] Output message detected, content:', (message as any).content);\n    } else {\n      console.log('[TRACE] Unknown message type:', (message as any).type);\n    }\n\n    console.log('[TRACE] Adding message to state');\n    setMessages(prev => {\n      const newMessages = [...prev, message];\n      console.log('[TRACE] Total messages now:', newMessages.length);\n      return newMessages;\n    });\n    setRawJsonlOutput(prev => [...prev, JSON.stringify(message)]);\n\n    // Extract session info\n    if ((message as any).type === \"session_info\" && (message as any).session_id && (message as any).project_id) {\n      console.log('[TRACE] Session info detected:', (message as any).session_id, (message as any).project_id);\n      options.onSessionInfo?.({\n        sessionId: (message as any).session_id,\n        projectId: (message as any).project_id\n      });\n      setCurrentSessionId((message as any).session_id);\n    }\n  }, [currentSessionId, options]);\n\n  const clearMessages = useCallback(() => {\n    setMessages([]);\n    setRawJsonlOutput([]);\n    accumulatedContentRef.current = {};\n  }, []);\n\n  const loadMessages = useCallback(async (sessionId: string) => {\n    try {\n      const output = await api.getSessionOutput(parseInt(sessionId));\n      // Note: API returns a string, not an array of outputs\n      const outputs = [{ jsonl: output }];\n      const loadedMessages: ClaudeStreamMessage[] = [];\n      const loadedRawJsonl: string[] = [];\n      \n      outputs.forEach(output => {\n        if (output.jsonl) {\n          const lines = output.jsonl.split('\\n').filter(line => line.trim());\n          lines.forEach(line => {\n            try {\n              const msg = JSON.parse(line);\n              loadedMessages.push(msg);\n              loadedRawJsonl.push(line);\n            } catch (e) {\n              console.error(\"Failed to parse JSONL:\", e);\n            }\n          });\n        }\n      });\n      \n      setMessages(loadedMessages);\n      setRawJsonlOutput(loadedRawJsonl);\n    } catch (error) {\n      console.error(\"Failed to load session outputs:\", error);\n      throw error;\n    }\n  }, []);\n\n  // Set up event listener\n  useEffect(() => {\n    const setupListener = async () => {\n      console.log('[TRACE] useClaudeMessages setupListener called');\n      if (eventListenerRef.current) {\n        console.log('[TRACE] Cleaning up existing event listener');\n        eventListenerRef.current();\n      }\n      \n      const envInfo = getEnvironmentInfo();\n      console.log('[TRACE] Environment info:', envInfo);\n      \n      if (envInfo.isTauri && tauriListen) {\n        // Tauri mode - use Tauri's event system\n        console.log('[TRACE] Setting up Tauri event listener for claude-stream');\n        eventListenerRef.current = await tauriListen(\"claude-stream\", (event: any) => {\n          console.log('[TRACE] Tauri event received:', event);\n          try {\n            const message = JSON.parse(event.payload) as ClaudeStreamMessage;\n            console.log('[TRACE] Parsed Tauri message:', message);\n            handleMessage(message);\n          } catch (error) {\n            console.error(\"[TRACE] Failed to parse Claude stream message:\", error);\n          }\n        });\n        console.log('[TRACE] Tauri event listener setup complete');\n      } else {\n        // Web mode - use DOM events (these are dispatched by our WebSocket handler)\n        console.log('[TRACE] Setting up web event listener for claude-output');\n        const webEventHandler = (event: any) => {\n          console.log('[TRACE] Web event received:', event);\n          console.log('[TRACE] Event detail:', event.detail);\n          try {\n            const message = event.detail as ClaudeStreamMessage;\n            console.log('[TRACE] Calling handleMessage with:', message);\n            handleMessage(message);\n          } catch (error) {\n            console.error(\"[TRACE] Failed to parse Claude stream message:\", error);\n          }\n        };\n        \n        window.addEventListener('claude-output', webEventHandler);\n        console.log('[TRACE] Web event listener added for claude-output');\n        console.log('[TRACE] Event listener function:', webEventHandler);\n        \n        // Test if event listener is working\n        setTimeout(() => {\n          console.log('[TRACE] Testing event dispatch...');\n          window.dispatchEvent(new CustomEvent('claude-output', {\n            detail: { type: 'test', message: 'test event' }\n          }));\n        }, 1000);\n        \n        eventListenerRef.current = () => {\n          console.log('[TRACE] Removing web event listener');\n          window.removeEventListener('claude-output', webEventHandler);\n        };\n      }\n    };\n\n    setupListener();\n\n    return () => {\n      console.log('[TRACE] useClaudeMessages cleanup');\n      if (eventListenerRef.current) {\n        eventListenerRef.current();\n      }\n    };\n  }, [handleMessage]);\n\n  return {\n    messages,\n    rawJsonlOutput,\n    isStreaming,\n    currentSessionId,\n    clearMessages,\n    loadMessages,\n    handleMessage\n  };\n}"
  },
  {
    "path": "src/components/index.ts",
    "content": "export * from \"./AgentExecutionDemo\";\nexport * from \"./AgentRunOutputViewer\";\nexport * from \"./StreamMessage\";\nexport * from \"./ToolWidgets\"; \nexport * from \"./NFOCredits\"; \nexport * from \"./UsageDashboard\";\nexport * from \"./WebviewPreview\";\nexport * from \"./ImagePreview\";\nexport * from \"./MCPManager\";\nexport * from \"./MCPServerList\";\nexport * from \"./MCPAddServer\";\nexport * from \"./MCPImportExport\";\nexport * from \"./ClaudeVersionSelector\";\nexport * from \"./ui/badge\";\nexport * from \"./ui/button\";\nexport * from \"./ui/card\";\nexport * from \"./ui/dialog\";\nexport * from \"./ui/dropdown-menu\";\nexport * from \"./ui/input\";\nexport * from \"./ui/label\";\nexport * from \"./ui/select\";\nexport * from \"./ui/switch\";\nexport * from \"./ui/tabs\";\nexport * from \"./ui/textarea\";\nexport * from \"./ui/toast\";\nexport * from \"./ui/tooltip\";\nexport * from \"./SlashCommandPicker\";\nexport * from \"./SlashCommandsManager\";\nexport * from \"./ui/popover\";\nexport * from \"./ui/pagination\";\nexport * from \"./ui/split-pane\";\nexport * from \"./ui/scroll-area\"; \nexport * from \"./RunningClaudeSessions\"; \n"
  },
  {
    "path": "src/components/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\nexport { Badge, badgeVariants } "
  },
  {
    "path": "src/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Button variants configuration using class-variance-authority\n */\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\n/**\n * Button component with multiple variants and sizes\n * \n * @example\n * <Button variant=\"outline\" size=\"lg\" onClick={() => console.log('clicked')}>\n *   Click me\n * </Button>\n */\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, ...props }, ref) => {\n    return (\n      <button\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants }; "
  },
  {
    "path": "src/components/ui/card.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Card component - A container with consistent styling and sections\n * \n * @example\n * <Card>\n *   <CardHeader>\n *     <CardTitle>Card Title</CardTitle>\n *     <CardDescription>Card description</CardDescription>\n *   </CardHeader>\n *   <CardContent>\n *     Content goes here\n *   </CardContent>\n *   <CardFooter>\n *     Footer content\n *   </CardFooter>\n * </Card>\n */\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border shadow-xs\",\n      className\n    )}\n    style={{\n      borderColor: \"var(--color-border)\",\n      backgroundColor: \"var(--color-card)\",\n      color: \"var(--color-card-foreground)\"\n    }}\n    {...props}\n  />\n));\nCard.displayName = \"Card\";\n\n/**\n * CardHeader component - Top section of a card\n */\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n));\nCardHeader.displayName = \"CardHeader\";\n\n/**\n * CardTitle component - Main title within CardHeader\n */\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\"font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nCardTitle.displayName = \"CardTitle\";\n\n/**\n * CardDescription component - Descriptive text within CardHeader\n */\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nCardDescription.displayName = \"CardDescription\";\n\n/**\n * CardContent component - Main content area of a card\n */\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\n/**\n * CardFooter component - Bottom section of a card\n */\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; "
  },
  {
    "path": "src/components/ui/dialog.tsx",
    "content": "import * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n} "
  },
  {
    "path": "src/components/ui/dropdown-menu.tsx",
    "content": "import * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "src/components/ui/input.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\n/**\n * Input component for text/number inputs\n * \n * @example\n * <Input type=\"text\" placeholder=\"Enter value...\" />\n */\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors\",\n          \"file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n          \"focus-visible:outline-none focus-visible:ring-1\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        style={{\n          borderColor: \"var(--color-input)\",\n          backgroundColor: \"transparent\",\n          color: \"var(--color-foreground)\"\n        }}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\n\nInput.displayName = \"Input\";\n\nexport { Input }; "
  },
  {
    "path": "src/components/ui/label.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface LabelProps\n  extends React.LabelHTMLAttributes<HTMLLabelElement> {}\n\n/**\n * Label component for form fields\n * \n * @example\n * <Label htmlFor=\"input-id\">Field Label</Label>\n */\nconst Label = React.forwardRef<HTMLLabelElement, LabelProps>(\n  ({ className, ...props }, ref) => (\n    <label\n      ref={ref}\n      className={cn(\n        \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n        className\n      )}\n      {...props}\n    />\n  )\n);\n\nLabel.displayName = \"Label\";\n\nexport { Label }; "
  },
  {
    "path": "src/components/ui/pagination.tsx",
    "content": "import * as React from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ninterface PaginationProps {\n  /**\n   * Current page number (1-indexed)\n   */\n  currentPage: number;\n  /**\n   * Total number of pages\n   */\n  totalPages: number;\n  /**\n   * Callback when page changes\n   */\n  onPageChange: (page: number) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * Pagination component for navigating through paginated content\n * \n * @example\n * <Pagination\n *   currentPage={1}\n *   totalPages={5}\n *   onPageChange={(page) => setCurrentPage(page)}\n * />\n */\nexport const Pagination: React.FC<PaginationProps> = ({\n  currentPage,\n  totalPages,\n  onPageChange,\n  className,\n}) => {\n  if (totalPages <= 1) {\n    return null;\n  }\n\n  return (\n    <div className={cn(\"flex items-center justify-center space-x-2\", className)}>\n      <Button\n        variant=\"outline\"\n        size=\"icon\"\n        onClick={() => onPageChange(currentPage - 1)}\n        disabled={currentPage <= 1}\n        className=\"h-8 w-8\"\n      >\n        <ChevronLeft className=\"h-4 w-4\" />\n      </Button>\n      \n      <span className=\"text-sm text-muted-foreground\">\n        Page {currentPage} of {totalPages}\n      </span>\n      \n      <Button\n        variant=\"outline\"\n        size=\"icon\"\n        onClick={() => onPageChange(currentPage + 1)}\n        disabled={currentPage >= totalPages}\n        className=\"h-8 w-8\"\n      >\n        <ChevronRight className=\"h-4 w-4\" />\n      </Button>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/ui/popover.tsx",
    "content": "import * as React from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\ninterface PopoverProps {\n  /**\n   * The trigger element\n   */\n  trigger: React.ReactNode;\n  /**\n   * The content to display in the popover\n   */\n  content: React.ReactNode;\n  /**\n   * Whether the popover is open\n   */\n  open?: boolean;\n  /**\n   * Callback when the open state changes\n   */\n  onOpenChange?: (open: boolean) => void;\n  /**\n   * Optional className for the content\n   */\n  className?: string;\n  /**\n   * Alignment of the popover relative to the trigger\n   */\n  align?: \"start\" | \"center\" | \"end\";\n  /**\n   * Side of the trigger to display the popover\n   */\n  side?: \"top\" | \"bottom\";\n}\n\n/**\n * Popover component for displaying floating content\n * \n * @example\n * <Popover\n *   trigger={<Button>Click me</Button>}\n *   content={<div>Popover content</div>}\n *   side=\"top\"\n * />\n */\nexport const Popover: React.FC<PopoverProps> = ({\n  trigger,\n  content,\n  open: controlledOpen,\n  onOpenChange,\n  className,\n  align = \"center\",\n  side = \"bottom\",\n}) => {\n  const [internalOpen, setInternalOpen] = React.useState(false);\n  const open = controlledOpen !== undefined ? controlledOpen : internalOpen;\n  const setOpen = onOpenChange || setInternalOpen;\n  \n  const triggerRef = React.useRef<HTMLDivElement>(null);\n  const contentRef = React.useRef<HTMLDivElement>(null);\n  \n  // Close on click outside\n  React.useEffect(() => {\n    if (!open) return;\n    \n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        triggerRef.current &&\n        contentRef.current &&\n        !triggerRef.current.contains(event.target as Node) &&\n        !contentRef.current.contains(event.target as Node)\n      ) {\n        setOpen(false);\n      }\n    };\n    \n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [open, setOpen]);\n  \n  // Close on escape\n  React.useEffect(() => {\n    if (!open) return;\n    \n    const handleEscape = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        setOpen(false);\n      }\n    };\n    \n    document.addEventListener(\"keydown\", handleEscape);\n    return () => document.removeEventListener(\"keydown\", handleEscape);\n  }, [open, setOpen]);\n  \n  const alignClass = {\n    start: \"left-0\",\n    center: \"left-1/2 -translate-x-1/2\",\n    end: \"right-0\",\n  }[align];\n  \n  const sideClass = side === \"top\" ? \"bottom-full mb-2\" : \"top-full mt-2\";\n  const animationY = side === \"top\" ? { initial: 10, exit: 10 } : { initial: -10, exit: -10 };\n  \n  return (\n    <div className=\"relative inline-block\">\n      <div\n        ref={triggerRef}\n        onClick={() => setOpen(!open)}\n      >\n        {trigger}\n      </div>\n      \n      <AnimatePresence>\n        {open && (\n          <motion.div\n            ref={contentRef}\n            initial={{ opacity: 0, scale: 0.95, y: animationY.initial }}\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            exit={{ opacity: 0, scale: 0.95, y: animationY.exit }}\n            transition={{ duration: 0.15 }}\n            className={cn(\n              \"absolute z-50 min-w-[200px] rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md\",\n              sideClass,\n              alignClass,\n              className\n            )}\n          >\n            {content}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/ui/radio-group.tsx",
    "content": "import * as React from \"react\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Circle } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\"grid gap-2\", className)}\n      {...props}\n      ref={ref}\n    />\n  );\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        \"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Circle className=\"h-2.5 w-2.5 fill-current text-current\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem }; "
  },
  {
    "path": "src/components/ui/scroll-area.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n  /**\n   * Children to render inside the scroll area\n   */\n  children: React.ReactNode;\n}\n\n/**\n * ScrollArea component for scrollable content with custom scrollbar styling\n * \n * @example\n * <ScrollArea className=\"h-[200px]\">\n *   <div>Scrollable content here</div>\n * </ScrollArea>\n */\nexport const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(\n  ({ className, children, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\"relative overflow-auto\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  }\n);\n\nScrollArea.displayName = \"ScrollArea\"; "
  },
  {
    "path": "src/components/ui/select.tsx",
    "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\n// Legacy interface for backward compatibility\nexport interface SelectOption {\n  value: string;\n  label: string;\n}\n\nexport interface SelectProps {\n  /**\n   * The current value\n   */\n  value: string;\n  /**\n   * Callback when value changes\n   */\n  onValueChange: (value: string) => void;\n  /**\n   * Available options\n   */\n  options: SelectOption[];\n  /**\n   * Placeholder text\n   */\n  placeholder?: string;\n  /**\n   * Whether the select is disabled\n   */\n  disabled?: boolean;\n  /**\n   * Additional CSS classes\n   */\n  className?: string;\n}\n\n/**\n * Simple select dropdown component\n * \n * @example\n * <Select\n *   value={selected}\n *   onValueChange={setSelected}\n *   options={[\n *     { value: \"option1\", label: \"Option 1\" },\n *     { value: \"option2\", label: \"Option 2\" }\n *   ]}\n * />\n */\nconst SimpleSelect: React.FC<SelectProps> = ({\n  value,\n  onValueChange,\n  options,\n  placeholder = \"Select an option\",\n  disabled = false,\n  className,\n}) => {\n  return (\n    <Select value={value} onValueChange={onValueChange} disabled={disabled}>\n      <SelectTrigger className={className}>\n        <SelectValue placeholder={placeholder} />\n      </SelectTrigger>\n      <SelectContent>\n        {options.map((option) => (\n          <SelectItem key={option.value} value={option.value}>\n            {option.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n  SimpleSelect as SelectComponent,\n}; "
  },
  {
    "path": "src/components/ui/split-pane.tsx",
    "content": "import React, { useState, useRef, useEffect, useCallback } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SplitPaneProps {\n  /**\n   * Content for the left pane\n   */\n  left: React.ReactNode;\n  /**\n   * Content for the right pane\n   */\n  right: React.ReactNode;\n  /**\n   * Initial split position as percentage (0-100)\n   * @default 50\n   */\n  initialSplit?: number;\n  /**\n   * Minimum width for left pane in pixels\n   * @default 200\n   */\n  minLeftWidth?: number;\n  /**\n   * Minimum width for right pane in pixels\n   * @default 200\n   */\n  minRightWidth?: number;\n  /**\n   * Callback when split position changes\n   */\n  onSplitChange?: (position: number) => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * Resizable split pane component for side-by-side layouts\n * \n * @example\n * <SplitPane\n *   left={<div>Left content</div>}\n *   right={<div>Right content</div>}\n *   initialSplit={60}\n *   onSplitChange={(pos) => console.log('Split at', pos)}\n * />\n */\nexport const SplitPane: React.FC<SplitPaneProps> = ({\n  left,\n  right,\n  initialSplit = 50,\n  minLeftWidth = 200,\n  minRightWidth = 200,\n  onSplitChange,\n  className,\n}) => {\n  const [splitPosition, setSplitPosition] = useState(initialSplit);\n  const [isDragging, setIsDragging] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const dragStartX = useRef(0);\n  const dragStartSplit = useRef(0);\n  const animationFrameRef = useRef<number | null>(null);\n\n  // Handle mouse down on divider\n  const handleMouseDown = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setIsDragging(true);\n    dragStartX.current = e.clientX;\n    dragStartSplit.current = splitPosition;\n    document.body.style.cursor = 'col-resize';\n    document.body.style.userSelect = 'none';\n  };\n\n  // Handle mouse move\n  const handleMouseMove = useCallback((e: MouseEvent) => {\n    if (!isDragging || !containerRef.current) return;\n\n    if (animationFrameRef.current) {\n      cancelAnimationFrame(animationFrameRef.current);\n    }\n\n    animationFrameRef.current = requestAnimationFrame(() => {\n      const containerWidth = containerRef.current!.offsetWidth;\n      const deltaX = e.clientX - dragStartX.current;\n      const deltaPercent = (deltaX / containerWidth) * 100;\n      const newSplit = dragStartSplit.current + deltaPercent;\n\n      // Calculate min/max based on pixel constraints\n      const minSplit = (minLeftWidth / containerWidth) * 100;\n      const maxSplit = 100 - (minRightWidth / containerWidth) * 100;\n\n      const clampedSplit = Math.min(Math.max(newSplit, minSplit), maxSplit);\n      setSplitPosition(clampedSplit);\n      onSplitChange?.(clampedSplit);\n    });\n  }, [isDragging, minLeftWidth, minRightWidth, onSplitChange]);\n\n  // Handle mouse up\n  const handleMouseUp = useCallback(() => {\n    if (animationFrameRef.current) {\n      cancelAnimationFrame(animationFrameRef.current);\n    }\n    setIsDragging(false);\n    document.body.style.cursor = '';\n    document.body.style.userSelect = '';\n  }, []);\n\n  // Add global mouse event listeners\n  useEffect(() => {\n    if (isDragging) {\n      document.addEventListener('mousemove', handleMouseMove);\n      document.addEventListener('mouseup', handleMouseUp);\n      return () => {\n        document.removeEventListener('mousemove', handleMouseMove);\n        document.removeEventListener('mouseup', handleMouseUp);\n      };\n    }\n  }, [isDragging, handleMouseMove, handleMouseUp]);\n\n  // Handle keyboard navigation\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (!containerRef.current) return;\n\n    const step = e.shiftKey ? 10 : 2; // Larger steps with shift\n    const containerWidth = containerRef.current.offsetWidth;\n    const minSplit = (minLeftWidth / containerWidth) * 100;\n    const maxSplit = 100 - (minRightWidth / containerWidth) * 100;\n\n    let newSplit = splitPosition;\n\n    switch (e.key) {\n      case 'ArrowLeft':\n        e.preventDefault();\n        newSplit = Math.max(splitPosition - step, minSplit);\n        break;\n      case 'ArrowRight':\n        e.preventDefault();\n        newSplit = Math.min(splitPosition + step, maxSplit);\n        break;\n      case 'Home':\n        e.preventDefault();\n        newSplit = minSplit;\n        break;\n      case 'End':\n        e.preventDefault();\n        newSplit = maxSplit;\n        break;\n      default:\n        return;\n    }\n\n    setSplitPosition(newSplit);\n    onSplitChange?.(newSplit);\n  };\n\n  return (\n    <div \n      ref={containerRef}\n      className={cn(\"flex h-full w-full relative\", className)}\n    >\n      {/* Left pane */}\n      <div \n        className=\"h-full overflow-hidden\"\n        style={{ width: `${splitPosition}%` }}\n      >\n        {left}\n      </div>\n\n      {/* Divider */}\n      <div\n        className={cn(\n          \"relative flex-shrink-0 group\",\n          \"w-1 hover:w-2 transition-all duration-150\",\n          \"bg-border hover:bg-primary/50\",\n          \"cursor-col-resize\",\n          isDragging && \"bg-primary w-2\"\n        )}\n        onMouseDown={handleMouseDown}\n        onKeyDown={handleKeyDown}\n        tabIndex={0}\n        role=\"separator\"\n        aria-label=\"Resize panes\"\n        aria-valuenow={Math.round(splitPosition)}\n        aria-valuemin={0}\n        aria-valuemax={100}\n      >\n        {/* Expand hit area for easier dragging */}\n        <div className=\"absolute inset-y-0 -left-2 -right-2 z-10\" />\n        \n        {/* Visual indicator dots */}\n        <div className={cn(\n          \"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2\",\n          \"flex flex-col items-center justify-center gap-1\",\n          \"opacity-0 group-hover:opacity-100 transition-opacity\",\n          isDragging && \"opacity-100\"\n        )}>\n          <div className=\"w-1 h-1 bg-primary rounded-full\" />\n          <div className=\"w-1 h-1 bg-primary rounded-full\" />\n          <div className=\"w-1 h-1 bg-primary rounded-full\" />\n        </div>\n      </div>\n\n      {/* Right pane */}\n      <div \n        className=\"h-full overflow-hidden flex-1\"\n        style={{ width: `${100 - splitPosition}%` }}\n      >\n        {right}\n      </div>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/ui/switch.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SwitchProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {\n  /**\n   * Whether the switch is checked\n   */\n  checked?: boolean;\n  /**\n   * Callback when the switch state changes\n   */\n  onCheckedChange?: (checked: boolean) => void;\n}\n\n/**\n * Switch component for toggling boolean values\n * \n * @example\n * <Switch checked={isEnabled} onCheckedChange={setIsEnabled} />\n */\nconst Switch = React.forwardRef<HTMLInputElement, SwitchProps>(\n  ({ className, checked, onCheckedChange, disabled, ...props }, ref) => {\n    return (\n      <button\n        type=\"button\"\n        role=\"switch\"\n        aria-checked={checked}\n        disabled={disabled}\n        onClick={() => onCheckedChange?.(!checked)}\n        className={cn(\n          \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        style={{\n          backgroundColor: checked ? \"var(--color-primary)\" : \"var(--color-muted)\"\n        }}\n      >\n        <span\n          className={cn(\n            \"pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform\",\n            checked ? \"translate-x-4\" : \"translate-x-0\"\n          )}\n          style={{\n            backgroundColor: \"var(--color-background)\"\n          }}\n        />\n        <input\n          ref={ref}\n          type=\"checkbox\"\n          checked={checked}\n          disabled={disabled}\n          className=\"sr-only\"\n          onChange={() => {}}\n          {...props}\n        />\n      </button>\n    );\n  }\n);\n\nSwitch.displayName = \"Switch\";\n\nexport { Switch }; "
  },
  {
    "path": "src/components/ui/tabs.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst TabsContext = React.createContext<{\n  value: string;\n  onValueChange: (value: string) => void;\n}>({\n  value: \"\",\n  onValueChange: () => {},\n});\n\nexport interface TabsProps {\n  /**\n   * The controlled value of the tab to activate\n   */\n  value: string;\n  /**\n   * Event handler called when the value changes\n   */\n  onValueChange: (value: string) => void;\n  /**\n   * The tabs and their content\n   */\n  children: React.ReactNode;\n  /**\n   * Additional CSS classes\n   */\n  className?: string;\n}\n\n/**\n * Root tabs component\n * \n * @example\n * <Tabs value={activeTab} onValueChange={setActiveTab}>\n *   <TabsList>\n *     <TabsTrigger value=\"general\">General</TabsTrigger>\n *   </TabsList>\n *   <TabsContent value=\"general\">Content</TabsContent>\n * </Tabs>\n */\nconst Tabs: React.FC<TabsProps> = ({\n  value,\n  onValueChange,\n  children,\n  className,\n}) => {\n  return (\n    <TabsContext.Provider value={{ value, onValueChange }}>\n      <div className={cn(\"w-full\", className)}>{children}</div>\n    </TabsContext.Provider>\n  );\n};\n\nexport interface TabsListProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\n/**\n * Container for tab triggers\n */\nconst TabsList = React.forwardRef<HTMLDivElement, TabsListProps>(\n  ({ className, ...props }, ref) => (\n    <div\n      ref={ref}\n      className={cn(\n        \"flex h-9 items-center justify-start rounded-lg p-1\",\n        className\n      )}\n      style={{\n        backgroundColor: \"var(--color-muted)\",\n        color: \"var(--color-muted-foreground)\"\n      }}\n      {...props}\n    />\n  )\n);\n\nTabsList.displayName = \"TabsList\";\n\nexport interface TabsTriggerProps {\n  value: string;\n  children: React.ReactNode;\n  className?: string;\n  disabled?: boolean;\n}\n\n/**\n * Individual tab trigger button\n */\nconst TabsTrigger = React.forwardRef<\n  HTMLButtonElement,\n  TabsTriggerProps\n>(({ className, value, disabled, ...props }, ref) => {\n  const { value: selectedValue, onValueChange } = React.useContext(TabsContext);\n  const isSelected = selectedValue === value;\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      role=\"tab\"\n      aria-selected={isSelected}\n      disabled={disabled}\n      onClick={() => onValueChange(value)}\n      className={cn(\n        \"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n      style={{\n        backgroundColor: isSelected ? \"var(--color-background)\" : \"transparent\",\n        color: isSelected ? \"var(--color-foreground)\" : \"inherit\",\n        boxShadow: isSelected ? \"0 1px 2px rgba(0,0,0,0.1)\" : \"none\"\n      }}\n      {...props}\n    />\n  );\n});\n\nTabsTrigger.displayName = \"TabsTrigger\";\n\nexport interface TabsContentProps {\n  value: string;\n  children: React.ReactNode;\n  className?: string;\n}\n\n/**\n * Tab content panel\n */\nconst TabsContent = React.forwardRef<\n  HTMLDivElement,\n  TabsContentProps\n>(({ className, value, ...props }, ref) => {\n  const { value: selectedValue } = React.useContext(TabsContext);\n  const isSelected = selectedValue === value;\n\n  if (!isSelected) return null;\n\n  return (\n    <div\n      ref={ref}\n      role=\"tabpanel\"\n      className={cn(\n        \"mt-2\",\n        className\n      )}\n      {...props}\n    />\n  );\n});\n\nTabsContent.displayName = \"TabsContent\";\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }; "
  },
  {
    "path": "src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea } "
  },
  {
    "path": "src/components/ui/toast.tsx",
    "content": "import * as React from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { X, CheckCircle, AlertCircle, Info } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToastType = \"success\" | \"error\" | \"info\";\n\ninterface ToastProps {\n  /**\n   * The message to display\n   */\n  message: string;\n  /**\n   * The type of toast\n   */\n  type?: ToastType;\n  /**\n   * Duration in milliseconds before auto-dismiss\n   */\n  duration?: number;\n  /**\n   * Callback when the toast is dismissed\n   */\n  onDismiss?: () => void;\n  /**\n   * Optional className for styling\n   */\n  className?: string;\n}\n\n/**\n * Toast component for showing temporary notifications\n * \n * @example\n * <Toast\n *   message=\"File saved successfully\"\n *   type=\"success\"\n *   duration={3000}\n *   onDismiss={() => setShowToast(false)}\n * />\n */\nexport const Toast: React.FC<ToastProps> = ({\n  message,\n  type = \"info\",\n  duration = 3000,\n  onDismiss,\n  className,\n}) => {\n  React.useEffect(() => {\n    if (duration && duration > 0) {\n      const timer = setTimeout(() => {\n        onDismiss?.();\n      }, duration);\n      \n      return () => clearTimeout(timer);\n    }\n  }, [duration, onDismiss]);\n  \n  const icons = {\n    success: <CheckCircle className=\"h-4 w-4\" />,\n    error: <AlertCircle className=\"h-4 w-4\" />,\n    info: <Info className=\"h-4 w-4\" />,\n  };\n  \n  const colors = {\n    success: \"text-green-500\",\n    error: \"text-red-500\",\n    info: \"text-primary\",\n  };\n  \n  return (\n    <motion.div\n      initial={{ opacity: 0, y: 50, scale: 0.95 }}\n      animate={{ opacity: 1, y: 0, scale: 1 }}\n      exit={{ opacity: 0, y: 20, scale: 0.95 }}\n      transition={{ duration: 0.2 }}\n      className={cn(\n        \"flex items-center space-x-3 rounded-lg border border-border bg-card px-4 py-3 shadow-lg\",\n        className\n      )}\n    >\n      <span className={colors[type]}>{icons[type]}</span>\n      <span className=\"flex-1 text-sm\">{message}</span>\n      {onDismiss && (\n        <button\n          onClick={onDismiss}\n          className=\"text-muted-foreground hover:text-foreground transition-colors\"\n        >\n          <X className=\"h-4 w-4\" />\n        </button>\n      )}\n    </motion.div>\n  );\n};\n\n// Toast container for positioning\ninterface ToastContainerProps {\n  children: React.ReactNode;\n}\n\nexport const ToastContainer: React.FC<ToastContainerProps> = ({ children }) => {\n  return (\n    <div className=\"fixed bottom-0 left-0 right-0 z-50 flex justify-center p-4 pointer-events-none\">\n      <div className=\"pointer-events-auto\">\n        <AnimatePresence mode=\"wait\">\n          {children}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}; "
  },
  {
    "path": "src/components/ui/tooltip-modern.tsx",
    "content": "import * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport { cn } from \"@/lib/utils\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 6, ...props }, ref) => (\n  <TooltipPrimitive.Portal>\n    <TooltipPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-[100] overflow-hidden rounded-lg border border-border bg-popover px-3 py-2\",\n        \"text-xs text-popover-foreground shadow-md\",\n        \"animate-in fade-in-0 zoom-in-95\",\n        \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n        \"data-[side=bottom]:slide-in-from-top-2\",\n        \"data-[side=left]:slide-in-from-right-2\",\n        \"data-[side=right]:slide-in-from-left-2\",\n        \"data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </TooltipPrimitive.Portal>\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\ninterface TooltipSimpleProps {\n  content: string\n  children: React.ReactNode\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n  align?: \"start\" | \"center\" | \"end\"\n  delayDuration?: number\n  className?: string\n  contentClassName?: string\n}\n\n/**\n * Simple tooltip wrapper for common use cases\n */\nexport const TooltipSimple: React.FC<TooltipSimpleProps> = ({\n  content,\n  children,\n  side = \"top\",\n  align = \"center\",\n  delayDuration = 200,\n  className,\n  contentClassName,\n}) => {\n  return (\n    <Tooltip delayDuration={delayDuration}>\n      <TooltipTrigger asChild className={className}>\n        {children}\n      </TooltipTrigger>\n      <TooltipContent side={side} align={align} className={contentClassName}>\n        {content}\n      </TooltipContent>\n    </Tooltip>\n  )\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }"
  },
  {
    "path": "src/components/ui/tooltip.tsx",
    "content": "import * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport { cn } from \"@/lib/utils\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n\n "
  },
  {
    "path": "src/components/widgets/BashWidget.tsx",
    "content": "import React from \"react\";\nimport { Terminal, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface BashWidgetProps {\n  command: string;\n  description?: string;\n  result?: any;\n}\n\nexport const BashWidget: React.FC<BashWidgetProps> = ({ command, description, result }) => {\n  // Extract result content if available\n  let resultContent = '';\n  let isError = false;\n  \n  if (result) {\n    isError = result.is_error || false;\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n  }\n  \n  return (\n    <div className=\"rounded-lg border bg-background overflow-hidden\">\n      <div className=\"px-4 py-2 bg-muted/50 flex items-center gap-2 border-b\">\n        <Terminal className=\"h-3.5 w-3.5 text-green-500\" />\n        <span className=\"text-xs font-mono text-muted-foreground\">Terminal</span>\n        {description && (\n          <>\n            <ChevronRight className=\"h-3 w-3 text-muted-foreground\" />\n            <span className=\"text-xs text-muted-foreground\">{description}</span>\n          </>\n        )}\n        {/* Show loading indicator when no result yet */}\n        {!result && (\n          <div className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n            <div className=\"h-2 w-2 bg-green-500 rounded-full animate-pulse\" />\n            <span>Running...</span>\n          </div>\n        )}\n      </div>\n      <div className=\"p-4 space-y-3\">\n        <code className=\"text-xs font-mono text-green-400 block\">\n          $ {command}\n        </code>\n        \n        {/* Show result if available */}\n        {result && (\n          <div className={cn(\n            \"mt-3 p-3 rounded-md border text-xs font-mono whitespace-pre-wrap overflow-x-auto\",\n            isError \n              ? \"border-[color:var(--color-destructive)]/20 bg-[color:var(--color-destructive)]/5 text-[color:var(--color-destructive)]\" \n              : \"border-[color:var(--color-green-500)]/20 bg-[color:var(--color-green-500)]/5 text-[color:var(--color-green-500)]\"\n          )}>\n            {resultContent || (isError ? \"Command failed\" : \"Command completed\")}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};"
  },
  {
    "path": "src/components/widgets/LSWidget.tsx",
    "content": "import React, { useState } from \"react\";\nimport { FolderOpen, Folder, FileCode, FileText, Terminal, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface LSWidgetProps {\n  path: string;\n  result?: any;\n}\n\nexport const LSWidget: React.FC<LSWidgetProps> = ({ path, result }) => {\n  // If we have a result, show it using the LSResultWidget\n  if (result) {\n    let resultContent = '';\n    if (typeof result.content === 'string') {\n      resultContent = result.content;\n    } else if (result.content && typeof result.content === 'object') {\n      if (result.content.text) {\n        resultContent = result.content.text;\n      } else if (Array.isArray(result.content)) {\n        resultContent = result.content\n          .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))\n          .join('\\n');\n      } else {\n        resultContent = JSON.stringify(result.content, null, 2);\n      }\n    }\n    \n    return (\n      <div className=\"space-y-2\">\n        <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n          <FolderOpen className=\"h-4 w-4 text-primary\" />\n          <span className=\"text-sm\">Directory contents for:</span>\n          <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded\">\n            {path}\n          </code>\n        </div>\n        {resultContent && <LSResultWidget content={resultContent} />}\n      </div>\n    );\n  }\n  \n  return (\n    <div className=\"flex items-center gap-2 p-3 rounded-lg bg-muted/50\">\n      <FolderOpen className=\"h-4 w-4 text-primary\" />\n      <span className=\"text-sm\">Listing directory:</span>\n      <code className=\"text-sm font-mono bg-background px-2 py-0.5 rounded\">\n        {path}\n      </code>\n      {!result && (\n        <div className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n          <div className=\"h-2 w-2 bg-blue-500 rounded-full animate-pulse\" />\n          <span>Loading...</span>\n        </div>\n      )}\n    </div>\n  );\n};\n\ninterface LSResultWidgetProps {\n  content: string;\n}\n\nexport const LSResultWidget: React.FC<LSResultWidgetProps> = ({ content }) => {\n  const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());\n  \n  // Parse the directory tree structure\n  const parseDirectoryTree = (rawContent: string) => {\n    const lines = rawContent.split('\\n');\n    const entries: Array<{\n      path: string;\n      name: string;\n      type: 'file' | 'directory';\n      level: number;\n    }> = [];\n    \n    let currentPath: string[] = [];\n    \n    for (const line of lines) {\n      // Skip NOTE section and everything after it\n      if (line.startsWith('NOTE:')) {\n        break;\n      }\n      \n      // Skip empty lines\n      if (!line.trim()) continue;\n      \n      // Calculate indentation level\n      const indent = line.match(/^(\\s*)/)?.[1] || '';\n      const level = Math.floor(indent.length / 2);\n      \n      // Extract the entry name\n      const entryMatch = line.match(/^\\s*-\\s+(.+?)(\\/$)?$/);\n      if (!entryMatch) continue;\n      \n      const fullName = entryMatch[1];\n      const isDirectory = line.trim().endsWith('/');\n      const name = isDirectory ? fullName : fullName;\n      \n      // Update current path based on level\n      currentPath = currentPath.slice(0, level);\n      currentPath.push(name);\n      \n      entries.push({\n        path: currentPath.join('/'),\n        name,\n        type: isDirectory ? 'directory' : 'file',\n        level,\n      });\n    }\n    \n    return entries;\n  };\n  \n  const entries = parseDirectoryTree(content);\n  \n  const toggleDirectory = (path: string) => {\n    setExpandedDirs(prev => {\n      const next = new Set(prev);\n      if (next.has(path)) {\n        next.delete(path);\n      } else {\n        next.add(path);\n      }\n      return next;\n    });\n  };\n  \n  // Group entries by parent for collapsible display\n  const getChildren = (parentPath: string, parentLevel: number) => {\n    return entries.filter(e => {\n      if (e.level !== parentLevel + 1) return false;\n      const parentParts = parentPath.split('/').filter(Boolean);\n      const entryParts = e.path.split('/').filter(Boolean);\n      \n      // Check if this entry is a direct child of the parent\n      if (entryParts.length !== parentParts.length + 1) return false;\n      \n      // Check if all parent parts match\n      for (let i = 0; i < parentParts.length; i++) {\n        if (parentParts[i] !== entryParts[i]) return false;\n      }\n      \n      return true;\n    });\n  };\n  \n  const renderEntry = (entry: typeof entries[0], isRoot = false) => {\n    const hasChildren = entry.type === 'directory' && \n      entries.some(e => e.path.startsWith(entry.path + '/') && e.level === entry.level + 1);\n    const isExpanded = expandedDirs.has(entry.path) || isRoot;\n    \n    const getIcon = () => {\n      if (entry.type === 'directory') {\n        return isExpanded ? \n          <FolderOpen className=\"h-3.5 w-3.5 text-blue-500\" /> : \n          <Folder className=\"h-3.5 w-3.5 text-blue-500\" />;\n      }\n      \n      // File type icons based on extension\n      const ext = entry.name.split('.').pop()?.toLowerCase();\n      switch (ext) {\n        case 'rs':\n          return <FileCode className=\"h-3.5 w-3.5 text-orange-500\" />;\n        case 'toml':\n        case 'yaml':\n        case 'yml':\n        case 'json':\n          return <FileText className=\"h-3.5 w-3.5 text-yellow-500\" />;\n        case 'md':\n          return <FileText className=\"h-3.5 w-3.5 text-blue-400\" />;\n        case 'js':\n        case 'jsx':\n        case 'ts':\n        case 'tsx':\n          return <FileCode className=\"h-3.5 w-3.5 text-yellow-400\" />;\n        case 'py':\n          return <FileCode className=\"h-3.5 w-3.5 text-blue-500\" />;\n        case 'go':\n          return <FileCode className=\"h-3.5 w-3.5 text-cyan-500\" />;\n        case 'sh':\n        case 'bash':\n          return <Terminal className=\"h-3.5 w-3.5 text-green-500\" />;\n        default:\n          return <FileText className=\"h-3.5 w-3.5 text-muted-foreground\" />;\n      }\n    };\n    \n    return (\n      <div key={entry.path}>\n        <div \n          className={cn(\n            \"flex items-center gap-2 py-1 px-2 rounded hover:bg-muted/50 transition-colors cursor-pointer\",\n            !isRoot && \"ml-4\"\n          )}\n          onClick={() => entry.type === 'directory' && hasChildren && toggleDirectory(entry.path)}\n        >\n          {entry.type === 'directory' && hasChildren && (\n            <ChevronRight className={cn(\n              \"h-3 w-3 text-muted-foreground transition-transform\",\n              isExpanded && \"rotate-90\"\n            )} />\n          )}\n          {(!hasChildren || entry.type !== 'directory') && (\n            <div className=\"w-3\" />\n          )}\n          {getIcon()}\n          <span className=\"text-sm font-mono\">{entry.name}</span>\n        </div>\n        \n        {entry.type === 'directory' && hasChildren && isExpanded && (\n          <div className=\"ml-2\">\n            {getChildren(entry.path, entry.level).map(child => renderEntry(child))}\n          </div>\n        )}\n      </div>\n    );\n  };\n  \n  // Get root entries\n  const rootEntries = entries.filter(e => e.level === 0);\n  \n  return (\n    <div className=\"rounded-lg border bg-muted/20 p-3\">\n      <div className=\"space-y-1\">\n        {rootEntries.map(entry => renderEntry(entry, true))}\n      </div>\n    </div>\n  );\n};"
  },
  {
    "path": "src/components/widgets/TodoWidget.tsx",
    "content": "import React from \"react\";\nimport { CheckCircle2, Circle, Clock, FileEdit } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\n\ninterface TodoWidgetProps {\n  todos: any[];\n  result?: any;\n}\n\nexport const TodoWidget: React.FC<TodoWidgetProps> = ({ todos, result: _result }) => {\n  const statusIcons = {\n    completed: <CheckCircle2 className=\"h-4 w-4 text-green-500\" />,\n    in_progress: <Clock className=\"h-4 w-4 text-blue-500 animate-pulse\" />,\n    pending: <Circle className=\"h-4 w-4 text-muted-foreground\" />\n  };\n\n  const priorityColors = {\n    high: \"bg-red-500/10 text-red-500 border-red-500/20\",\n    medium: \"bg-yellow-500/10 text-yellow-500 border-yellow-500/20\",\n    low: \"bg-green-500/10 text-green-500 border-green-500/20\"\n  };\n\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2 mb-3\">\n        <FileEdit className=\"h-4 w-4 text-primary\" />\n        <span className=\"text-sm font-medium\">Todo List</span>\n      </div>\n      <div className=\"space-y-2\">\n        {todos.map((todo, idx) => (\n          <div\n            key={todo.id || idx}\n            className={cn(\n              \"flex items-start gap-3 p-3 rounded-lg border bg-card/50\",\n              todo.status === \"completed\" && \"opacity-60\"\n            )}\n          >\n            <div className=\"mt-0.5\">\n              {statusIcons[todo.status as keyof typeof statusIcons] || statusIcons.pending}\n            </div>\n            <div className=\"flex-1 space-y-1\">\n              <p className={cn(\n                \"text-sm\",\n                todo.status === \"completed\" && \"line-through\"\n              )}>\n                {todo.content}\n              </p>\n              {todo.priority && (\n                <Badge \n                  variant=\"outline\" \n                  className={cn(\"text-xs\", priorityColors[todo.priority as keyof typeof priorityColors])}\n                >\n                  {todo.priority}\n                </Badge>\n              )}\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};"
  },
  {
    "path": "src/components/widgets/index.ts",
    "content": "// Re-export all widgets from their individual files\nexport { TodoWidget } from './TodoWidget';\nexport { LSWidget } from './LSWidget';\nexport { BashWidget } from './BashWidget';\n\n// TODO: Add these widgets as they are implemented\n// export { LSResultWidget } from './LSWidget';\n// export { ReadWidget } from './ReadWidget';\n// export { ReadResultWidget } from './ReadResultWidget';\n// export { GlobWidget } from './GlobWidget';\n// export { WriteWidget } from './WriteWidget';\n// export { GrepWidget } from './GrepWidget';\n// export { EditWidget } from './EditWidget';\n// export { EditResultWidget } from './EditResultWidget';\n// export { MCPWidget } from './MCPWidget';\n// export { CommandWidget } from './CommandWidget';\n// export { CommandOutputWidget } from './CommandOutputWidget';\n// export { SummaryWidget } from './SummaryWidget';\n// export { MultiEditWidget } from './MultiEditWidget';\n// export { MultiEditResultWidget } from './MultiEditResultWidget';\n// export { SystemReminderWidget } from './SystemReminderWidget';\n// export { SystemInitializedWidget } from './SystemInitializedWidget';\n// export { TaskWidget } from './TaskWidget';\n// export { WebSearchWidget } from './WebSearchWidget';\n// export { ThinkingWidget } from './ThinkingWidget';\n// export { WebFetchWidget } from './WebFetchWidget';\n// export { TodoReadWidget } from './TodoReadWidget';"
  },
  {
    "path": "src/contexts/TabContext.tsx",
    "content": "import React, { createContext, useState, useContext, useCallback, useEffect, useRef } from 'react';\nimport { TabPersistenceService } from '@/services/tabPersistence';\nimport { SessionPersistenceService } from '@/services/sessionPersistence';\n\nexport interface Tab {\n  id: string;\n  type: 'chat' | 'agent' | 'agents' | 'projects' | 'usage' | 'mcp' | 'settings' | 'claude-md' | 'claude-file' | 'agent-execution' | 'create-agent' | 'import-agent';\n  title: string;\n  sessionId?: string;  // for chat tabs\n  sessionData?: any; // for chat tabs - stores full session object\n  agentRunId?: string; // for agent tabs\n  agentData?: any; // for agent-execution tabs\n  claudeFileId?: string; // for claude-file tabs\n  initialProjectPath?: string; // for chat tabs\n  projectPath?: string; // for agent-execution tabs\n  status: 'active' | 'idle' | 'running' | 'complete' | 'error';\n  hasUnsavedChanges: boolean;\n  order: number;\n  icon?: string;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\ninterface TabContextType {\n  tabs: Tab[];\n  activeTabId: string | null;\n  addTab: (tab: Omit<Tab, 'id' | 'order' | 'createdAt' | 'updatedAt'>) => string;\n  removeTab: (id: string) => void;\n  updateTab: (id: string, updates: Partial<Tab>) => void;\n  setActiveTab: (id: string) => void;\n  reorderTabs: (startIndex: number, endIndex: number) => void;\n  getTabById: (id: string) => Tab | undefined;\n  closeAllTabs: () => void;\n  getTabsByType: (type: 'chat' | 'agent') => Tab[];\n}\n\nconst TabContext = createContext<TabContextType | undefined>(undefined);\n\n// const STORAGE_KEY = 'opcode_tabs'; // No longer needed - persistence disabled\nconst MAX_TABS = 20;\n\nexport const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n  const [tabs, setTabs] = useState<Tab[]>([]);\n  const [activeTabId, setActiveTabId] = useState<string | null>(null);\n  const isInitialized = useRef(false);\n  const saveTimeoutRef = useRef<NodeJS.Timeout>();\n\n  // Load tabs from storage on mount\n  useEffect(() => {\n    const loadTabs = async () => {\n    if (isInitialized.current) return;\n    isInitialized.current = true;\n\n    // Migrate from old format if needed\n    TabPersistenceService.migrateFromOldFormat();\n\n    // Try to load saved tabs\n    const { tabs: savedTabs, activeTabId: savedActiveTabId } = TabPersistenceService.loadTabs();\n    \n    if (savedTabs.length > 0) {\n      // For chat tabs, restore session data\n      const restoredTabs = await Promise.all(savedTabs.map(async (tab) => {\n        if (tab.type === 'chat' && tab.sessionId) {\n          // Check if session can be restored\n          const sessionData = SessionPersistenceService.loadSession(tab.sessionId);\n          if (sessionData) {\n            // Create a Session object for the tab\n            const session = SessionPersistenceService.createSessionFromRestoreData(sessionData);\n            return {\n              ...tab,\n              sessionData: session,\n              initialProjectPath: sessionData.projectPath\n            };\n          }\n        }\n        return tab;\n      }));\n      \n      setTabs(restoredTabs);\n      setActiveTabId(savedActiveTabId);\n    } else {\n      // Create default projects tab if no saved tabs\n      const defaultTab: Tab = {\n        id: generateTabId(),\n        type: 'projects',\n        title: 'Projects',\n        status: 'idle',\n        hasUnsavedChanges: false,\n        order: 0,\n        createdAt: new Date(),\n        updatedAt: new Date()\n      };\n      setTabs([defaultTab]);\n      setActiveTabId(defaultTab.id);\n    }\n    };\n    \n    loadTabs();\n  }, []);\n\n  // Save tabs to localStorage with debounce\n  useEffect(() => {\n    // Don't save if not initialized\n    if (!isInitialized.current) return;\n    \n    // Clear existing timeout\n    if (saveTimeoutRef.current) {\n      clearTimeout(saveTimeoutRef.current);\n    }\n\n    // Debounce saving to avoid excessive writes\n    saveTimeoutRef.current = setTimeout(() => {\n      TabPersistenceService.saveTabs(tabs, activeTabId);\n    }, 500); // Wait 500ms after last change before saving\n\n    return () => {\n      if (saveTimeoutRef.current) {\n        clearTimeout(saveTimeoutRef.current);\n      }\n    };\n  }, [tabs, activeTabId]);\n\n  // Save tabs immediately when window is about to close\n  useEffect(() => {\n    const handleBeforeUnload = () => {\n      if (isInitialized.current && tabs.length > 0) {\n        TabPersistenceService.saveTabs(tabs, activeTabId);\n      }\n    };\n\n    window.addEventListener('beforeunload', handleBeforeUnload);\n    \n    return () => {\n      window.removeEventListener('beforeunload', handleBeforeUnload);\n      // Save one final time when component unmounts\n      if (isInitialized.current && tabs.length > 0) {\n        TabPersistenceService.saveTabs(tabs, activeTabId);\n      }\n    };\n  }, [tabs, activeTabId]);\n\n  const generateTabId = () => {\n    return `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n  };\n\n  const addTab = useCallback((tabData: Omit<Tab, 'id' | 'order' | 'createdAt' | 'updatedAt'>): string => {\n    if (tabs.length >= MAX_TABS) {\n      throw new Error(`Maximum number of tabs (${MAX_TABS}) reached`);\n    }\n\n    const newTab: Tab = {\n      ...tabData,\n      id: generateTabId(),\n      order: tabs.length,\n      createdAt: new Date(),\n      updatedAt: new Date()\n    };\n\n    setTabs(prevTabs => [...prevTabs, newTab]);\n    setActiveTabId(newTab.id);\n    return newTab.id;\n  }, [tabs.length]);\n\n  const removeTab = useCallback((id: string) => {\n    setTabs(prevTabs => {\n      const filteredTabs = prevTabs.filter(tab => tab.id !== id);\n      \n      // Reorder remaining tabs\n      const reorderedTabs = filteredTabs.map((tab, index) => ({\n        ...tab,\n        order: index\n      }));\n\n      // Update active tab if necessary\n      if (activeTabId === id && reorderedTabs.length > 0) {\n        const removedTabIndex = prevTabs.findIndex(tab => tab.id === id);\n        const newActiveIndex = Math.min(removedTabIndex, reorderedTabs.length - 1);\n        setActiveTabId(reorderedTabs[newActiveIndex].id);\n      } else if (reorderedTabs.length === 0) {\n        setActiveTabId(null);\n      }\n\n      return reorderedTabs;\n    });\n  }, [activeTabId]);\n\n  const updateTab = useCallback((id: string, updates: Partial<Tab>) => {\n    setTabs(prevTabs => \n      prevTabs.map(tab => \n        tab.id === id \n          ? { ...tab, ...updates, updatedAt: new Date() }\n          : tab\n      )\n    );\n  }, []);\n\n  const setActiveTab = useCallback((id: string) => {\n    if (tabs.find(tab => tab.id === id)) {\n      setActiveTabId(id);\n    }\n  }, [tabs]);\n\n  const reorderTabs = useCallback((startIndex: number, endIndex: number) => {\n    setTabs(prevTabs => {\n      const newTabs = [...prevTabs];\n      const [removed] = newTabs.splice(startIndex, 1);\n      newTabs.splice(endIndex, 0, removed);\n      \n      // Update order property\n      return newTabs.map((tab, index) => ({\n        ...tab,\n        order: index\n      }));\n    });\n  }, []);\n\n  const getTabById = useCallback((id: string): Tab | undefined => {\n    return tabs.find(tab => tab.id === id);\n  }, [tabs]);\n\n  const closeAllTabs = useCallback(() => {\n    setTabs([]);\n    setActiveTabId(null);\n    TabPersistenceService.clearTabs();\n  }, []);\n\n  const getTabsByType = useCallback((type: 'chat' | 'agent'): Tab[] => {\n    return tabs.filter(tab => tab.type === type);\n  }, [tabs]);\n\n  const value: TabContextType = {\n    tabs,\n    activeTabId,\n    addTab,\n    removeTab,\n    updateTab,\n    setActiveTab,\n    reorderTabs,\n    getTabById,\n    closeAllTabs,\n    getTabsByType\n  };\n\n  return (\n    <TabContext.Provider value={value}>\n      {children}\n    </TabContext.Provider>\n  );\n};\n\nexport const useTabContext = () => {\n  const context = useContext(TabContext);\n  if (!context) {\n    throw new Error('useTabContext must be used within a TabProvider');\n  }\n  return context;\n};\n"
  },
  {
    "path": "src/contexts/ThemeContext.tsx",
    "content": "import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';\nimport { api } from '../lib/api';\n\nexport type ThemeMode = 'dark' | 'gray' | 'light' | 'custom';\n\nexport interface CustomThemeColors {\n  background: string;\n  foreground: string;\n  card: string;\n  cardForeground: string;\n  primary: string;\n  primaryForeground: string;\n  secondary: string;\n  secondaryForeground: string;\n  muted: string;\n  mutedForeground: string;\n  accent: string;\n  accentForeground: string;\n  destructive: string;\n  destructiveForeground: string;\n  border: string;\n  input: string;\n  ring: string;\n}\n\ninterface ThemeContextType {\n  theme: ThemeMode;\n  customColors: CustomThemeColors;\n  setTheme: (theme: ThemeMode) => Promise<void>;\n  setCustomColors: (colors: Partial<CustomThemeColors>) => Promise<void>;\n  isLoading: boolean;\n}\n\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\nconst THEME_STORAGE_KEY = 'theme_preference';\nconst CUSTOM_COLORS_STORAGE_KEY = 'theme_custom_colors';\n\n// Default custom theme colors (based on current dark theme)\nconst DEFAULT_CUSTOM_COLORS: CustomThemeColors = {\n  background: 'oklch(0.12 0.01 240)',\n  foreground: 'oklch(0.98 0.01 240)',\n  card: 'oklch(0.14 0.01 240)',\n  cardForeground: 'oklch(0.98 0.01 240)',\n  primary: 'oklch(0.98 0.01 240)',\n  primaryForeground: 'oklch(0.12 0.01 240)',\n  secondary: 'oklch(0.16 0.01 240)',\n  secondaryForeground: 'oklch(0.98 0.01 240)',\n  muted: 'oklch(0.16 0.01 240)',\n  mutedForeground: 'oklch(0.65 0.01 240)',\n  accent: 'oklch(0.16 0.01 240)',\n  accentForeground: 'oklch(0.98 0.01 240)',\n  destructive: 'oklch(0.6 0.2 25)',\n  destructiveForeground: 'oklch(0.98 0.01 240)',\n  border: 'oklch(0.16 0.01 240)',\n  input: 'oklch(0.16 0.01 240)',\n  ring: 'oklch(0.98 0.01 240)',\n};\n\nexport const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n  const [theme, setThemeState] = useState<ThemeMode>('gray');\n  const [customColors, setCustomColorsState] = useState<CustomThemeColors>(DEFAULT_CUSTOM_COLORS);\n  const [isLoading, setIsLoading] = useState(true);\n\n  // Load theme preference and custom colors from storage\n  useEffect(() => {\n    const loadTheme = async () => {\n      try {\n        // Load theme preference\n        const savedTheme = await api.getSetting(THEME_STORAGE_KEY);\n        \n        if (savedTheme) {\n          const themeMode = savedTheme as ThemeMode;\n          setThemeState(themeMode);\n          await applyTheme(themeMode, customColors);\n        } else {\n          // No saved preference: apply gray as the default theme\n          setThemeState('gray');\n          await applyTheme('gray', customColors);\n        }\n\n        // Load custom colors\n        const savedColors = await api.getSetting(CUSTOM_COLORS_STORAGE_KEY);\n        \n        if (savedColors) {\n          const colors = JSON.parse(savedColors) as CustomThemeColors;\n          setCustomColorsState(colors);\n          if (theme === 'custom') {\n            await applyTheme('custom', colors);\n          }\n        }\n      } catch (error) {\n        console.error('Failed to load theme settings:', error);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    loadTheme();\n  }, []);\n\n  // Apply theme to document\n  const applyTheme = useCallback(async (themeMode: ThemeMode, colors: CustomThemeColors) => {\n    const root = document.documentElement;\n    \n    // Remove all theme classes\n    root.classList.remove('theme-dark', 'theme-gray', 'theme-light', 'theme-custom');\n    \n    // Add new theme class\n    root.classList.add(`theme-${themeMode}`);\n    \n    // If custom theme, apply custom colors as CSS variables\n    if (themeMode === 'custom') {\n      Object.entries(colors).forEach(([key, value]) => {\n        const cssVarName = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;\n        root.style.setProperty(cssVarName, value);\n      });\n    } else {\n      // Clear custom CSS variables when not using custom theme\n      Object.keys(colors).forEach((key) => {\n        const cssVarName = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;\n        root.style.removeProperty(cssVarName);\n      });\n    }\n\n    // Note: Window theme updates removed since we're using custom titlebar\n  }, []);\n\n  const setTheme = useCallback(async (newTheme: ThemeMode) => {\n    try {\n      setIsLoading(true);\n      \n      // Apply theme immediately\n      setThemeState(newTheme);\n      await applyTheme(newTheme, customColors);\n      \n      // Save to storage\n      await api.saveSetting(THEME_STORAGE_KEY, newTheme);\n    } catch (error) {\n      console.error('Failed to save theme preference:', error);\n    } finally {\n      setIsLoading(false);\n    }\n  }, [customColors, applyTheme]);\n\n  const setCustomColors = useCallback(async (colors: Partial<CustomThemeColors>) => {\n    try {\n      setIsLoading(true);\n      \n      const newColors = { ...customColors, ...colors };\n      setCustomColorsState(newColors);\n      \n      // Apply immediately if custom theme is active\n      if (theme === 'custom') {\n        await applyTheme('custom', newColors);\n      }\n      \n      // Save to storage\n      await api.saveSetting(CUSTOM_COLORS_STORAGE_KEY, JSON.stringify(newColors));\n    } catch (error) {\n      console.error('Failed to save custom colors:', error);\n    } finally {\n      setIsLoading(false);\n    }\n  }, [theme, customColors, applyTheme]);\n\n  const value: ThemeContextType = {\n    theme,\n    customColors,\n    setTheme,\n    setCustomColors,\n    isLoading,\n  };\n\n  return (\n    <ThemeContext.Provider value={value}>\n      {children}\n    </ThemeContext.Provider>\n  );\n};\n\nexport const useThemeContext = () => {\n  const context = useContext(ThemeContext);\n  if (!context) {\n    throw new Error('useThemeContext must be used within a ThemeProvider');\n  }\n  return context;\n};\n"
  },
  {
    "path": "src/hooks/index.ts",
    "content": "// Export all custom hooks from a single entry point\nexport { useLoadingState } from './useLoadingState';\nexport { useDebounce, useDebouncedCallback } from './useDebounce';\nexport { useApiCall } from './useApiCall';\nexport { usePagination } from './usePagination';\nexport { useTheme } from './useTheme';\nexport { \n  useAnalytics, \n  useTrackEvent, \n  usePageView, \n  useAppLifecycle,\n  useComponentMetrics,\n  useInteractionTracking,\n  useScreenTracking,\n  useFeatureExperiment,\n  usePathTracking,\n  useFeatureAdoptionTracking,\n  useWorkflowTracking,\n  useAIInteractionTracking,\n  useNetworkPerformanceTracking\n} from './useAnalytics';\nexport { \n  usePerformanceMonitor, \n  useAsyncPerformanceTracker \n} from './usePerformanceMonitor';\nexport { TAB_SCREEN_NAMES } from './useAnalytics';\n"
  },
  {
    "path": "src/hooks/useAnalytics.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react';\nimport { analytics, ANALYTICS_EVENTS, eventBuilders } from '@/lib/analytics';\nimport type { EventName } from '@/lib/analytics/types';\n\n// Screen name mapping for tab types\nconst TAB_SCREEN_NAMES: Record<string, string> = {\n  'chat': 'chat_session',\n  'agent': 'agent_view',\n  'projects': 'projects_list',\n  'usage': 'usage_dashboard',\n  'mcp': 'mcp_manager',\n  'settings': 'settings',\n  'claude-md': 'markdown_editor',\n  'claude-file': 'file_editor',\n  'agent-execution': 'agent_execution',\n  'create-agent': 'create_agent',\n  'import-agent': 'import_agent',\n};\n\ninterface UseAnalyticsReturn {\n  track: (eventName: EventName | string, properties?: Record<string, any>) => void;\n  trackEvent: ReturnType<typeof useTrackEvent>;\n  isEnabled: boolean;\n  hasConsented: boolean;\n}\n\nexport function useAnalytics(): UseAnalyticsReturn {\n  const isEnabled = analytics.isEnabled();\n  const hasConsented = analytics.hasConsented();\n  \n  const track = useCallback((eventName: EventName | string, properties?: Record<string, any>) => {\n    analytics.track(eventName, properties);\n  }, []);\n  \n  const trackEvent = useTrackEvent();\n  \n  return {\n    track,\n    trackEvent,\n    isEnabled,\n    hasConsented,\n  };\n}\n\nexport function useTrackEvent() {\n  return {\n    // Session events\n    sessionCreated: (model: string, source?: string) => {\n      const event = eventBuilders.session({ model, source });\n      analytics.track(event.event, event.properties);\n    },\n    \n    sessionCompleted: () => {\n      analytics.track(ANALYTICS_EVENTS.SESSION_COMPLETED);\n    },\n    \n    sessionResumed: (checkpointId: string) => {\n      const event = eventBuilders.session({ resumed: true, checkpoint_id: checkpointId });\n      analytics.track(ANALYTICS_EVENTS.SESSION_RESUMED, event.properties);\n    },\n    \n    // Feature usage\n    featureUsed: (feature: string, subfeature?: string, metadata?: Record<string, any>) => {\n      const event = eventBuilders.feature(feature, subfeature, metadata);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Model selection\n    modelSelected: (newModel: string, previousModel?: string, source?: string) => {\n      const event = eventBuilders.model(newModel, previousModel, source);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Tab events\n    tabCreated: (tabType: string) => {\n      analytics.track(ANALYTICS_EVENTS.TAB_CREATED, { tab_type: tabType });\n    },\n    \n    tabClosed: (tabType: string) => {\n      analytics.track(ANALYTICS_EVENTS.TAB_CLOSED, { tab_type: tabType });\n    },\n    \n    // File operations\n    fileOpened: (fileType: string) => {\n      analytics.track(ANALYTICS_EVENTS.FILE_OPENED, { file_type: fileType });\n    },\n    \n    fileEdited: (fileType: string) => {\n      analytics.track(ANALYTICS_EVENTS.FILE_EDITED, { file_type: fileType });\n    },\n    \n    fileSaved: (fileType: string) => {\n      analytics.track(ANALYTICS_EVENTS.FILE_SAVED, { file_type: fileType });\n    },\n    \n    // Agent execution\n    agentExecuted: (agentType: string, success: boolean, agentName?: string, durationMs?: number) => {\n      const event = eventBuilders.agent(agentType, success, agentName, durationMs);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // MCP events\n    mcpServerConnected: (serverName: string, success: boolean, serverType?: string) => {\n      const event = eventBuilders.mcp(serverName, success, serverType);\n      analytics.track(event.event, event.properties);\n    },\n    \n    mcpServerDisconnected: (serverName: string) => {\n      analytics.track(ANALYTICS_EVENTS.MCP_SERVER_DISCONNECTED, { server_name: serverName });\n    },\n    \n    // Slash commands\n    slashCommandUsed: (command: string, success: boolean) => {\n      const event = eventBuilders.slashCommand(command, success);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Settings\n    settingsChanged: (setting: string, value: any) => {\n      analytics.track(ANALYTICS_EVENTS.SETTINGS_CHANGED, { setting, value });\n    },\n    \n    // Errors\n    errorOccurred: (errorType: string, errorCode?: string, context?: string) => {\n      const event = eventBuilders.error(errorType, errorCode, context);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Performance\n    performanceMetrics: (metrics: Record<string, number>) => {\n      const event = eventBuilders.performance(metrics);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Claude Code Session events\n    promptSubmitted: (props: Parameters<typeof eventBuilders.promptSubmitted>[0]) => {\n      const event = eventBuilders.promptSubmitted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    sessionStopped: (props: Parameters<typeof eventBuilders.sessionStopped>[0]) => {\n      const event = eventBuilders.sessionStopped(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    enhancedSessionStopped: (props: Parameters<typeof eventBuilders.enhancedSessionStopped>[0]) => {\n      const event = eventBuilders.enhancedSessionStopped(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    checkpointCreated: (props: Parameters<typeof eventBuilders.checkpointCreated>[0]) => {\n      const event = eventBuilders.checkpointCreated(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    checkpointRestored: (props: Parameters<typeof eventBuilders.checkpointRestored>[0]) => {\n      const event = eventBuilders.checkpointRestored(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    toolExecuted: (props: Parameters<typeof eventBuilders.toolExecuted>[0]) => {\n      const event = eventBuilders.toolExecuted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Enhanced Agent events\n    agentStarted: (props: Parameters<typeof eventBuilders.agentStarted>[0]) => {\n      const event = eventBuilders.agentStarted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    agentProgress: (props: Parameters<typeof eventBuilders.agentProgress>[0]) => {\n      const event = eventBuilders.agentProgress(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    agentError: (props: Parameters<typeof eventBuilders.agentError>[0]) => {\n      const event = eventBuilders.agentError(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // MCP events\n    mcpServerAdded: (props: Parameters<typeof eventBuilders.mcpServerAdded>[0]) => {\n      const event = eventBuilders.mcpServerAdded(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    mcpServerRemoved: (props: Parameters<typeof eventBuilders.mcpServerRemoved>[0]) => {\n      const event = eventBuilders.mcpServerRemoved(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    mcpToolInvoked: (props: Parameters<typeof eventBuilders.mcpToolInvoked>[0]) => {\n      const event = eventBuilders.mcpToolInvoked(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    mcpConnectionError: (props: Parameters<typeof eventBuilders.mcpConnectionError>[0]) => {\n      const event = eventBuilders.mcpConnectionError(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Slash Command events\n    slashCommandSelected: (props: Parameters<typeof eventBuilders.slashCommandSelected>[0]) => {\n      const event = eventBuilders.slashCommandSelected(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    slashCommandExecuted: (props: Parameters<typeof eventBuilders.slashCommandExecuted>[0]) => {\n      const event = eventBuilders.slashCommandExecuted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    slashCommandCreated: (props: Parameters<typeof eventBuilders.slashCommandCreated>[0]) => {\n      const event = eventBuilders.slashCommandCreated(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Error and Performance events\n    apiError: (props: Parameters<typeof eventBuilders.apiError>[0]) => {\n      const event = eventBuilders.apiError(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    uiError: (props: Parameters<typeof eventBuilders.uiError>[0]) => {\n      const event = eventBuilders.uiError(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    performanceBottleneck: (props: Parameters<typeof eventBuilders.performanceBottleneck>[0]) => {\n      const event = eventBuilders.performanceBottleneck(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    memoryWarning: (props: Parameters<typeof eventBuilders.memoryWarning>[0]) => {\n      const event = eventBuilders.memoryWarning(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // User journey events\n    journeyMilestone: (props: Parameters<typeof eventBuilders.journeyMilestone>[0]) => {\n      const event = eventBuilders.journeyMilestone(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Enhanced tracking methods\n    enhancedPromptSubmitted: (props: Parameters<typeof eventBuilders.enhancedPromptSubmitted>[0]) => {\n      const event = eventBuilders.enhancedPromptSubmitted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    enhancedToolExecuted: (props: Parameters<typeof eventBuilders.enhancedToolExecuted>[0]) => {\n      const event = eventBuilders.enhancedToolExecuted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    enhancedError: (props: Parameters<typeof eventBuilders.enhancedError>[0]) => {\n      const event = eventBuilders.enhancedError(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Session engagement\n    sessionEngagement: (props: Parameters<typeof eventBuilders.sessionEngagement>[0]) => {\n      const event = eventBuilders.sessionEngagement(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Feature discovery and adoption\n    featureDiscovered: (props: Parameters<typeof eventBuilders.featureDiscovered>[0]) => {\n      const event = eventBuilders.featureDiscovered(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    featureAdopted: (props: Parameters<typeof eventBuilders.featureAdopted>[0]) => {\n      const event = eventBuilders.featureAdopted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    featureCombination: (props: Parameters<typeof eventBuilders.featureCombination>[0]) => {\n      const event = eventBuilders.featureCombination(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Quality metrics\n    outputRegenerated: (props: Parameters<typeof eventBuilders.outputRegenerated>[0]) => {\n      const event = eventBuilders.outputRegenerated(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    conversationAbandoned: (reason: string, messagesCount: number) => {\n      const event = eventBuilders.conversationAbandoned(reason, messagesCount);\n      analytics.track(event.event, event.properties);\n    },\n    \n    suggestionAccepted: (props: Parameters<typeof eventBuilders.suggestionAccepted>[0]) => {\n      const event = eventBuilders.suggestionAccepted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    suggestionRejected: (props: Parameters<typeof eventBuilders.suggestionRejected>[0]) => {\n      const event = eventBuilders.suggestionRejected(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // AI interactions\n    aiInteraction: (props: Parameters<typeof eventBuilders.aiInteraction>[0]) => {\n      const event = eventBuilders.aiInteraction(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    promptPattern: (props: Parameters<typeof eventBuilders.promptPattern>[0]) => {\n      const event = eventBuilders.promptPattern(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Workflow tracking\n    workflowStarted: (props: Parameters<typeof eventBuilders.workflowStarted>[0]) => {\n      const event = eventBuilders.workflowStarted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    workflowCompleted: (props: Parameters<typeof eventBuilders.workflowCompleted>[0]) => {\n      const event = eventBuilders.workflowCompleted(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    workflowAbandoned: (props: Parameters<typeof eventBuilders.workflowAbandoned>[0]) => {\n      const event = eventBuilders.workflowAbandoned(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Network performance\n    networkPerformance: (props: Parameters<typeof eventBuilders.networkPerformance>[0]) => {\n      const event = eventBuilders.networkPerformance(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    networkFailure: (props: Parameters<typeof eventBuilders.networkFailure>[0]) => {\n      const event = eventBuilders.networkFailure(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    // Resource usage (direct methods)\n    resourceUsageHigh: (props: Parameters<typeof eventBuilders.resourceUsageHigh>[0]) => {\n      const event = eventBuilders.resourceUsageHigh(props);\n      analytics.track(event.event, event.properties);\n    },\n    \n    resourceUsageSampled: (props: Parameters<typeof eventBuilders.resourceUsageSampled>[0]) => {\n      const event = eventBuilders.resourceUsageSampled(props);\n      analytics.track(event.event, event.properties);\n    },\n  };\n}\n\nexport function usePageView(pageName: string, properties?: Record<string, any>) {\n  const hasTracked = useRef(false);\n  \n  useEffect(() => {\n    if (!hasTracked.current && analytics.isEnabled()) {\n      analytics.track('$pageview', {\n        page_name: pageName,\n        ...properties,\n      });\n      hasTracked.current = true;\n    }\n  }, [pageName, properties]);\n}\n\nexport function useAppLifecycle() {\n  useEffect(() => {\n    // Track app start\n    analytics.track(ANALYTICS_EVENTS.APP_STARTED);\n    \n    // Track app close\n    const handleUnload = () => {\n      analytics.track(ANALYTICS_EVENTS.APP_CLOSED);\n      analytics.shutdown();\n    };\n    \n    window.addEventListener('beforeunload', handleUnload);\n    return () => window.removeEventListener('beforeunload', handleUnload);\n  }, []);\n}\n\n// Hook for tracking component-specific metrics\nexport function useComponentMetrics(componentName: string) {\n  const mountTime = useRef(Date.now());\n  const renderCount = useRef(0);\n  \n  useEffect(() => {\n    renderCount.current += 1;\n  });\n  \n  useEffect(() => {\n    return () => {\n      // Track component unmount metrics\n      const lifetime = Date.now() - mountTime.current;\n      analytics.track('component_metrics', {\n        component: componentName,\n        lifetime_ms: lifetime,\n        render_count: renderCount.current,\n      });\n    };\n  }, [componentName]);\n}\n\n// Hook for tracking user interactions\nexport function useInteractionTracking(interactionType: string) {\n  return useCallback((details?: Record<string, any>) => {\n    analytics.track('user_interaction', {\n      interaction_type: interactionType,\n      ...details,\n    });\n  }, [interactionType]);\n}\n\n// Hook for tracking screen changes\nexport function useScreenTracking(tabType?: string, tabId?: string) {\n  useEffect(() => {\n    if (tabType) {\n      const screenName = TAB_SCREEN_NAMES[tabType] || tabType;\n      const screenContext = tabId \n        ? `${screenName}/${tabId.substring(0, 8)}` \n        : screenName;\n      \n      analytics.setScreen(screenContext);\n    }\n  }, [tabType, tabId]);\n}\n\n// Export screen names for external use\nexport { TAB_SCREEN_NAMES };\n\n// Hook for tracking feature experiments\nexport function useFeatureExperiment(featureName: string, variant: string) {\n  // const trackEvent = useTrackEvent();\n  \n  useEffect(() => {\n    analytics.track('experiment_exposure', {\n      experiment_name: featureName,\n      variant,\n      exposure_time: Date.now(),\n    });\n  }, [featureName, variant]);\n  \n  const trackConversion = useCallback((conversionType: string) => {\n    analytics.track('experiment_conversion', {\n      experiment_name: featureName,\n      variant,\n      conversion_type: conversionType,\n    });\n  }, [featureName, variant]);\n  \n  return { trackConversion };\n}\n\n// Hook for tracking user paths/navigation\nexport function usePathTracking(pathname: string) {\n  const previousPath = useRef<string>('');\n  \n  useEffect(() => {\n    if (previousPath.current && previousPath.current !== pathname) {\n      analytics.track('path_transition', {\n        from: previousPath.current,\n        to: pathname,\n        transition_type: 'navigation',\n      });\n    }\n    previousPath.current = pathname;\n  }, [pathname]);\n}\n\n// Hook for tracking feature adoption\nexport function useFeatureAdoptionTracking(featureName: string) {\n  const startTime = useRef<number>(Date.now());\n  const usageCount = useRef<number>(0);\n  const trackEvent = useTrackEvent();\n  \n  const trackUsage = useCallback(() => {\n    usageCount.current += 1;\n    \n    // Track discovery on first use\n    if (usageCount.current === 1) {\n      trackEvent.featureDiscovered({\n        feature_name: featureName,\n        discovery_method: 'organic',\n        time_to_first_use_ms: Date.now() - startTime.current,\n        initial_success: true,\n      });\n    }\n    \n    // Track adoption after 5 uses\n    if (usageCount.current === 5) {\n      const daysSinceFirst = (Date.now() - startTime.current) / (1000 * 60 * 60 * 24);\n      trackEvent.featureAdopted({\n        feature: featureName,\n        adoption_stage: 'adopted',\n        usage_count: usageCount.current,\n        days_since_first_use: daysSinceFirst,\n        usage_trend: 'increasing',\n      });\n    }\n  }, [featureName, trackEvent]);\n  \n  return { trackUsage, usageCount: usageCount.current };\n}\n\n// Hook for tracking workflow completion\nexport function useWorkflowTracking(workflowType: string) {\n  const startTime = useRef<number | null>(null);\n  const stepsCompleted = useRef<number>(0);\n  const toolsUsed = useRef<Set<string>>(new Set());\n  const interruptions = useRef<number>(0);\n  const trackEvent = useTrackEvent();\n  \n  const startWorkflow = useCallback((totalSteps: number) => {\n    startTime.current = Date.now();\n    stepsCompleted.current = 0;\n    toolsUsed.current.clear();\n    interruptions.current = 0;\n    \n    trackEvent.workflowStarted({\n      workflow_type: workflowType,\n      steps_completed: 0,\n      total_steps: totalSteps,\n      duration_ms: 0,\n      interruptions: 0,\n      completion_rate: 0,\n      tools_used: [],\n    });\n  }, [workflowType, trackEvent]);\n  \n  const trackStep = useCallback((toolName?: string) => {\n    stepsCompleted.current += 1;\n    if (toolName) {\n      toolsUsed.current.add(toolName);\n    }\n  }, []);\n  \n  const trackInterruption = useCallback(() => {\n    interruptions.current += 1;\n  }, []);\n  \n  const completeWorkflow = useCallback((totalSteps: number, success: boolean = true) => {\n    if (!startTime.current) return;\n    \n    const duration = Date.now() - startTime.current;\n    const completionRate = stepsCompleted.current / totalSteps;\n    \n    const eventData = {\n      workflow_type: workflowType,\n      steps_completed: stepsCompleted.current,\n      total_steps: totalSteps,\n      duration_ms: duration,\n      interruptions: interruptions.current,\n      completion_rate: completionRate,\n      tools_used: Array.from(toolsUsed.current),\n    };\n    \n    if (success) {\n      trackEvent.workflowCompleted(eventData);\n    } else {\n      trackEvent.workflowAbandoned(eventData);\n    }\n    \n    // Reset\n    startTime.current = null;\n  }, [workflowType, trackEvent]);\n  \n  return {\n    startWorkflow,\n    trackStep,\n    trackInterruption,\n    completeWorkflow,\n  };\n}\n\n// Hook for tracking AI interaction quality\nexport function useAIInteractionTracking(model: string) {\n  const interactionStart = useRef<number | null>(null);\n  const contextSwitches = useRef<number>(0);\n  const clarificationRequests = useRef<number>(0);\n  const trackEvent = useTrackEvent();\n  \n  const startInteraction = useCallback(() => {\n    interactionStart.current = Date.now();\n    contextSwitches.current = 0;\n    clarificationRequests.current = 0;\n  }, []);\n  \n  const trackContextSwitch = useCallback(() => {\n    contextSwitches.current += 1;\n  }, []);\n  \n  const trackClarificationRequest = useCallback(() => {\n    clarificationRequests.current += 1;\n  }, []);\n  \n  const completeInteraction = useCallback((\n    requestTokens: number,\n    responseTokens: number,\n    qualityScore?: number\n  ) => {\n    if (!interactionStart.current) return;\n    \n    trackEvent.aiInteraction({\n      model,\n      request_tokens: requestTokens,\n      response_tokens: responseTokens,\n      response_quality_score: qualityScore,\n      context_switches: contextSwitches.current,\n      clarification_requests: clarificationRequests.current,\n    });\n    \n    // Reset\n    interactionStart.current = null;\n  }, [model, trackEvent]);\n  \n  return {\n    startInteraction,\n    trackContextSwitch,\n    trackClarificationRequest,\n    completeInteraction,\n  };\n}\n\n// Hook for tracking network performance\nexport function useNetworkPerformanceTracking() {\n  const trackEvent = useTrackEvent();\n  \n  const trackRequest = useCallback((\n    _endpoint: string,\n    endpointType: 'mcp' | 'api' | 'webhook',\n    latency: number,\n    payloadSize: number,\n    success: boolean,\n    retryCount: number = 0\n  ) => {\n    const connectionQuality: 'excellent' | 'good' | 'poor' = \n      latency < 100 ? 'excellent' :\n      latency < 500 ? 'good' : 'poor';\n    \n    const eventData = {\n      endpoint_type: endpointType,\n      latency_ms: latency,\n      payload_size_bytes: payloadSize,\n      connection_quality: connectionQuality,\n      retry_count: retryCount,\n      circuit_breaker_triggered: false,\n    };\n    \n    if (success) {\n      trackEvent.networkPerformance(eventData);\n    } else {\n      trackEvent.networkFailure(eventData);\n    }\n  }, [trackEvent]);\n  \n  return { trackRequest };\n}\n"
  },
  {
    "path": "src/hooks/useApiCall.ts",
    "content": "import { useState, useCallback, useRef, useEffect } from 'react';\n\ninterface ApiCallOptions {\n  onSuccess?: (data: any) => void;\n  onError?: (error: Error) => void;\n  showErrorToast?: boolean;\n  showSuccessToast?: boolean;\n  successMessage?: string;\n  errorMessage?: string;\n}\n\ninterface ApiCallState<T> {\n  data: T | null;\n  isLoading: boolean;\n  error: Error | null;\n  call: (...args: any[]) => Promise<T | null>;\n  reset: () => void;\n}\n\n/**\n * Custom hook for making API calls with consistent error handling and loading states\n * Includes automatic toast notifications and cleanup on unmount\n */\nexport function useApiCall<T>(\n  apiFunction: (...args: any[]) => Promise<T>,\n  options: ApiCallOptions = {}\n): ApiCallState<T> {\n  const [data, setData] = useState<T | null>(null);\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n  const abortControllerRef = useRef<AbortController | null>(null);\n  const isMountedRef = useRef(true);\n\n  const {\n    onSuccess,\n    onError,\n    showErrorToast = true,\n    showSuccessToast = false,\n    successMessage = 'Operation completed successfully',\n    errorMessage\n  } = options;\n\n  const call = useCallback(\n    async (...args: any[]): Promise<T | null> => {\n      try {\n        // Cancel any pending request\n        if (abortControllerRef.current) {\n          abortControllerRef.current.abort();\n        }\n\n        // Create new abort controller\n        abortControllerRef.current = new AbortController();\n\n        setIsLoading(true);\n        setError(null);\n\n        const result = await apiFunction(...args);\n\n        // Only update state if component is still mounted\n        if (!isMountedRef.current) return null;\n\n        setData(result);\n        \n        if (showSuccessToast) {\n          // TODO: Implement toast notification\n          console.log('Success:', successMessage);\n        }\n\n        onSuccess?.(result);\n        return result;\n      } catch (err) {\n        // Ignore aborted requests\n        if (err instanceof Error && err.name === 'AbortError') {\n          return null;\n        }\n\n        // Only update state if component is still mounted\n        if (!isMountedRef.current) return null;\n\n        const error = err instanceof Error ? err : new Error('An error occurred');\n        setError(error);\n\n        if (showErrorToast) {\n          // TODO: Implement toast notification\n          console.error('Error:', errorMessage || error.message);\n        }\n\n        onError?.(error);\n        return null;\n      } finally {\n        if (isMountedRef.current) {\n          setIsLoading(false);\n        }\n      }\n    },\n    [apiFunction, onSuccess, onError, showErrorToast, showSuccessToast, successMessage, errorMessage]\n  );\n\n  const reset = useCallback(() => {\n    setData(null);\n    setError(null);\n    setIsLoading(false);\n  }, []);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      isMountedRef.current = false;\n      if (abortControllerRef.current) {\n        abortControllerRef.current.abort();\n      }\n    };\n  }, []);\n\n  return { data, isLoading, error, call, reset };\n}"
  },
  {
    "path": "src/hooks/useDebounce.ts",
    "content": "import { useEffect, useState, useRef } from 'react';\n\n/**\n * Custom hook that debounces a value\n * Useful for search inputs and reducing API calls\n */\nexport function useDebounce<T>(value: T, delay: number): T {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n\n/**\n * Custom hook that returns a debounced callback\n * The callback will only be invoked after the delay has passed since the last call\n */\nexport function useDebouncedCallback<T extends (...args: any[]) => any>(\n  callback: T,\n  delay: number\n): T {\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n  const callbackRef = useRef(callback);\n\n  // Update callback ref on each render to avoid stale closures\n  callbackRef.current = callback;\n\n  return useRef(\n    ((...args: Parameters<T>) => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n      }\n\n      timeoutRef.current = setTimeout(() => {\n        callbackRef.current(...args);\n      }, delay);\n    }) as T\n  ).current;\n}"
  },
  {
    "path": "src/hooks/useLoadingState.ts",
    "content": "import { useState, useCallback } from 'react';\n\ninterface LoadingState<T> {\n  data: T | null;\n  isLoading: boolean;\n  error: Error | null;\n  execute: (...args: any[]) => Promise<T>;\n  reset: () => void;\n}\n\n/**\n * Custom hook for managing loading states with error handling\n * Reduces boilerplate code for async operations\n */\nexport function useLoadingState<T>(\n  asyncFunction: (...args: any[]) => Promise<T>\n): LoadingState<T> {\n  const [data, setData] = useState<T | null>(null);\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n\n  const execute = useCallback(\n    async (...args: any[]): Promise<T> => {\n      try {\n        setIsLoading(true);\n        setError(null);\n        const result = await asyncFunction(...args);\n        setData(result);\n        return result;\n      } catch (err) {\n        const error = err instanceof Error ? err : new Error('An error occurred');\n        setError(error);\n        throw error;\n      } finally {\n        setIsLoading(false);\n      }\n    },\n    [asyncFunction]\n  );\n\n  const reset = useCallback(() => {\n    setData(null);\n    setError(null);\n    setIsLoading(false);\n  }, []);\n\n  return { data, isLoading, error, execute, reset };\n}"
  },
  {
    "path": "src/hooks/usePagination.ts",
    "content": "import { useState, useMemo, useCallback } from 'react';\n\ninterface PaginationOptions {\n  initialPage?: number;\n  initialPageSize?: number;\n  pageSizeOptions?: number[];\n}\n\ninterface PaginationResult<T> {\n  currentPage: number;\n  pageSize: number;\n  totalPages: number;\n  totalItems: number;\n  paginatedData: T[];\n  goToPage: (page: number) => void;\n  nextPage: () => void;\n  previousPage: () => void;\n  setPageSize: (size: number) => void;\n  canGoNext: boolean;\n  canGoPrevious: boolean;\n  pageRange: number[];\n}\n\n/**\n * Custom hook for handling pagination logic\n * Returns paginated data and pagination controls\n */\nexport function usePagination<T>(\n  data: T[],\n  options: PaginationOptions = {}\n): PaginationResult<T> {\n  const {\n    initialPage = 1,\n    initialPageSize = 10,\n    pageSizeOptions: _pageSizeOptions = [10, 25, 50, 100]\n  } = options;\n\n  const [currentPage, setCurrentPage] = useState(initialPage);\n  const [pageSize, setPageSize] = useState(initialPageSize);\n\n  const totalItems = data.length;\n  const totalPages = Math.ceil(totalItems / pageSize);\n\n  // Calculate paginated data\n  const paginatedData = useMemo(() => {\n    const startIndex = (currentPage - 1) * pageSize;\n    const endIndex = startIndex + pageSize;\n    return data.slice(startIndex, endIndex);\n  }, [data, currentPage, pageSize]);\n\n  // Navigation functions\n  const goToPage = useCallback((page: number) => {\n    setCurrentPage(Math.max(1, Math.min(page, totalPages)));\n  }, [totalPages]);\n\n  const nextPage = useCallback(() => {\n    goToPage(currentPage + 1);\n  }, [currentPage, goToPage]);\n\n  const previousPage = useCallback(() => {\n    goToPage(currentPage - 1);\n  }, [currentPage, goToPage]);\n\n  const handleSetPageSize = useCallback((size: number) => {\n    setPageSize(size);\n    // Reset to first page when page size changes\n    setCurrentPage(1);\n  }, []);\n\n  // Generate page range for pagination UI\n  const pageRange = useMemo(() => {\n    const range: number[] = [];\n    const maxVisible = 7; // Maximum number of page buttons to show\n    \n    if (totalPages <= maxVisible) {\n      // Show all pages if total is less than max\n      for (let i = 1; i <= totalPages; i++) {\n        range.push(i);\n      }\n    } else {\n      // Always show first page\n      range.push(1);\n      \n      if (currentPage > 3) {\n        range.push(-1); // Ellipsis\n      }\n      \n      // Show pages around current page\n      const start = Math.max(2, currentPage - 1);\n      const end = Math.min(totalPages - 1, currentPage + 1);\n      \n      for (let i = start; i <= end; i++) {\n        range.push(i);\n      }\n      \n      if (currentPage < totalPages - 2) {\n        range.push(-1); // Ellipsis\n      }\n      \n      // Always show last page\n      if (totalPages > 1) {\n        range.push(totalPages);\n      }\n    }\n    \n    return range;\n  }, [currentPage, totalPages]);\n\n  return {\n    currentPage,\n    pageSize,\n    totalPages,\n    totalItems,\n    paginatedData,\n    goToPage,\n    nextPage,\n    previousPage,\n    setPageSize: handleSetPageSize,\n    canGoNext: currentPage < totalPages,\n    canGoPrevious: currentPage > 1,\n    pageRange\n  };\n}"
  },
  {
    "path": "src/hooks/usePerformanceMonitor.ts",
    "content": "import { useEffect, useRef } from 'react';\nimport { eventBuilders, analytics } from '@/lib/analytics';\n\ninterface PerformanceThresholds {\n  renderTime?: number;  // ms\n  memoryUsage?: number; // MB\n}\n\nconst DEFAULT_THRESHOLDS: PerformanceThresholds = {\n  renderTime: 16, // 60fps threshold\n  memoryUsage: 50, // 50MB\n};\n\n/**\n * Hook to monitor component performance and track bottlenecks\n */\nexport function usePerformanceMonitor(\n  componentName: string,\n  thresholds: PerformanceThresholds = DEFAULT_THRESHOLDS\n) {\n  const renderCount = useRef(0);\n  const lastRenderTime = useRef(performance.now());\n  const mountTime = useRef(performance.now());\n  \n  useEffect(() => {\n    renderCount.current += 1;\n    const currentTime = performance.now();\n    const renderTime = currentTime - lastRenderTime.current;\n    lastRenderTime.current = currentTime;\n    \n    // Skip first render (mount)\n    if (renderCount.current === 1) return;\n    \n    // Check render performance\n    if (thresholds.renderTime && renderTime > thresholds.renderTime) {\n      const event = eventBuilders.performanceBottleneck({\n        operation_type: `render.${componentName}`,\n        duration_ms: renderTime,\n        data_size: renderCount.current,\n        threshold_exceeded: true,\n      });\n      analytics.track(event.event, event.properties);\n    }\n    \n    // Check memory usage if available\n    if ('memory' in performance && (performance as any).memory && thresholds.memoryUsage) {\n      const memoryMB = (performance as any).memory.usedJSHeapSize / (1024 * 1024);\n      if (memoryMB > thresholds.memoryUsage) {\n        const event = eventBuilders.memoryWarning({\n          component: componentName,\n          memory_mb: memoryMB,\n          threshold_exceeded: true,\n          gc_count: undefined,\n        });\n        analytics.track(event.event, event.properties);\n      }\n    }\n  });\n  \n  // Track component unmount metrics\n  useEffect(() => {\n    return () => {\n      const lifetime = performance.now() - mountTime.current;\n      \n      // Only track if component lived for more than 5 seconds and had many renders\n      if (lifetime > 5000 && renderCount.current > 100) {\n        const avgRenderTime = lifetime / renderCount.current;\n        \n        // Track if average render time is high\n        if (avgRenderTime > 10) {\n          const event = eventBuilders.performanceBottleneck({\n            operation_type: `lifecycle.${componentName}`,\n            duration_ms: avgRenderTime,\n            data_size: renderCount.current,\n            threshold_exceeded: true,\n          });\n          analytics.track(event.event, event.properties);\n        }\n      }\n    };\n  }, [componentName]);\n}\n\n/**\n * Hook to track async operation performance\n */\nexport function useAsyncPerformanceTracker(operationName: string) {\n  const operationStart = useRef<number | null>(null);\n  \n  const startTracking = () => {\n    operationStart.current = performance.now();\n  };\n  \n  const endTracking = (success: boolean = true, dataSize?: number) => {\n    if (!operationStart.current) return;\n    \n    const duration = performance.now() - operationStart.current;\n    operationStart.current = null;\n    \n    // Track if operation took too long\n    if (duration > 3000) {\n      const event = eventBuilders.performanceBottleneck({\n        operation_type: `async.${operationName}`,\n        duration_ms: duration,\n        data_size: dataSize,\n        threshold_exceeded: true,\n      });\n      analytics.track(event.event, event.properties);\n    }\n    \n    // Track errors\n    if (!success) {\n      const event = eventBuilders.apiError({\n        endpoint: operationName,\n        error_code: 'async_operation_failed',\n        retry_count: 0,\n        response_time_ms: duration,\n      });\n      analytics.track(event.event, event.properties);\n    }\n  };\n  \n  return { startTracking, endTracking };\n}"
  },
  {
    "path": "src/hooks/useTabState.ts",
    "content": "import { useCallback, useMemo } from 'react';\nimport { useTabContext } from '@/contexts/TabContext';\nimport { Tab } from '@/contexts/TabContext';\n\ninterface UseTabStateReturn {\n  // State\n  tabs: Tab[];\n  activeTab: Tab | undefined;\n  activeTabId: string | null;\n  tabCount: number;\n  chatTabCount: number;\n  agentTabCount: number;\n  \n  // Operations\n  createChatTab: (projectId?: string, title?: string, projectPath?: string) => string;\n  createAgentTab: (agentRunId: string, agentName: string) => string;\n  createAgentExecutionTab: (agent: any, tabId: string, projectPath?: string) => string;\n  createProjectsTab: () => string | null;\n  createAgentsTab: () => string | null;\n  createUsageTab: () => string | null;\n  createMCPTab: () => string | null;\n  createSettingsTab: () => string | null;\n  createClaudeMdTab: () => string | null;\n  createClaudeFileTab: (fileId: string, fileName: string) => string;\n  createCreateAgentTab: () => string;\n  createImportAgentTab: () => string;\n  closeTab: (id: string, force?: boolean) => Promise<boolean>;\n  closeCurrentTab: () => Promise<boolean>;\n  switchToTab: (id: string) => void;\n  switchToNextTab: () => void;\n  switchToPreviousTab: () => void;\n  switchToTabByIndex: (index: number) => void;\n  updateTab: (id: string, updates: Partial<Tab>) => void;\n  updateTabTitle: (id: string, title: string) => void;\n  updateTabStatus: (id: string, status: Tab['status']) => void;\n  markTabAsChanged: (id: string, hasChanges: boolean) => void;\n  findTabBySessionId: (sessionId: string) => Tab | undefined;\n  findTabByAgentRunId: (agentRunId: string) => Tab | undefined;\n  findTabByType: (type: Tab['type']) => Tab | undefined;\n  canAddTab: () => boolean;\n}\n\nexport const useTabState = (): UseTabStateReturn => {\n  const {\n    tabs,\n    activeTabId,\n    addTab,\n    removeTab,\n    updateTab,\n    setActiveTab,\n    getTabById,\n    getTabsByType\n  } = useTabContext();\n\n  const activeTab = useMemo(() => \n    activeTabId ? getTabById(activeTabId) : undefined,\n    [activeTabId, getTabById]\n  );\n\n  const tabCount = tabs.length;\n  const chatTabCount = useMemo(() => getTabsByType('chat').length, [getTabsByType]);\n  const agentTabCount = useMemo(() => getTabsByType('agent').length, [getTabsByType]);\n\n  const createChatTab = useCallback((projectId?: string, title?: string, projectPath?: string): string => {\n    const tabTitle = title || `Chat ${chatTabCount + 1}`;\n    return addTab({\n      type: 'chat',\n      title: tabTitle,\n      sessionId: projectId,\n      initialProjectPath: projectPath,\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'message-square'\n    });\n  }, [addTab, chatTabCount]);\n\n  const createAgentTab = useCallback((agentRunId: string, agentName: string): string => {\n    // Check if tab already exists\n    const existingTab = tabs.find(tab => tab.agentRunId === agentRunId);\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'agent',\n      title: agentName,\n      agentRunId,\n      status: 'running',\n      hasUnsavedChanges: false,\n      icon: 'bot'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createProjectsTab = useCallback((): string | null => {\n    // Allow multiple projects tabs\n    return addTab({\n      type: 'projects',\n      title: 'Projects',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'folder'\n    });\n  }, [addTab]);\n\n  const createAgentsTab = useCallback((): string | null => {\n    // Check if agents tab already exists (singleton)\n    const existingTab = tabs.find(tab => tab.type === 'agents');\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'agents',\n      title: 'Agents',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'bot'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createUsageTab = useCallback((): string | null => {\n    // Check if usage tab already exists (singleton)\n    const existingTab = tabs.find(tab => tab.type === 'usage');\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'usage',\n      title: 'Usage',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'bar-chart'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createMCPTab = useCallback((): string | null => {\n    // Check if MCP tab already exists (singleton)\n    const existingTab = tabs.find(tab => tab.type === 'mcp');\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'mcp',\n      title: 'MCP Servers',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'server'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createSettingsTab = useCallback((): string | null => {\n    // Check if settings tab already exists (singleton)\n    const existingTab = tabs.find(tab => tab.type === 'settings');\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'settings',\n      title: 'Settings',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'settings'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createClaudeMdTab = useCallback((): string | null => {\n    // Check if claude-md tab already exists (singleton)\n    const existingTab = tabs.find(tab => tab.type === 'claude-md');\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'claude-md',\n      title: 'CLAUDE.md',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'file-text'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createClaudeFileTab = useCallback((fileId: string, fileName: string): string => {\n    // Check if tab already exists for this file\n    const existingTab = tabs.find(tab => tab.type === 'claude-file' && tab.claudeFileId === fileId);\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'claude-file',\n      title: fileName,\n      claudeFileId: fileId,\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'file-text'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createAgentExecutionTab = useCallback((agent: any, _tabId: string, projectPath?: string): string => {\n    return addTab({\n      type: 'agent-execution',\n      title: `Run: ${agent.name}`,\n      agentData: agent,\n      projectPath: projectPath,\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'bot'\n    });\n  }, [addTab]);\n\n  const createCreateAgentTab = useCallback((): string => {\n    // Check if create agent tab already exists (singleton)\n    const existingTab = tabs.find(tab => tab.type === 'create-agent');\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'create-agent',\n      title: 'Create Agent',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'plus'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const createImportAgentTab = useCallback((): string => {\n    // Check if import agent tab already exists (singleton)\n    const existingTab = tabs.find(tab => tab.type === 'import-agent');\n    if (existingTab) {\n      setActiveTab(existingTab.id);\n      return existingTab.id;\n    }\n\n    return addTab({\n      type: 'import-agent',\n      title: 'Import Agent',\n      status: 'idle',\n      hasUnsavedChanges: false,\n      icon: 'import'\n    });\n  }, [addTab, tabs, setActiveTab]);\n\n  const closeTab = useCallback(async (id: string, force: boolean = false): Promise<boolean> => {\n    const tab = getTabById(id);\n    if (!tab) return true;\n\n    // Check for unsaved changes\n    if (!force && tab.hasUnsavedChanges) {\n      // In a real implementation, you'd show a confirmation dialog here\n      const confirmed = window.confirm(`Tab \"${tab.title}\" has unsaved changes. Close anyway?`);\n      if (!confirmed) return false;\n    }\n\n    removeTab(id);\n    return true;\n  }, [getTabById, removeTab]);\n\n  const closeCurrentTab = useCallback(async (): Promise<boolean> => {\n    if (!activeTabId) return true;\n    return closeTab(activeTabId);\n  }, [activeTabId, closeTab]);\n\n  const switchToNextTab = useCallback(() => {\n    if (tabs.length === 0) return;\n    \n    const currentIndex = tabs.findIndex(tab => tab.id === activeTabId);\n    const nextIndex = (currentIndex + 1) % tabs.length;\n    setActiveTab(tabs[nextIndex].id);\n  }, [tabs, activeTabId, setActiveTab]);\n\n  const switchToPreviousTab = useCallback(() => {\n    if (tabs.length === 0) return;\n    \n    const currentIndex = tabs.findIndex(tab => tab.id === activeTabId);\n    const previousIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;\n    setActiveTab(tabs[previousIndex].id);\n  }, [tabs, activeTabId, setActiveTab]);\n\n  const switchToTabByIndex = useCallback((index: number) => {\n    if (index >= 0 && index < tabs.length) {\n      setActiveTab(tabs[index].id);\n    }\n  }, [tabs, setActiveTab]);\n\n  const updateTabTitle = useCallback((id: string, title: string) => {\n    updateTab(id, { title });\n  }, [updateTab]);\n\n  const updateTabStatus = useCallback((id: string, status: Tab['status']) => {\n    updateTab(id, { status });\n  }, [updateTab]);\n\n  const markTabAsChanged = useCallback((id: string, hasChanges: boolean) => {\n    updateTab(id, { hasUnsavedChanges: hasChanges });\n  }, [updateTab]);\n\n  const findTabBySessionId = useCallback((sessionId: string): Tab | undefined => {\n    return tabs.find(tab => tab.type === 'chat' && tab.sessionId === sessionId);\n  }, [tabs]);\n\n  const findTabByAgentRunId = useCallback((agentRunId: string): Tab | undefined => {\n    return tabs.find(tab => tab.type === 'agent' && tab.agentRunId === agentRunId);\n  }, [tabs]);\n\n  const findTabByType = useCallback((type: Tab['type']): Tab | undefined => {\n    return tabs.find(tab => tab.type === type);\n  }, [tabs]);\n\n  const canAddTab = useCallback((): boolean => {\n    return tabs.length < 20; // MAX_TABS from context\n  }, [tabs.length]);\n\n  return {\n    // State\n    tabs,\n    activeTab,\n    activeTabId,\n    tabCount,\n    chatTabCount,\n    agentTabCount,\n    \n    // Operations\n    createChatTab,\n    createAgentTab,\n    createAgentExecutionTab,\n    createProjectsTab,\n    createAgentsTab,\n    createUsageTab,\n    createMCPTab,\n    createSettingsTab,\n    createClaudeMdTab,\n    createClaudeFileTab,\n    createCreateAgentTab,\n    createImportAgentTab,\n    closeTab,\n    closeCurrentTab,\n    switchToTab: setActiveTab,\n    switchToNextTab,\n    switchToPreviousTab,\n    switchToTabByIndex,\n    updateTab,\n    updateTabTitle,\n    updateTabStatus,\n    markTabAsChanged,\n    findTabBySessionId,\n    findTabByAgentRunId,\n    findTabByType,\n    canAddTab\n  };\n};"
  },
  {
    "path": "src/hooks/useTheme.ts",
    "content": "import { useThemeContext } from '../contexts/ThemeContext';\n\n/**\n * Hook to access and control the theme system\n * \n * @returns {Object} Theme utilities and state\n * @returns {ThemeMode} theme - Current theme mode ('dark' | 'gray' | 'light' | 'custom')\n * @returns {CustomThemeColors} customColors - Custom theme color configuration\n * @returns {Function} setTheme - Function to change the theme mode\n * @returns {Function} setCustomColors - Function to update custom theme colors\n * @returns {boolean} isLoading - Whether theme operations are in progress\n * \n * @example\n * const { theme, setTheme } = useTheme();\n * \n * // Change theme\n * await setTheme('light');\n * \n * // Update custom colors\n * await setCustomColors({ background: 'oklch(0.98 0.01 240)' });\n */\nexport const useTheme = () => {\n  return useThemeContext();\n};"
  },
  {
    "path": "src/lib/analytics/consent.ts",
    "content": "import type { AnalyticsSettings } from './types';\n\nconst ANALYTICS_STORAGE_KEY = 'opcode-analytics-settings';\n\nexport class ConsentManager {\n  private static instance: ConsentManager;\n  private settings: AnalyticsSettings | null = null;\n  \n  private constructor() {}\n  \n  static getInstance(): ConsentManager {\n    if (!ConsentManager.instance) {\n      ConsentManager.instance = new ConsentManager();\n    }\n    return ConsentManager.instance;\n  }\n  \n  async initialize(): Promise<AnalyticsSettings> {\n    try {\n      // Try to load from localStorage first\n      const stored = localStorage.getItem(ANALYTICS_STORAGE_KEY);\n      if (stored) {\n        this.settings = JSON.parse(stored);\n      } else {\n        // Initialize with default settings\n        this.settings = {\n          enabled: true,\n          hasConsented: true,\n        };\n      }\n      \n      // Generate anonymous user ID if not exists\n      if (this.settings && !this.settings.userId) {\n        this.settings.userId = this.generateAnonymousId();\n        await this.saveSettings();\n      }\n      \n      // Generate session ID\n      if (this.settings) {\n        this.settings.sessionId = this.generateSessionId();\n      }\n      \n      return this.settings || {\n        enabled: true,\n        hasConsented: true,\n      };\n    } catch (error) {\n      console.error('Failed to initialize consent manager:', error);\n      // Return default settings on error\n      return {\n        enabled: true,\n        hasConsented: true,\n      };\n    }\n  }\n  \n  async grantConsent(): Promise<void> {\n    if (!this.settings) {\n      await this.initialize();\n    }\n    \n    this.settings!.enabled = true;\n    this.settings!.hasConsented = true;\n    this.settings!.consentDate = new Date().toISOString();\n    \n    await this.saveSettings();\n  }\n  \n  async revokeConsent(): Promise<void> {\n    if (!this.settings) {\n      await this.initialize();\n    }\n    \n    this.settings!.enabled = false;\n    \n    await this.saveSettings();\n  }\n  \n  async deleteAllData(): Promise<void> {\n    // Clear local storage\n    localStorage.removeItem(ANALYTICS_STORAGE_KEY);\n    \n    // Reset settings with new anonymous ID\n    this.settings = {\n      enabled: true,\n      hasConsented: true,\n      userId: this.generateAnonymousId(),\n      sessionId: this.generateSessionId(),\n    };\n    \n    await this.saveSettings();\n  }\n  \n  getSettings(): AnalyticsSettings | null {\n    return this.settings;\n  }\n  \n  hasConsented(): boolean {\n    return this.settings?.hasConsented || false;\n  }\n  \n  isEnabled(): boolean {\n    return this.settings?.enabled || false;\n  }\n  \n  getUserId(): string {\n    return this.settings?.userId || this.generateAnonymousId();\n  }\n  \n  getSessionId(): string {\n    return this.settings?.sessionId || this.generateSessionId();\n  }\n  \n  private async saveSettings(): Promise<void> {\n    if (!this.settings) return;\n    \n    try {\n      localStorage.setItem(ANALYTICS_STORAGE_KEY, JSON.stringify(this.settings));\n    } catch (error) {\n      console.error('Failed to save analytics settings:', error);\n    }\n  }\n  \n  private generateAnonymousId(): string {\n    // Generate a UUID v4\n    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n      const r = (Math.random() * 16) | 0;\n      const v = c === 'x' ? r : (r & 0x3) | 0x8;\n      return v.toString(16);\n    });\n  }\n  \n  private generateSessionId(): string {\n    // Simple session ID based on timestamp and random value\n    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n  }\n}\n"
  },
  {
    "path": "src/lib/analytics/events.ts",
    "content": "import type { \n  EventName,\n  FeatureUsageProperties,\n  ErrorProperties,\n  SessionProperties,\n  ModelProperties,\n  AgentProperties,\n  MCPProperties,\n  SlashCommandProperties,\n  PerformanceMetrics,\n  PromptSubmittedProperties,\n  SessionStoppedProperties,\n  EnhancedSessionStoppedProperties,\n  CheckpointCreatedProperties,\n  CheckpointRestoredProperties,\n  ToolExecutedProperties,\n  AgentStartedProperties,\n  AgentProgressProperties,\n  AgentErrorProperties,\n  MCPServerAddedProperties,\n  MCPServerRemovedProperties,\n  MCPToolInvokedProperties,\n  MCPConnectionErrorProperties,\n  SlashCommandSelectedProperties,\n  SlashCommandExecutedProperties,\n  SlashCommandCreatedProperties,\n  APIErrorProperties,\n  UIErrorProperties,\n  PerformanceBottleneckProperties,\n  MemoryWarningProperties,\n  UserJourneyProperties,\n  EnhancedPromptSubmittedProperties,\n  EnhancedToolExecutedProperties,\n  EnhancedErrorProperties,\n  SessionEngagementProperties,\n  FeatureDiscoveryProperties,\n  OutputQualityProperties,\n  ResourceUsageProperties,\n  FeatureAdoptionProperties,\n  FeatureCombinationProperties,\n  AIInteractionProperties,\n  PromptPatternProperties,\n  WorkflowProperties,\n  NetworkPerformanceProperties,\n  SuggestionProperties\n} from './types';\n\nexport const ANALYTICS_EVENTS = {\n  // Session events\n  SESSION_CREATED: 'session_created' as EventName,\n  SESSION_COMPLETED: 'session_completed' as EventName,\n  SESSION_RESUMED: 'session_resumed' as EventName,\n  PROMPT_SUBMITTED: 'prompt_submitted' as EventName,\n  SESSION_STOPPED: 'session_stopped' as EventName,\n  CHECKPOINT_CREATED: 'checkpoint_created' as EventName,\n  CHECKPOINT_RESTORED: 'checkpoint_restored' as EventName,\n  TOOL_EXECUTED: 'tool_executed' as EventName,\n  \n  // Feature usage events\n  FEATURE_USED: 'feature_used' as EventName,\n  MODEL_SELECTED: 'model_selected' as EventName,\n  TAB_CREATED: 'tab_created' as EventName,\n  TAB_CLOSED: 'tab_closed' as EventName,\n  FILE_OPENED: 'file_opened' as EventName,\n  FILE_EDITED: 'file_edited' as EventName,\n  FILE_SAVED: 'file_saved' as EventName,\n  \n  // Agent events\n  AGENT_EXECUTED: 'agent_executed' as EventName,\n  AGENT_STARTED: 'agent_started' as EventName,\n  AGENT_PROGRESS: 'agent_progress' as EventName,\n  AGENT_ERROR: 'agent_error' as EventName,\n  \n  // MCP events\n  MCP_SERVER_CONNECTED: 'mcp_server_connected' as EventName,\n  MCP_SERVER_DISCONNECTED: 'mcp_server_disconnected' as EventName,\n  MCP_SERVER_ADDED: 'mcp_server_added' as EventName,\n  MCP_SERVER_REMOVED: 'mcp_server_removed' as EventName,\n  MCP_TOOL_INVOKED: 'mcp_tool_invoked' as EventName,\n  MCP_CONNECTION_ERROR: 'mcp_connection_error' as EventName,\n  \n  // Slash command events\n  SLASH_COMMAND_USED: 'slash_command_used' as EventName,\n  SLASH_COMMAND_SELECTED: 'slash_command_selected' as EventName,\n  SLASH_COMMAND_EXECUTED: 'slash_command_executed' as EventName,\n  SLASH_COMMAND_CREATED: 'slash_command_created' as EventName,\n  \n  // Settings and system events\n  SETTINGS_CHANGED: 'settings_changed' as EventName,\n  APP_STARTED: 'app_started' as EventName,\n  APP_CLOSED: 'app_closed' as EventName,\n  \n  // Error and performance events\n  ERROR_OCCURRED: 'error_occurred' as EventName,\n  API_ERROR: 'api_error' as EventName,\n  UI_ERROR: 'ui_error' as EventName,\n  PERFORMANCE_BOTTLENECK: 'performance_bottleneck' as EventName,\n  MEMORY_WARNING: 'memory_warning' as EventName,\n  \n  // User journey events\n  JOURNEY_MILESTONE: 'journey_milestone' as EventName,\n  USER_RETENTION: 'user_retention' as EventName,\n  \n  // AI interaction events\n  AI_INTERACTION: 'ai_interaction' as EventName,\n  PROMPT_PATTERN: 'prompt_pattern' as EventName,\n  \n  // Quality events\n  OUTPUT_REGENERATED: 'output_regenerated' as EventName,\n  CONVERSATION_ABANDONED: 'conversation_abandoned' as EventName,\n  SUGGESTION_ACCEPTED: 'suggestion_accepted' as EventName,\n  SUGGESTION_REJECTED: 'suggestion_rejected' as EventName,\n  \n  // Workflow events\n  WORKFLOW_STARTED: 'workflow_started' as EventName,\n  WORKFLOW_COMPLETED: 'workflow_completed' as EventName,\n  WORKFLOW_ABANDONED: 'workflow_abandoned' as EventName,\n  \n  // Feature adoption events\n  FEATURE_DISCOVERED: 'feature_discovered' as EventName,\n  FEATURE_ADOPTED: 'feature_adopted' as EventName,\n  FEATURE_COMBINATION: 'feature_combination' as EventName,\n  \n  // Resource usage events\n  RESOURCE_USAGE_HIGH: 'resource_usage_high' as EventName,\n  RESOURCE_USAGE_SAMPLED: 'resource_usage_sampled' as EventName,\n  \n  // Network performance events\n  NETWORK_PERFORMANCE: 'network_performance' as EventName,\n  NETWORK_FAILURE: 'network_failure' as EventName,\n  \n  // Engagement events\n  SESSION_ENGAGEMENT: 'session_engagement' as EventName,\n} as const;\n\n// Event property builders - help ensure consistent event structure\nexport const eventBuilders = {\n  session: (props: SessionProperties) => ({\n    event: ANALYTICS_EVENTS.SESSION_CREATED,\n    properties: {\n      category: 'session',\n      ...props,\n    },\n  }),\n  \n  feature: (feature: string, subfeature?: string, metadata?: Record<string, any>) => ({\n    event: ANALYTICS_EVENTS.FEATURE_USED,\n    properties: {\n      category: 'feature',\n      feature,\n      subfeature,\n      ...metadata,\n    } as FeatureUsageProperties,\n  }),\n  \n  error: (errorType: string, errorCode?: string, context?: string) => ({\n    event: ANALYTICS_EVENTS.ERROR_OCCURRED,\n    properties: {\n      category: 'error',\n      error_type: errorType,\n      error_code: errorCode,\n      context,\n    } as ErrorProperties,\n  }),\n  \n  model: (newModel: string, previousModel?: string, source?: string) => ({\n    event: ANALYTICS_EVENTS.MODEL_SELECTED,\n    properties: {\n      category: 'model',\n      new_model: newModel,\n      previous_model: previousModel,\n      source,\n    } as ModelProperties,\n  }),\n  \n  agent: (agentType: string, success: boolean, agentName?: string, durationMs?: number) => ({\n    event: ANALYTICS_EVENTS.AGENT_EXECUTED,\n    properties: {\n      category: 'agent',\n      agent_type: agentType,\n      agent_name: agentName,\n      success,\n      duration_ms: durationMs,\n    } as AgentProperties,\n  }),\n  \n  mcp: (serverName: string, success: boolean, serverType?: string) => ({\n    event: ANALYTICS_EVENTS.MCP_SERVER_CONNECTED,\n    properties: {\n      category: 'mcp',\n      server_name: serverName,\n      server_type: serverType,\n      success,\n    } as MCPProperties,\n  }),\n  \n  slashCommand: (command: string, success: boolean) => ({\n    event: ANALYTICS_EVENTS.SLASH_COMMAND_USED,\n    properties: {\n      category: 'slash_command',\n      command,\n      success,\n    } as SlashCommandProperties,\n  }),\n  \n  performance: (metrics: PerformanceMetrics) => ({\n    event: ANALYTICS_EVENTS.FEATURE_USED,\n    properties: {\n      category: 'performance',\n      feature: 'system_metrics',\n      ...metrics,\n    },\n  }),\n  \n  // Claude Code Session event builders\n  promptSubmitted: (props: PromptSubmittedProperties) => ({\n    event: ANALYTICS_EVENTS.PROMPT_SUBMITTED,\n    properties: {\n      category: 'session',\n      ...props,\n    },\n  }),\n  \n  sessionStopped: (props: SessionStoppedProperties) => ({\n    event: ANALYTICS_EVENTS.SESSION_STOPPED,\n    properties: {\n      category: 'session',\n      ...props,\n    },\n  }),\n  \n  // Enhanced session stopped with detailed metrics\n  enhancedSessionStopped: (props: EnhancedSessionStoppedProperties) => ({\n    event: ANALYTICS_EVENTS.SESSION_STOPPED,\n    properties: {\n      category: 'session',\n      duration_ms: props.duration_ms,\n      messages_count: props.messages_count,\n      reason: props.reason,\n      // Timing metrics\n      time_to_first_message_ms: props.time_to_first_message_ms,\n      average_response_time_ms: props.average_response_time_ms,\n      idle_time_ms: props.idle_time_ms,\n      // Interaction metrics\n      prompts_sent: props.prompts_sent,\n      tools_executed: props.tools_executed,\n      tools_failed: props.tools_failed,\n      files_created: props.files_created,\n      files_modified: props.files_modified,\n      files_deleted: props.files_deleted,\n      // Content metrics\n      total_tokens_used: props.total_tokens_used,\n      code_blocks_generated: props.code_blocks_generated,\n      errors_encountered: props.errors_encountered,\n      // Session context\n      model: props.model,\n      has_checkpoints: props.has_checkpoints,\n      checkpoint_count: props.checkpoint_count,\n      was_resumed: props.was_resumed,\n      // Agent context\n      agent_type: props.agent_type,\n      agent_name: props.agent_name ? sanitizers.sanitizeAgentName(props.agent_name) : undefined,\n      agent_success: props.agent_success,\n      // Stop context\n      stop_source: props.stop_source,\n      final_state: props.final_state,\n      has_pending_prompts: props.has_pending_prompts,\n      pending_prompts_count: props.pending_prompts_count,\n    },\n  }),\n  \n  checkpointCreated: (props: CheckpointCreatedProperties) => ({\n    event: ANALYTICS_EVENTS.CHECKPOINT_CREATED,\n    properties: {\n      category: 'session',\n      ...props,\n    },\n  }),\n  \n  checkpointRestored: (props: CheckpointRestoredProperties) => ({\n    event: ANALYTICS_EVENTS.CHECKPOINT_RESTORED,\n    properties: {\n      category: 'session',\n      ...props,\n    },\n  }),\n  \n  toolExecuted: (props: ToolExecutedProperties) => ({\n    event: ANALYTICS_EVENTS.TOOL_EXECUTED,\n    properties: {\n      category: 'session',\n      tool_name: sanitizers.sanitizeToolName(props.tool_name),\n      execution_time_ms: props.execution_time_ms,\n      success: props.success,\n      error_message: props.error_message ? sanitizers.sanitizeErrorMessage(props.error_message) : undefined,\n    },\n  }),\n  \n  // Enhanced Agent event builders\n  agentStarted: (props: AgentStartedProperties) => ({\n    event: ANALYTICS_EVENTS.AGENT_STARTED,\n    properties: {\n      category: 'agent',\n      agent_type: props.agent_type,\n      agent_name: props.agent_name ? sanitizers.sanitizeAgentName(props.agent_name) : undefined,\n      has_custom_prompt: props.has_custom_prompt,\n    },\n  }),\n  \n  agentProgress: (props: AgentProgressProperties) => ({\n    event: ANALYTICS_EVENTS.AGENT_PROGRESS,\n    properties: {\n      category: 'agent',\n      ...props,\n    },\n  }),\n  \n  agentError: (props: AgentErrorProperties) => ({\n    event: ANALYTICS_EVENTS.AGENT_ERROR,\n    properties: {\n      category: 'agent',\n      ...props,\n    },\n  }),\n  \n  // MCP event builders\n  mcpServerAdded: (props: MCPServerAddedProperties) => ({\n    event: ANALYTICS_EVENTS.MCP_SERVER_ADDED,\n    properties: {\n      category: 'mcp',\n      ...props,\n    },\n  }),\n  \n  mcpServerRemoved: (props: MCPServerRemovedProperties) => ({\n    event: ANALYTICS_EVENTS.MCP_SERVER_REMOVED,\n    properties: {\n      category: 'mcp',\n      server_name: sanitizers.sanitizeServerName(props.server_name),\n      was_connected: props.was_connected,\n    },\n  }),\n  \n  mcpToolInvoked: (props: MCPToolInvokedProperties) => ({\n    event: ANALYTICS_EVENTS.MCP_TOOL_INVOKED,\n    properties: {\n      category: 'mcp',\n      server_name: sanitizers.sanitizeServerName(props.server_name),\n      tool_name: sanitizers.sanitizeToolName(props.tool_name),\n      invocation_source: props.invocation_source,\n    },\n  }),\n  \n  mcpConnectionError: (props: MCPConnectionErrorProperties) => ({\n    event: ANALYTICS_EVENTS.MCP_CONNECTION_ERROR,\n    properties: {\n      category: 'mcp',\n      server_name: sanitizers.sanitizeServerName(props.server_name),\n      error_type: props.error_type,\n      retry_attempt: props.retry_attempt,\n    },\n  }),\n  \n  // Slash Command event builders\n  slashCommandSelected: (props: SlashCommandSelectedProperties) => ({\n    event: ANALYTICS_EVENTS.SLASH_COMMAND_SELECTED,\n    properties: {\n      category: 'slash_command',\n      command_name: sanitizers.sanitizeCommandName(props.command_name),\n      selection_method: props.selection_method,\n    },\n  }),\n  \n  slashCommandExecuted: (props: SlashCommandExecutedProperties) => ({\n    event: ANALYTICS_EVENTS.SLASH_COMMAND_EXECUTED,\n    properties: {\n      category: 'slash_command',\n      command_name: sanitizers.sanitizeCommandName(props.command_name),\n      parameters_count: props.parameters_count,\n      execution_time_ms: props.execution_time_ms,\n    },\n  }),\n  \n  slashCommandCreated: (props: SlashCommandCreatedProperties) => ({\n    event: ANALYTICS_EVENTS.SLASH_COMMAND_CREATED,\n    properties: {\n      category: 'slash_command',\n      ...props,\n    },\n  }),\n  \n  // Error and Performance event builders\n  apiError: (props: APIErrorProperties) => ({\n    event: ANALYTICS_EVENTS.API_ERROR,\n    properties: {\n      category: 'error',\n      endpoint: sanitizers.sanitizeEndpoint(props.endpoint),\n      error_code: props.error_code,\n      retry_count: props.retry_count,\n      response_time_ms: props.response_time_ms,\n    },\n  }),\n  \n  uiError: (props: UIErrorProperties) => ({\n    event: ANALYTICS_EVENTS.UI_ERROR,\n    properties: {\n      category: 'error',\n      ...props,\n    },\n  }),\n  \n  performanceBottleneck: (props: PerformanceBottleneckProperties) => ({\n    event: ANALYTICS_EVENTS.PERFORMANCE_BOTTLENECK,\n    properties: {\n      category: 'performance',\n      ...props,\n    },\n  }),\n  \n  memoryWarning: (props: MemoryWarningProperties) => ({\n    event: ANALYTICS_EVENTS.MEMORY_WARNING,\n    properties: {\n      category: 'performance',\n      ...props,\n    },\n  }),\n  \n  // User journey event builders\n  journeyMilestone: (props: UserJourneyProperties) => ({\n    event: ANALYTICS_EVENTS.JOURNEY_MILESTONE,\n    properties: {\n      category: 'user_journey',\n      ...props,\n    },\n  }),\n  \n  // Enhanced prompt submission with more context\n  enhancedPromptSubmitted: (props: EnhancedPromptSubmittedProperties) => ({\n    event: ANALYTICS_EVENTS.PROMPT_SUBMITTED,\n    properties: {\n      category: 'session',\n      prompt_length: props.prompt_length,\n      model: props.model,\n      has_attachments: props.has_attachments,\n      source: props.source,\n      word_count: props.word_count,\n      conversation_depth: props.conversation_depth,\n      prompt_complexity: props.prompt_complexity,\n      contains_code: props.contains_code,\n      language_detected: props.language_detected,\n      session_age_ms: props.session_age_ms,\n    },\n  }),\n  \n  // Enhanced tool execution with more context\n  enhancedToolExecuted: (props: EnhancedToolExecutedProperties) => ({\n    event: ANALYTICS_EVENTS.TOOL_EXECUTED,\n    properties: {\n      category: 'session',\n      tool_name: sanitizers.sanitizeToolName(props.tool_name),\n      execution_time_ms: props.execution_time_ms,\n      success: props.success,\n      error_message: props.error_message ? sanitizers.sanitizeErrorMessage(props.error_message) : undefined,\n      tool_category: props.tool_category,\n      consecutive_failures: props.consecutive_failures,\n      retry_attempted: props.retry_attempted,\n      input_size_bytes: props.input_size_bytes,\n      output_size_bytes: props.output_size_bytes,\n    },\n  }),\n  \n  // Enhanced error tracking\n  enhancedError: (props: EnhancedErrorProperties) => ({\n    event: ANALYTICS_EVENTS.ERROR_OCCURRED,\n    properties: {\n      category: 'error',\n      error_type: props.error_type,\n      error_code: props.error_code,\n      error_message: props.error_message ? sanitizers.sanitizeErrorMessage(props.error_message) : undefined,\n      context: props.context,\n      user_action_before_error: props.user_action_before_error,\n      recovery_attempted: props.recovery_attempted,\n      recovery_successful: props.recovery_successful,\n      error_frequency: props.error_frequency,\n      stack_trace_hash: props.stack_trace_hash,\n    },\n  }),\n  \n  // Session engagement\n  sessionEngagement: (props: SessionEngagementProperties) => ({\n    event: ANALYTICS_EVENTS.SESSION_ENGAGEMENT,\n    properties: {\n      category: 'engagement',\n      ...props,\n    },\n  }),\n  \n  // Feature discovery\n  featureDiscovered: (props: FeatureDiscoveryProperties) => ({\n    event: ANALYTICS_EVENTS.FEATURE_DISCOVERED,\n    properties: {\n      category: 'feature_adoption',\n      ...props,\n    },\n  }),\n  \n  // Output quality\n  outputRegenerated: (props: OutputQualityProperties) => ({\n    event: ANALYTICS_EVENTS.OUTPUT_REGENERATED,\n    properties: {\n      category: 'quality',\n      ...props,\n    },\n  }),\n  \n  // Conversation abandoned\n  conversationAbandoned: (reason: string, messagesCount: number) => ({\n    event: ANALYTICS_EVENTS.CONVERSATION_ABANDONED,\n    properties: {\n      category: 'quality',\n      reason,\n      messages_count: messagesCount,\n    },\n  }),\n  \n  // Suggestion tracking\n  suggestionAccepted: (props: SuggestionProperties) => ({\n    event: ANALYTICS_EVENTS.SUGGESTION_ACCEPTED,\n    properties: {\n      category: 'quality',\n      ...props,\n    },\n  }),\n  \n  suggestionRejected: (props: SuggestionProperties) => ({\n    event: ANALYTICS_EVENTS.SUGGESTION_REJECTED,\n    properties: {\n      category: 'quality',\n      ...props,\n    },\n  }),\n  \n  // Resource usage\n  resourceUsageHigh: (props: ResourceUsageProperties) => ({\n    event: ANALYTICS_EVENTS.RESOURCE_USAGE_HIGH,\n    properties: {\n      category: 'performance',\n      ...props,\n    },\n  }),\n  \n  resourceUsageSampled: (props: ResourceUsageProperties) => ({\n    event: ANALYTICS_EVENTS.RESOURCE_USAGE_SAMPLED,\n    properties: {\n      category: 'performance',\n      ...props,\n    },\n  }),\n  \n  // Feature adoption\n  featureAdopted: (props: FeatureAdoptionProperties) => ({\n    event: ANALYTICS_EVENTS.FEATURE_ADOPTED,\n    properties: {\n      category: 'feature_adoption',\n      ...props,\n    },\n  }),\n  \n  featureCombination: (props: FeatureCombinationProperties) => ({\n    event: ANALYTICS_EVENTS.FEATURE_COMBINATION,\n    properties: {\n      category: 'feature_adoption',\n      ...props,\n    },\n  }),\n  \n  // AI interactions\n  aiInteraction: (props: AIInteractionProperties) => ({\n    event: ANALYTICS_EVENTS.AI_INTERACTION,\n    properties: {\n      category: 'ai',\n      ...props,\n    },\n  }),\n  \n  promptPattern: (props: PromptPatternProperties) => ({\n    event: ANALYTICS_EVENTS.PROMPT_PATTERN,\n    properties: {\n      category: 'ai',\n      ...props,\n    },\n  }),\n  \n  // Workflow tracking\n  workflowStarted: (props: WorkflowProperties) => ({\n    event: ANALYTICS_EVENTS.WORKFLOW_STARTED,\n    properties: {\n      category: 'workflow',\n      ...props,\n    },\n  }),\n  \n  workflowCompleted: (props: WorkflowProperties) => ({\n    event: ANALYTICS_EVENTS.WORKFLOW_COMPLETED,\n    properties: {\n      category: 'workflow',\n      ...props,\n    },\n  }),\n  \n  workflowAbandoned: (props: WorkflowProperties) => ({\n    event: ANALYTICS_EVENTS.WORKFLOW_ABANDONED,\n    properties: {\n      category: 'workflow',\n      ...props,\n    },\n  }),\n  \n  // Network performance\n  networkPerformance: (props: NetworkPerformanceProperties) => ({\n    event: ANALYTICS_EVENTS.NETWORK_PERFORMANCE,\n    properties: {\n      category: 'network',\n      endpoint_type: props.endpoint_type,\n      latency_ms: props.latency_ms,\n      payload_size_bytes: props.payload_size_bytes,\n      connection_quality: props.connection_quality,\n      retry_count: props.retry_count,\n      circuit_breaker_triggered: props.circuit_breaker_triggered,\n    },\n  }),\n  \n  networkFailure: (props: NetworkPerformanceProperties) => ({\n    event: ANALYTICS_EVENTS.NETWORK_FAILURE,\n    properties: {\n      category: 'network',\n      endpoint_type: props.endpoint_type,\n      latency_ms: props.latency_ms,\n      payload_size_bytes: props.payload_size_bytes,\n      connection_quality: props.connection_quality,\n      retry_count: props.retry_count,\n      circuit_breaker_triggered: props.circuit_breaker_triggered,\n    },\n  }),\n};\n\n// Sanitization helpers to remove PII\nexport const sanitizers = {\n  // Remove file paths, keeping only extension\n  sanitizeFilePath: (path: string): string => {\n    const ext = path.split('.').pop();\n    return ext ? `*.${ext}` : 'unknown';\n  },\n  \n  // Remove project names and paths\n  sanitizeProjectPath: (_path: string): string => {\n    return 'project';\n  },\n  \n  // Sanitize error messages that might contain sensitive info\n  sanitizeErrorMessage: (message: string): string => {\n    // Remove file paths\n    message = message.replace(/\\/[\\w\\-\\/\\.]+/g, '/***');\n    // Remove potential API keys or tokens\n    message = message.replace(/[a-zA-Z0-9]{20,}/g, '***');\n    // Remove email addresses\n    message = message.replace(/[\\w\\.-]+@[\\w\\.-]+\\.\\w+/g, '***@***.***');\n    return message;\n  },\n  \n  // Sanitize agent names that might contain user info\n  sanitizeAgentName: (name: string): string => {\n    // Only keep the type, remove custom names\n    return name.split('-')[0] || 'custom';\n  },\n  \n  // Sanitize tool names to remove any user-specific info\n  sanitizeToolName: (name: string): string => {\n    // Remove any path-like structures\n    return name.replace(/\\/[\\w\\-\\/\\.]+/g, '').toLowerCase();\n  },\n  \n  // Sanitize server names to remove any user-specific info\n  sanitizeServerName: (name: string): string => {\n    // Keep only the type or first part\n    return name.split(/[\\-_]/)[0] || 'custom';\n  },\n  \n  // Sanitize command names\n  sanitizeCommandName: (name: string): string => {\n    // Remove any custom prefixes or user-specific parts\n    return name.replace(/^custom-/, '').split('-')[0] || 'custom';\n  },\n  \n  // Sanitize API endpoints\n  sanitizeEndpoint: (endpoint: string): string => {\n    // Remove any dynamic IDs or user-specific parts\n    return endpoint.replace(/\\/\\d+/g, '/:id').replace(/\\/[\\w\\-]{20,}/g, '/:id');\n  },\n};\n"
  },
  {
    "path": "src/lib/analytics/index.ts",
    "content": "import posthog from 'posthog-js';\nimport { ConsentManager } from './consent';\nimport { sanitizers } from './events';\nimport type { \n  AnalyticsConfig, \n  AnalyticsEvent, \n  EventName,\n  AnalyticsSettings \n} from './types';\n\nexport * from './types';\nexport * from './events';\nexport { ConsentManager } from './consent';\nexport { ResourceMonitor, resourceMonitor } from './resourceMonitor';\n\nclass AnalyticsService {\n  private static instance: AnalyticsService;\n  private initialized = false;\n  private consentManager: ConsentManager;\n  private config: AnalyticsConfig;\n  private eventQueue: AnalyticsEvent[] = [];\n  private flushInterval: NodeJS.Timeout | null = null;\n  private currentScreen: string = 'app_start';\n  \n  private constructor() {\n    this.consentManager = ConsentManager.getInstance();\n    \n    // Default configuration - pulled from Vite environment variables\n    this.config = {\n      apiKey: 'phc_6seRe1SJkFckJU2qQWeeIy62kaSoaUbCsdVCm1TQZg8',\n      apiHost: 'https://us.i.posthog.com',\n      persistence: 'localStorage',\n      autocapture: false, // We'll manually track events\n      disable_session_recording: true, // Privacy first\n      opt_out_capturing_by_default: false, // Capture enabled by default\n    };\n  }\n  \n  static getInstance(): AnalyticsService {\n    if (!AnalyticsService.instance) {\n      AnalyticsService.instance = new AnalyticsService();\n    }\n    return AnalyticsService.instance;\n  }\n  \n  async initialize(): Promise<void> {\n    if (this.initialized) return;\n    \n    try {\n      // Initialize consent manager\n      const settings = await this.consentManager.initialize();\n      \n      // Only initialize PostHog if user has consented\n      if (settings.hasConsented && settings.enabled) {\n        this.initializePostHog(settings);\n      }\n      \n      // Start event queue flush interval\n      this.startFlushInterval();\n      \n      this.initialized = true;\n    } catch (error) {\n      console.error('Failed to initialize analytics:', error);\n    }\n  }\n  \n  private initializePostHog(settings: AnalyticsSettings): void {\n    try {\n      posthog.init(this.config.apiKey, {\n        api_host: this.config.apiHost,\n        capture_pageview: false, // Disable automatic pageview capture\n        capture_pageleave: false, // Disable automatic pageleave\n        bootstrap: {\n          distinctID: settings.userId,\n        },\n        persistence: this.config.persistence,\n        autocapture: this.config.autocapture,\n        disable_session_recording: this.config.disable_session_recording,\n        opt_out_capturing_by_default: this.config.opt_out_capturing_by_default,\n        loaded: (ph) => {\n          // Set user properties\n          ph.identify(settings.userId, {\n            anonymous: true,\n            consent_date: settings.consentDate,\n            app_type: 'desktop',\n            app_name: 'opcode',\n          });\n          \n          // Set initial screen\n          ph.capture('$screen', {\n            $screen_name: 'app_start',\n          });\n          \n          // Opt in since user has consented\n          ph.opt_in_capturing();\n          \n          if (this.config.loaded) {\n            this.config.loaded(ph);\n          }\n        },\n      });\n    } catch (error) {\n      console.error('Failed to initialize PostHog:', error);\n    }\n  }\n  \n  async enable(): Promise<void> {\n    await this.consentManager.grantConsent();\n    const settings = this.consentManager.getSettings();\n    if (settings) {\n      this.initializePostHog(settings);\n    }\n  }\n  \n  async disable(): Promise<void> {\n    await this.consentManager.revokeConsent();\n    if (typeof posthog !== 'undefined' && posthog.opt_out_capturing) {\n      posthog.opt_out_capturing();\n    }\n  }\n  \n  async deleteAllData(): Promise<void> {\n    await this.consentManager.deleteAllData();\n    if (typeof posthog !== 'undefined' && posthog.reset) {\n      posthog.reset();\n    }\n  }\n  \n  setScreen(screenName: string): void {\n    this.currentScreen = screenName;\n    \n    // Track screen view in PostHog\n    if (typeof posthog !== 'undefined' && typeof posthog.capture === 'function') {\n      posthog.capture('$screen', {\n        $screen_name: screenName,\n      });\n    }\n  }\n  \n  track(eventName: EventName | string, properties?: Record<string, any>): void {\n    // Check if analytics is enabled\n    if (!this.consentManager.isEnabled()) {\n      return;\n    }\n    \n    // Sanitize properties to remove PII\n    const sanitizedProperties = this.sanitizeProperties(properties || {});\n    \n    // Add screen context to all events\n    const enhancedProperties = {\n      ...sanitizedProperties,\n      screen_name: this.currentScreen,\n      app_context: 'opcode_desktop',\n    };\n    \n    // Create event\n    const event: AnalyticsEvent = {\n      event: eventName,\n      properties: enhancedProperties,\n      timestamp: Date.now(),\n      sessionId: this.consentManager.getSessionId(),\n      userId: this.consentManager.getUserId(),\n    };\n    \n    // Add to queue\n    this.eventQueue.push(event);\n    \n    // Send immediately if PostHog is initialized\n    if (typeof posthog !== 'undefined' && typeof posthog.capture === 'function') {\n      this.flushEvents();\n    }\n  }\n  \n  identify(traits?: Record<string, any>): void {\n    if (!this.consentManager.isEnabled()) {\n      return;\n    }\n    \n    const userId = this.consentManager.getUserId();\n    const sanitizedTraits = this.sanitizeProperties(traits || {});\n    \n    if (typeof posthog !== 'undefined' && posthog.identify) {\n      posthog.identify(userId, {\n        ...sanitizedTraits,\n        anonymous: true,\n      });\n    }\n  }\n  \n  private sanitizeProperties(properties: Record<string, any>): Record<string, any> {\n    const sanitized: Record<string, any> = {};\n    \n    for (const [key, value] of Object.entries(properties)) {\n      // Skip null/undefined values\n      if (value == null) continue;\n      \n      // Apply specific sanitizers based on key\n      if (key.includes('path') || key.includes('file')) {\n        sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeFilePath(value) : value;\n      } else if (key.includes('project')) {\n        sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeProjectPath(value) : value;\n      } else if (key.includes('error') || key.includes('message')) {\n        sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeErrorMessage(value) : value;\n      } else if (key.includes('agent_name')) {\n        sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeAgentName(value) : value;\n      } else {\n        // For other properties, ensure no PII\n        if (typeof value === 'string') {\n          // Remove potential file paths\n          let cleanValue = value.replace(/\\/[\\w\\-\\/\\.]+/g, '/***');\n          // Remove potential API keys\n          cleanValue = cleanValue.replace(/[a-zA-Z0-9]{32,}/g, '***');\n          // Remove emails\n          cleanValue = cleanValue.replace(/[\\w\\.-]+@[\\w\\.-]+\\.\\w+/g, '***@***.***');\n          sanitized[key] = cleanValue;\n        } else {\n          sanitized[key] = value;\n        }\n      }\n    }\n    \n    return sanitized;\n  }\n  \n  private flushEvents(): void {\n    if (this.eventQueue.length === 0) return;\n    \n    const events = [...this.eventQueue];\n    this.eventQueue = [];\n    \n    events.forEach(event => {\n      if (typeof posthog !== 'undefined' && posthog.capture) {\n        posthog.capture(event.event, {\n          ...event.properties,\n          $session_id: event.sessionId,\n          timestamp: event.timestamp,\n          $current_url: `opcode://${event.properties?.screen_name || 'unknown'}`,\n        });\n      }\n    });\n  }\n  \n  private startFlushInterval(): void {\n    // Flush events every 5 seconds\n    this.flushInterval = setInterval(() => {\n      if (this.consentManager.isEnabled()) {\n        this.flushEvents();\n      }\n    }, 5000);\n  }\n  \n  shutdown(): void {\n    if (this.flushInterval) {\n      clearInterval(this.flushInterval);\n      this.flushInterval = null;\n    }\n    \n    // Flush any remaining events\n    this.flushEvents();\n  }\n  \n  // Convenience methods\n  isEnabled(): boolean {\n    return this.consentManager.isEnabled();\n  }\n  \n  hasConsented(): boolean {\n    return this.consentManager.hasConsented();\n  }\n  \n  getSettings(): AnalyticsSettings | null {\n    return this.consentManager.getSettings();\n  }\n}\n\n// Export singleton instance\nexport const analytics = AnalyticsService.getInstance();\n\n// Export for direct usage\nexport default analytics;\n\n/**\n * Performance tracking utility for better insights\n */\nexport class PerformanceTracker {\n  private static performanceData: Map<string, number[]> = new Map();\n  \n  /**\n   * Record a performance metric\n   * Automatically tracks percentiles when enough data is collected\n   */\n  static recordMetric(operation: string, duration: number): void {\n    if (!this.performanceData.has(operation)) {\n      this.performanceData.set(operation, []);\n    }\n    \n    const data = this.performanceData.get(operation)!;\n    data.push(duration);\n    \n    // Keep last 100 measurements for memory efficiency\n    if (data.length > 100) {\n      data.shift();\n    }\n    \n    // Track percentiles every 10 measurements\n    if (data.length >= 10 && data.length % 10 === 0) {\n      const sorted = [...data].sort((a, b) => a - b);\n      const p50 = sorted[Math.floor(sorted.length * 0.5)];\n      const p95 = sorted[Math.floor(sorted.length * 0.95)];\n      const p99 = sorted[Math.floor(sorted.length * 0.99)];\n      \n      analytics.track('performance_percentiles', {\n        operation,\n        p50,\n        p95,\n        p99,\n        sample_size: data.length,\n        min: sorted[0],\n        max: sorted[sorted.length - 1],\n        avg: data.reduce((a, b) => a + b, 0) / data.length,\n      });\n    }\n  }\n  \n  /**\n   * Get current statistics for an operation\n   */\n  static getStats(operation: string): { p50: number; p95: number; p99: number; count: number } | null {\n    const data = this.performanceData.get(operation);\n    if (!data || data.length === 0) return null;\n    \n    const sorted = [...data].sort((a, b) => a - b);\n    return {\n      p50: sorted[Math.floor(sorted.length * 0.5)],\n      p95: sorted[Math.floor(sorted.length * 0.95)],\n      p99: sorted[Math.floor(sorted.length * 0.99)],\n      count: data.length,\n    };\n  }\n  \n  /**\n   * Clear data for an operation or all operations\n   */\n  static clear(operation?: string): void {\n    if (operation) {\n      this.performanceData.delete(operation);\n    } else {\n      this.performanceData.clear();\n    }\n  }\n}\n"
  },
  {
    "path": "src/lib/analytics/resourceMonitor.ts",
    "content": "import { analytics, eventBuilders } from '@/lib/analytics';\nimport type { ResourceUsageProperties } from './types';\n\n/**\n * Resource monitoring utility for tracking system resource usage and performance\n * Helps identify performance bottlenecks and resource-intensive operations\n */\nexport class ResourceMonitor {\n  private static instance: ResourceMonitor;\n  private monitoringInterval: NodeJS.Timeout | null = null;\n  private isMonitoring = false;\n  private sampleCount = 0;\n  private highUsageThresholds = {\n    memory: 500, // MB\n    cpu: 80, // percent\n    networkRequests: 50, // per interval\n  };\n  \n  private constructor() {}\n  \n  static getInstance(): ResourceMonitor {\n    if (!ResourceMonitor.instance) {\n      ResourceMonitor.instance = new ResourceMonitor();\n    }\n    return ResourceMonitor.instance;\n  }\n  \n  /**\n   * Start monitoring resource usage with periodic sampling\n   * @param intervalMs - Sampling interval in milliseconds (default: 60000ms = 1 minute)\n   */\n  startMonitoring(intervalMs: number = 60000): void {\n    if (this.isMonitoring) {\n      console.warn('Resource monitoring is already active');\n      return;\n    }\n    \n    this.isMonitoring = true;\n    this.sampleCount = 0;\n    \n    // Initial sample\n    this.collectAndReportMetrics();\n    \n    // Set up periodic sampling\n    this.monitoringInterval = setInterval(() => {\n      this.collectAndReportMetrics();\n    }, intervalMs);\n    \n    console.log(`Resource monitoring started with ${intervalMs}ms interval`);\n  }\n  \n  /**\n   * Stop resource monitoring\n   */\n  stopMonitoring(): void {\n    if (this.monitoringInterval) {\n      clearInterval(this.monitoringInterval);\n      this.monitoringInterval = null;\n    }\n    this.isMonitoring = false;\n    console.log('Resource monitoring stopped');\n  }\n  \n  /**\n   * Collect current resource metrics\n   */\n  private collectResourceMetrics(): ResourceUsageProperties {\n    const metrics: ResourceUsageProperties = {\n      memory_usage_mb: this.getMemoryUsage(),\n      network_requests_count: this.getNetworkRequestsCount(),\n      active_connections: this.getActiveConnections(),\n    };\n    \n    // Add CPU usage if available\n    const cpuUsage = this.getCPUUsage();\n    if (cpuUsage !== null) {\n      metrics.cpu_usage_percent = cpuUsage;\n    }\n    \n    // Add cache hit rate if available\n    const cacheHitRate = this.getCacheHitRate();\n    if (cacheHitRate !== null) {\n      metrics.cache_hit_rate = cacheHitRate;\n    }\n    \n    return metrics;\n  }\n  \n  /**\n   * Collect metrics and report to analytics\n   */\n  private collectAndReportMetrics(): void {\n    try {\n      const metrics = this.collectResourceMetrics();\n      this.sampleCount++;\n      \n      // Always send sampled data every 10th sample for baseline tracking\n      if (this.sampleCount % 10 === 0) {\n        const event = eventBuilders.resourceUsageSampled(metrics);\n        analytics.track(event.event, event.properties);\n      }\n      \n      // Check for high usage conditions\n      const isHighUsage = \n        metrics.memory_usage_mb > this.highUsageThresholds.memory ||\n        (metrics.cpu_usage_percent && metrics.cpu_usage_percent > this.highUsageThresholds.cpu) ||\n        metrics.network_requests_count > this.highUsageThresholds.networkRequests;\n      \n      if (isHighUsage) {\n        const event = eventBuilders.resourceUsageHigh(metrics);\n        analytics.track(event.event, event.properties);\n      }\n    } catch (error) {\n      console.error('Failed to collect resource metrics:', error);\n    }\n  }\n  \n  /**\n   * Get current memory usage in MB\n   */\n  private getMemoryUsage(): number {\n    if ('memory' in performance && (performance as any).memory) {\n      return (performance as any).memory.usedJSHeapSize / (1024 * 1024);\n    }\n    \n    // Fallback: estimate based on performance timing\n    return 0;\n  }\n  \n  /**\n   * Get CPU usage percentage (if available)\n   */\n  private getCPUUsage(): number | null {\n    // This is a placeholder - actual CPU usage would require native APIs\n    // In a Tauri app, you could call a Rust function to get real CPU usage\n    return null;\n  }\n  \n  /**\n   * Get count of active network requests\n   */\n  private getNetworkRequestsCount(): number {\n    // Count active fetch requests if performance observer is available\n    if ('PerformanceObserver' in window) {\n      const entries = performance.getEntriesByType('resource');\n      const recentEntries = entries.filter(entry => \n        entry.startTime > performance.now() - 60000 // Last minute\n      );\n      return recentEntries.length;\n    }\n    return 0;\n  }\n  \n  /**\n   * Get number of active connections (WebSocket, SSE, etc.)\n   */\n  private getActiveConnections(): number {\n    // This would need to be tracked by your connection management code\n    // For now, return a placeholder\n    return 0;\n  }\n  \n  /**\n   * Get cache hit rate if available\n   */\n  private getCacheHitRate(): number | null {\n    // This would need to be calculated based on your caching implementation\n    return null;\n  }\n  \n  /**\n   * Set custom thresholds for high usage detection\n   */\n  setThresholds(thresholds: Partial<typeof ResourceMonitor.prototype.highUsageThresholds>): void {\n    this.highUsageThresholds = {\n      ...this.highUsageThresholds,\n      ...thresholds,\n    };\n  }\n  \n  /**\n   * Get current thresholds\n   */\n  getThresholds(): typeof ResourceMonitor.prototype.highUsageThresholds {\n    return { ...this.highUsageThresholds };\n  }\n  \n  /**\n   * Force a single metric collection and report\n   */\n  collectOnce(): ResourceUsageProperties {\n    const metrics = this.collectResourceMetrics();\n    \n    // Check for high usage\n    const isHighUsage = \n      metrics.memory_usage_mb > this.highUsageThresholds.memory ||\n      (metrics.cpu_usage_percent && metrics.cpu_usage_percent > this.highUsageThresholds.cpu) ||\n      metrics.network_requests_count > this.highUsageThresholds.networkRequests;\n    \n    if (isHighUsage) {\n      const event = eventBuilders.resourceUsageHigh(metrics);\n      analytics.track(event.event, event.properties);\n    }\n    \n    return metrics;\n  }\n}\n\n// Export singleton instance\nexport const resourceMonitor = ResourceMonitor.getInstance();\n"
  },
  {
    "path": "src/lib/analytics/types.ts",
    "content": "export interface AnalyticsEvent {\n  event: string;\n  properties?: {\n    category?: string;\n    action?: string;\n    label?: string;\n    value?: number;\n    [key: string]: any;\n  };\n  timestamp: number;\n  sessionId: string;\n  userId: string; // anonymous UUID\n}\n\nexport interface AnalyticsSettings {\n  enabled: boolean;\n  hasConsented: boolean;\n  consentDate?: string;\n  userId?: string;\n  sessionId?: string;\n}\n\nexport interface AnalyticsConfig {\n  apiKey: string;\n  apiHost?: string;\n  persistence?: 'localStorage' | 'memory';\n  autocapture?: boolean;\n  disable_session_recording?: boolean;\n  opt_out_capturing_by_default?: boolean;\n  loaded?: (posthog: any) => void;\n}\n\nexport type EventName = \n  | 'session_created'\n  | 'session_completed'\n  | 'session_resumed'\n  | 'feature_used'\n  | 'error_occurred'\n  | 'model_selected'\n  | 'tab_created'\n  | 'tab_closed'\n  | 'file_opened'\n  | 'file_edited'\n  | 'file_saved'\n  | 'agent_executed'\n  | 'mcp_server_connected'\n  | 'mcp_server_disconnected'\n  | 'slash_command_used'\n  | 'settings_changed'\n  | 'app_started'\n  | 'app_closed'\n  // New session events\n  | 'prompt_submitted'\n  | 'session_stopped'\n  | 'checkpoint_created'\n  | 'checkpoint_restored'\n  | 'tool_executed'\n  // New agent events\n  | 'agent_started'\n  | 'agent_progress'\n  | 'agent_error'\n  // New MCP events\n  | 'mcp_server_added'\n  | 'mcp_server_removed'\n  | 'mcp_tool_invoked'\n  | 'mcp_connection_error'\n  // New slash command events\n  | 'slash_command_selected'\n  | 'slash_command_executed'\n  | 'slash_command_created'\n  // New error and performance events\n  | 'api_error'\n  | 'ui_error'\n  | 'performance_bottleneck'\n  | 'memory_warning'\n  // User journey events\n  | 'journey_milestone'\n  | 'user_retention'\n  // AI interaction events\n  | 'ai_interaction'\n  | 'prompt_pattern'\n  // Quality events\n  | 'output_regenerated'\n  | 'conversation_abandoned'\n  | 'suggestion_accepted'\n  | 'suggestion_rejected'\n  // Workflow events\n  | 'workflow_started'\n  | 'workflow_completed'\n  | 'workflow_abandoned'\n  // Feature adoption events\n  | 'feature_discovered'\n  | 'feature_adopted'\n  | 'feature_combination'\n  // Resource usage events\n  | 'resource_usage_high'\n  | 'resource_usage_sampled'\n  // Network performance events\n  | 'network_performance'\n  | 'network_failure'\n  // Engagement events\n  | 'session_engagement';\n\nexport interface FeatureUsageProperties {\n  feature: string;\n  subfeature?: string;\n  metadata?: Record<string, any>;\n}\n\nexport interface ErrorProperties {\n  error_type: string;\n  error_code?: string;\n  error_message?: string;\n  context?: string;\n}\n\nexport interface SessionProperties {\n  model?: string;\n  source?: string;\n  resumed?: boolean;\n  checkpoint_id?: string;\n}\n\nexport interface ModelProperties {\n  previous_model?: string;\n  new_model: string;\n  source?: string;\n}\n\nexport interface AgentProperties {\n  agent_type: string;\n  agent_name?: string;\n  success: boolean;\n  duration_ms?: number;\n}\n\nexport interface MCPProperties {\n  server_name: string;\n  server_type?: string;\n  success: boolean;\n}\n\nexport interface SlashCommandProperties {\n  command: string;\n  success: boolean;\n}\n\nexport interface PerformanceMetrics {\n  startup_time_ms?: number;\n  memory_usage_mb?: number;\n  api_response_time_ms?: number;\n  render_time_ms?: number;\n}\n\n// Claude Code Session event properties\nexport interface PromptSubmittedProperties {\n  prompt_length: number;\n  model: string;\n  has_attachments: boolean;\n  source: 'keyboard' | 'button';\n  word_count: number;\n}\n\nexport interface SessionStoppedProperties {\n  duration_ms: number;\n  messages_count: number;\n  reason: 'user_stopped' | 'error' | 'completed';\n}\n\n// Enhanced session stopped properties for detailed analytics\nexport interface EnhancedSessionStoppedProperties extends SessionStoppedProperties {\n  // Timing metrics\n  time_to_first_message_ms?: number;\n  average_response_time_ms?: number;\n  idle_time_ms?: number;\n  \n  // Interaction metrics\n  prompts_sent: number;\n  tools_executed: number;\n  tools_failed: number;\n  files_created: number;\n  files_modified: number;\n  files_deleted: number;\n  \n  // Content metrics\n  total_tokens_used?: number;\n  code_blocks_generated?: number;\n  errors_encountered: number;\n  \n  // Session context\n  model: string;\n  has_checkpoints: boolean;\n  checkpoint_count?: number;\n  was_resumed: boolean;\n  \n  // Agent context (if applicable)\n  agent_type?: string;\n  agent_name?: string;\n  agent_success?: boolean;\n  \n  // Stop context\n  stop_source: 'user_button' | 'keyboard_shortcut' | 'timeout' | 'error' | 'completed';\n  final_state: 'success' | 'partial' | 'failed' | 'cancelled';\n  has_pending_prompts: boolean;\n  pending_prompts_count?: number;\n}\n\nexport interface CheckpointCreatedProperties {\n  checkpoint_number: number;\n  session_duration_at_checkpoint: number;\n}\n\nexport interface CheckpointRestoredProperties {\n  checkpoint_id: string;\n  time_since_checkpoint_ms: number;\n}\n\nexport interface ToolExecutedProperties {\n  tool_name: string;\n  execution_time_ms: number;\n  success: boolean;\n  error_message?: string;\n}\n\n// Enhanced Agent properties\nexport interface AgentStartedProperties {\n  agent_type: string;\n  agent_name?: string;\n  has_custom_prompt: boolean;\n}\n\nexport interface AgentProgressProperties {\n  step_number: number;\n  step_type: string;\n  duration_ms: number;\n  agent_type: string;\n}\n\nexport interface AgentErrorProperties {\n  error_type: string;\n  error_stage: string;\n  retry_count: number;\n  agent_type: string;\n}\n\n// MCP properties\nexport interface MCPServerAddedProperties {\n  server_type: string;\n  configuration_method: 'manual' | 'preset' | 'import';\n}\n\nexport interface MCPServerRemovedProperties {\n  server_name: string;\n  was_connected: boolean;\n}\n\nexport interface MCPToolInvokedProperties {\n  server_name: string;\n  tool_name: string;\n  invocation_source: 'user' | 'agent' | 'suggestion';\n}\n\nexport interface MCPConnectionErrorProperties {\n  server_name: string;\n  error_type: string;\n  retry_attempt: number;\n}\n\n// Slash Command properties\nexport interface SlashCommandSelectedProperties {\n  command_name: string;\n  selection_method: 'click' | 'keyboard' | 'autocomplete';\n}\n\nexport interface SlashCommandExecutedProperties {\n  command_name: string;\n  parameters_count: number;\n  execution_time_ms: number;\n}\n\nexport interface SlashCommandCreatedProperties {\n  command_type: 'custom' | 'imported';\n  has_parameters: boolean;\n}\n\n// Error and Performance properties\nexport interface APIErrorProperties {\n  endpoint: string;\n  error_code: string | number;\n  retry_count: number;\n  response_time_ms: number;\n}\n\nexport interface UIErrorProperties {\n  component_name: string;\n  error_type: string;\n  user_action?: string;\n}\n\nexport interface PerformanceBottleneckProperties {\n  operation_type: string;\n  duration_ms: number;\n  data_size?: number;\n  threshold_exceeded: boolean;\n}\n\nexport interface MemoryWarningProperties {\n  component: string;\n  memory_mb: number;\n  threshold_exceeded: boolean;\n  gc_count?: number;\n}\n\n// User Journey properties\nexport interface UserJourneyProperties {\n  journey_stage: 'onboarding' | 'first_chat' | 'first_agent' | 'power_user';\n  milestone_reached?: string;\n  time_to_milestone_ms?: number;\n}\n\n// Enhanced prompt properties\nexport interface EnhancedPromptSubmittedProperties extends PromptSubmittedProperties {\n  conversation_depth: number;\n  prompt_complexity: 'simple' | 'moderate' | 'complex';\n  contains_code: boolean;\n  language_detected?: string;\n  session_age_ms: number;\n}\n\n// Enhanced tool properties\nexport interface EnhancedToolExecutedProperties extends ToolExecutedProperties {\n  tool_category: 'file' | 'search' | 'system' | 'custom';\n  consecutive_failures?: number;\n  retry_attempted: boolean;\n  input_size_bytes?: number;\n  output_size_bytes?: number;\n}\n\n// Enhanced error properties\nexport interface EnhancedErrorProperties extends ErrorProperties {\n  user_action_before_error?: string;\n  recovery_attempted: boolean;\n  recovery_successful?: boolean;\n  error_frequency: number;\n  stack_trace_hash?: string;\n}\n\n// Session engagement properties\nexport interface SessionEngagementProperties {\n  session_duration_ms: number;\n  messages_sent: number;\n  tools_used: string[];\n  files_modified: number;\n  engagement_score: number;\n}\n\n// Feature discovery properties\nexport interface FeatureDiscoveryProperties {\n  feature_name: string;\n  discovery_method: 'organic' | 'prompted' | 'documentation';\n  time_to_first_use_ms: number;\n  initial_success: boolean;\n}\n\n// Output quality properties\nexport interface OutputQualityProperties {\n  regeneration_count: number;\n  modification_requested: boolean;\n  final_acceptance: boolean;\n  time_to_acceptance_ms: number;\n}\n\n// Resource usage properties\nexport interface ResourceUsageProperties {\n  cpu_usage_percent?: number;\n  memory_usage_mb: number;\n  disk_io_mb?: number;\n  network_requests_count: number;\n  cache_hit_rate?: number;\n  active_connections: number;\n}\n\n// Feature adoption properties\nexport interface FeatureAdoptionProperties {\n  feature: string;\n  adoption_stage: 'discovered' | 'tried' | 'adopted' | 'abandoned';\n  usage_count: number;\n  days_since_first_use: number;\n  usage_trend: 'increasing' | 'stable' | 'decreasing';\n}\n\n// Feature combination properties\nexport interface FeatureCombinationProperties {\n  primary_feature: string;\n  secondary_feature: string;\n  combination_frequency: number;\n  workflow_efficiency_gain?: number;\n}\n\n// AI interaction properties\nexport interface AIInteractionProperties {\n  model: string;\n  request_tokens: number;\n  response_tokens: number;\n  response_quality_score?: number;\n  context_switches: number;\n  clarification_requests: number;\n}\n\n// Prompt pattern properties\nexport interface PromptPatternProperties {\n  prompt_category: string;\n  prompt_effectiveness: 'high' | 'medium' | 'low';\n  required_iterations: number;\n  final_satisfaction: boolean;\n}\n\n// Workflow properties\nexport interface WorkflowProperties {\n  workflow_type: string;\n  steps_completed: number;\n  total_steps: number;\n  duration_ms: number;\n  interruptions: number;\n  completion_rate: number;\n  tools_used: string[];\n}\n\n// Network performance properties\nexport interface NetworkPerformanceProperties {\n  endpoint_type: 'mcp' | 'api' | 'webhook';\n  latency_ms: number;\n  payload_size_bytes: number;\n  connection_quality: 'excellent' | 'good' | 'poor';\n  retry_count: number;\n  circuit_breaker_triggered: boolean;\n}\n\n// Suggestion properties\nexport interface SuggestionProperties {\n  suggestion_type: string;\n  suggestion_source: string;\n  accepted: boolean;\n  response_time_ms: number;\n}\n"
  },
  {
    "path": "src/lib/api-tracker.ts",
    "content": "import { api as originalApi } from './api';\nimport { analytics, eventBuilders } from './analytics';\n\n// Performance thresholds (in milliseconds)\nconst PERFORMANCE_THRESHOLDS = {\n  fast: 100,\n  normal: 500,\n  slow: 2000,\n  bottleneck: 5000,\n};\n\n// Memory threshold (in MB)\nconst MEMORY_WARNING_THRESHOLD = 100;\n\n/**\n * Wraps an API method with error and performance tracking\n */\nfunction wrapApiMethod<T extends (...args: any[]) => Promise<any>>(\n  methodName: string,\n  method: T\n): T {\n  return (async (...args: any[]) => {\n    const startTime = performance.now();\n    const startMemory = ('memory' in performance ? (performance as any).memory?.usedJSHeapSize : 0) || 0;\n    let retryCount = 0;\n    \n    const trackPerformance = (success: boolean, error?: any) => {\n      const duration = performance.now() - startTime;\n      const memoryUsed = ((('memory' in performance ? (performance as any).memory?.usedJSHeapSize : 0) || 0) - startMemory) / (1024 * 1024); // Convert to MB\n      \n      // Track API errors\n      if (!success && error) {\n        const event = eventBuilders.apiError({\n          endpoint: methodName,\n          error_code: error.code || error.status || 'unknown',\n          retry_count: retryCount,\n          response_time_ms: duration,\n        });\n        analytics.track(event.event, event.properties);\n      }\n      \n      // Track performance bottlenecks\n      if (duration > PERFORMANCE_THRESHOLDS.bottleneck) {\n        const event = eventBuilders.performanceBottleneck({\n          operation_type: `api.${methodName}`,\n          duration_ms: duration,\n          data_size: undefined, // Could be enhanced to track payload size\n          threshold_exceeded: true,\n        });\n        analytics.track(event.event, event.properties);\n      }\n      \n      // Track network performance\n      const connectionQuality = \n        duration < PERFORMANCE_THRESHOLDS.fast ? 'excellent' :\n        duration < PERFORMANCE_THRESHOLDS.normal ? 'good' : 'poor';\n      \n      if (success) {\n        const networkEvent = eventBuilders.networkPerformance({\n          endpoint_type: 'api',\n          latency_ms: duration,\n          payload_size_bytes: 0, // Could be enhanced with actual payload size\n          connection_quality: connectionQuality,\n          retry_count: retryCount,\n          circuit_breaker_triggered: false,\n        });\n        analytics.track(networkEvent.event, networkEvent.properties);\n      }\n      \n      // Track memory warnings\n      if (memoryUsed > MEMORY_WARNING_THRESHOLD) {\n        const event = eventBuilders.memoryWarning({\n          component: `api.${methodName}`,\n          memory_mb: memoryUsed,\n          threshold_exceeded: true,\n          gc_count: undefined, // Could be enhanced with GC tracking\n        });\n        analytics.track(event.event, event.properties);\n      }\n    };\n    \n    try {\n      const result = await method(...args);\n      trackPerformance(true);\n      return result;\n    } catch (error) {\n      trackPerformance(false, error);\n      throw error;\n    }\n  }) as T;\n}\n\n/**\n * Creates a tracked version of the API object\n */\nfunction createTrackedApi() {\n  const trackedApi: any = {};\n  \n  // Wrap each method in the original API\n  for (const [key, value] of Object.entries(originalApi)) {\n    if (typeof value === 'function') {\n      trackedApi[key] = wrapApiMethod(key, value);\n    } else {\n      trackedApi[key] = value;\n    }\n  }\n  \n  return trackedApi as typeof originalApi;\n}\n\n// Export the tracked API\nexport const api = createTrackedApi();\n\n// Re-export types from the original API module\nexport * from './api';\n"
  },
  {
    "path": "src/lib/api.ts",
    "content": "import { apiCall } from './apiAdapter';\nimport type { HooksConfiguration } from '@/types/hooks';\n\n/** Process type for tracking in ProcessRegistry */\nexport type ProcessType = \n  | { AgentRun: { agent_id: number; agent_name: string } }\n  | { ClaudeSession: { session_id: string } };\n\n/** Information about a running process */\nexport interface ProcessInfo {\n  run_id: number;\n  process_type: ProcessType;\n  pid: number;\n  started_at: string;\n  project_path: string;\n  task: string;\n  model: string;\n}\n\n/**\n * Represents a project in the ~/.claude/projects directory\n */\nexport interface Project {\n  /** The project ID (derived from the directory name) */\n  id: string;\n  /** The original project path (decoded from the directory name) */\n  path: string;\n  /** List of session IDs (JSONL file names without extension) */\n  sessions: string[];\n  /** Unix timestamp when the project directory was created */\n  created_at: number;\n  /** Unix timestamp of the most recent session (if any) */\n  most_recent_session?: number;\n}\n\n/**\n * Represents a session with its metadata\n */\nexport interface Session {\n  /** The session ID (UUID) */\n  id: string;\n  /** The project ID this session belongs to */\n  project_id: string;\n  /** The project path */\n  project_path: string;\n  /** Optional todo data associated with this session */\n  todo_data?: any;\n  /** Unix timestamp when the session file was created */\n  created_at: number;\n  /** First user message content (if available) */\n  first_message?: string;\n  /** Timestamp of the first user message (if available) */\n  message_timestamp?: string;\n}\n\n/**\n * Represents the settings from ~/.claude/settings.json\n */\nexport interface ClaudeSettings {\n  [key: string]: any;\n}\n\n/**\n * Represents the Claude Code version status\n */\nexport interface ClaudeVersionStatus {\n  /** Whether Claude Code is installed and working */\n  is_installed: boolean;\n  /** The version string if available */\n  version?: string;\n  /** The full output from the command */\n  output: string;\n}\n\n/**\n * Represents a CLAUDE.md file found in the project\n */\nexport interface ClaudeMdFile {\n  /** Relative path from the project root */\n  relative_path: string;\n  /** Absolute path to the file */\n  absolute_path: string;\n  /** File size in bytes */\n  size: number;\n  /** Last modified timestamp */\n  modified: number;\n}\n\n/**\n * Represents a file or directory entry\n */\nexport interface FileEntry {\n  name: string;\n  path: string;\n  is_directory: boolean;\n  size: number;\n  extension?: string;\n}\n\n/**\n * Represents a Claude installation found on the system\n */\nexport interface ClaudeInstallation {\n  /** Full path to the Claude binary */\n  path: string;\n  /** Version string if available */\n  version?: string;\n  /** Source of discovery (e.g., \"nvm\", \"system\", \"homebrew\", \"which\") */\n  source: string;\n  /** Type of installation */\n  installation_type: \"System\" | \"Custom\";\n}\n\n// Agent API types\nexport interface Agent {\n  id?: number;\n  name: string;\n  icon: string;\n  system_prompt: string;\n  default_task?: string;\n  model: string;\n  hooks?: string; // JSON string of HooksConfiguration\n  created_at: string;\n  updated_at: string;\n}\n\nexport interface AgentExport {\n  version: number;\n  exported_at: string;\n  agent: {\n    name: string;\n    icon: string;\n    system_prompt: string;\n    default_task?: string;\n    model: string;\n    hooks?: string;\n  };\n}\n\nexport interface GitHubAgentFile {\n  name: string;\n  path: string;\n  download_url: string;\n  size: number;\n  sha: string;\n}\n\nexport interface AgentRun {\n  id?: number;\n  agent_id: number;\n  agent_name: string;\n  agent_icon: string;\n  task: string;\n  model: string;\n  project_path: string;\n  session_id: string;\n  status: string; // 'pending', 'running', 'completed', 'failed', 'cancelled'\n  pid?: number;\n  process_started_at?: string;\n  created_at: string;\n  completed_at?: string;\n}\n\nexport interface AgentRunMetrics {\n  duration_ms?: number;\n  total_tokens?: number;\n  cost_usd?: number;\n  message_count?: number;\n}\n\nexport interface AgentRunWithMetrics {\n  id?: number;\n  agent_id: number;\n  agent_name: string;\n  agent_icon: string;\n  task: string;\n  model: string;\n  project_path: string;\n  session_id: string;\n  status: string; // 'pending', 'running', 'completed', 'failed', 'cancelled'\n  pid?: number;\n  duration_ms?: number;\n  total_tokens?: number;\n  process_started_at?: string;\n  created_at: string;\n  completed_at?: string;\n  metrics?: AgentRunMetrics;\n  output?: string; // Real-time JSONL content\n}\n\n// Usage Dashboard types\nexport interface UsageEntry {\n  project: string;\n  timestamp: string;\n  model: string;\n  input_tokens: number;\n  output_tokens: number;\n  cache_write_tokens: number;\n  cache_read_tokens: number;\n  cost: number;\n}\n\nexport interface ModelUsage {\n  model: string;\n  total_cost: number;\n  total_tokens: number;\n  input_tokens: number;\n  output_tokens: number;\n  cache_creation_tokens: number;\n  cache_read_tokens: number;\n  session_count: number;\n}\n\nexport interface DailyUsage {\n  date: string;\n  total_cost: number;\n  total_tokens: number;\n  models_used: string[];\n}\n\nexport interface ProjectUsage {\n  project_path: string;\n  project_name: string;\n  total_cost: number;\n  total_tokens: number;\n  session_count: number;\n  last_used: string;\n}\n\nexport interface UsageStats {\n  total_cost: number;\n  total_tokens: number;\n  total_input_tokens: number;\n  total_output_tokens: number;\n  total_cache_creation_tokens: number;\n  total_cache_read_tokens: number;\n  total_sessions: number;\n  by_model: ModelUsage[];\n  by_date: DailyUsage[];\n  by_project: ProjectUsage[];\n}\n\n/**\n * Represents a checkpoint in the session timeline\n */\nexport interface Checkpoint {\n  id: string;\n  sessionId: string;\n  projectId: string;\n  messageIndex: number;\n  timestamp: string;\n  description?: string;\n  parentCheckpointId?: string;\n  metadata: CheckpointMetadata;\n}\n\n/**\n * Metadata associated with a checkpoint\n */\nexport interface CheckpointMetadata {\n  totalTokens: number;\n  modelUsed: string;\n  userPrompt: string;\n  fileChanges: number;\n  snapshotSize: number;\n}\n\n/**\n * Represents a file snapshot at a checkpoint\n */\nexport interface FileSnapshot {\n  checkpointId: string;\n  filePath: string;\n  content: string;\n  hash: string;\n  isDeleted: boolean;\n  permissions?: number;\n  size: number;\n}\n\n/**\n * Represents a node in the timeline tree\n */\nexport interface TimelineNode {\n  checkpoint: Checkpoint;\n  children: TimelineNode[];\n  fileSnapshotIds: string[];\n}\n\n/**\n * The complete timeline for a session\n */\nexport interface SessionTimeline {\n  sessionId: string;\n  rootNode?: TimelineNode;\n  currentCheckpointId?: string;\n  autoCheckpointEnabled: boolean;\n  checkpointStrategy: CheckpointStrategy;\n  totalCheckpoints: number;\n}\n\n/**\n * Strategy for automatic checkpoint creation\n */\nexport type CheckpointStrategy = 'manual' | 'per_prompt' | 'per_tool_use' | 'smart';\n\n/**\n * Result of a checkpoint operation\n */\nexport interface CheckpointResult {\n  checkpoint: Checkpoint;\n  filesProcessed: number;\n  warnings: string[];\n}\n\n/**\n * Diff between two checkpoints\n */\nexport interface CheckpointDiff {\n  fromCheckpointId: string;\n  toCheckpointId: string;\n  modifiedFiles: FileDiff[];\n  addedFiles: string[];\n  deletedFiles: string[];\n  tokenDelta: number;\n}\n\n/**\n * Diff for a single file\n */\nexport interface FileDiff {\n  path: string;\n  additions: number;\n  deletions: number;\n  diffContent?: string;\n}\n\n/**\n * Represents an MCP server configuration\n */\nexport interface MCPServer {\n  /** Server name/identifier */\n  name: string;\n  /** Transport type: \"stdio\" or \"sse\" */\n  transport: string;\n  /** Command to execute (for stdio) */\n  command?: string;\n  /** Command arguments (for stdio) */\n  args: string[];\n  /** Environment variables */\n  env: Record<string, string>;\n  /** URL endpoint (for SSE) */\n  url?: string;\n  /** Configuration scope: \"local\", \"project\", or \"user\" */\n  scope: string;\n  /** Whether the server is currently active */\n  is_active: boolean;\n  /** Server status */\n  status: ServerStatus;\n}\n\n/**\n * Server status information\n */\nexport interface ServerStatus {\n  /** Whether the server is running */\n  running: boolean;\n  /** Last error message if any */\n  error?: string;\n  /** Last checked timestamp */\n  last_checked?: number;\n}\n\n/**\n * MCP configuration for project scope (.mcp.json)\n */\nexport interface MCPProjectConfig {\n  mcpServers: Record<string, MCPServerConfig>;\n}\n\n/**\n * Individual server configuration in .mcp.json\n */\nexport interface MCPServerConfig {\n  command: string;\n  args: string[];\n  env: Record<string, string>;\n}\n\n/**\n * Represents a custom slash command\n */\nexport interface SlashCommand {\n  /** Unique identifier for the command */\n  id: string;\n  /** Command name (without prefix) */\n  name: string;\n  /** Full command with prefix (e.g., \"/project:optimize\") */\n  full_command: string;\n  /** Command scope: \"project\" or \"user\" */\n  scope: string;\n  /** Optional namespace (e.g., \"frontend\" in \"/project:frontend:component\") */\n  namespace?: string;\n  /** Path to the markdown file */\n  file_path: string;\n  /** Command content (markdown body) */\n  content: string;\n  /** Optional description from frontmatter */\n  description?: string;\n  /** Allowed tools from frontmatter */\n  allowed_tools: string[];\n  /** Whether the command has bash commands (!) */\n  has_bash_commands: boolean;\n  /** Whether the command has file references (@) */\n  has_file_references: boolean;\n  /** Whether the command uses $ARGUMENTS placeholder */\n  accepts_arguments: boolean;\n}\n\n/**\n * Result of adding a server\n */\nexport interface AddServerResult {\n  success: boolean;\n  message: string;\n  server_name?: string;\n}\n\n/**\n * Import result for multiple servers\n */\nexport interface ImportResult {\n  imported_count: number;\n  failed_count: number;\n  servers: ImportServerResult[];\n}\n\n/**\n * Result for individual server import\n */\nexport interface ImportServerResult {\n  name: string;\n  success: boolean;\n  error?: string;\n}\n\n/**\n * API client for interacting with the Rust backend\n */\nexport const api = {\n  /**\n   * Gets the user's home directory path\n   * @returns Promise resolving to the home directory path\n   */\n  async getHomeDirectory(): Promise<string> {\n    try {\n      return await apiCall<string>(\"get_home_directory\");\n    } catch (error) {\n      console.error(\"Failed to get home directory:\", error);\n      return \"/\";\n    }\n  },\n\n  /**\n   * Lists all projects in the ~/.claude/projects directory\n   * @returns Promise resolving to an array of projects\n   */\n  async listProjects(): Promise<Project[]> {\n    try {\n      return await apiCall<Project[]>(\"list_projects\");\n    } catch (error) {\n      console.error(\"Failed to list projects:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Creates a new project for the given directory path\n   * @param path - The directory path to create a project for\n   * @returns Promise resolving to the created project\n   */\n  async createProject(path: string): Promise<Project> {\n    try {\n      return await apiCall<Project>('create_project', { path });\n    } catch (error) {\n      console.error(\"Failed to create project:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Retrieves sessions for a specific project\n   * @param projectId - The ID of the project to retrieve sessions for\n   * @returns Promise resolving to an array of sessions\n   */\n  async getProjectSessions(projectId: string): Promise<Session[]> {\n    try {\n      return await apiCall<Session[]>('get_project_sessions', { projectId });\n    } catch (error) {\n      console.error(\"Failed to get project sessions:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Fetch list of agents from GitHub repository\n   * @returns Promise resolving to list of available agents on GitHub\n   */\n  async fetchGitHubAgents(): Promise<GitHubAgentFile[]> {\n    try {\n      return await apiCall<GitHubAgentFile[]>('fetch_github_agents');\n    } catch (error) {\n      console.error(\"Failed to fetch GitHub agents:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Fetch and preview a specific agent from GitHub\n   * @param downloadUrl - The download URL for the agent file\n   * @returns Promise resolving to the agent export data\n   */\n  async fetchGitHubAgentContent(downloadUrl: string): Promise<AgentExport> {\n    try {\n      return await apiCall<AgentExport>('fetch_github_agent_content', { downloadUrl });\n    } catch (error) {\n      console.error(\"Failed to fetch GitHub agent content:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Import an agent directly from GitHub\n   * @param downloadUrl - The download URL for the agent file\n   * @returns Promise resolving to the imported agent\n   */\n  async importAgentFromGitHub(downloadUrl: string): Promise<Agent> {\n    try {\n      return await apiCall<Agent>('import_agent_from_github', { downloadUrl });\n    } catch (error) {\n      console.error(\"Failed to import agent from GitHub:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Reads the Claude settings file\n   * @returns Promise resolving to the settings object\n   */\n  async getClaudeSettings(): Promise<ClaudeSettings> {\n    try {\n      const result = await apiCall<{ data: ClaudeSettings }>(\"get_claude_settings\");\n      console.log(\"Raw result from get_claude_settings:\", result);\n      \n      // The Rust backend returns ClaudeSettings { data: ... }\n      // We need to extract the data field\n      if (result && typeof result === 'object' && 'data' in result) {\n        return result.data;\n      }\n      \n      // If the result is already the settings object, return it\n      return result as ClaudeSettings;\n    } catch (error) {\n      console.error(\"Failed to get Claude settings:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Opens a new Claude Code session\n   * @param path - Optional path to open the session in\n   * @returns Promise resolving when the session is opened\n   */\n  async openNewSession(path?: string): Promise<string> {\n    try {\n      return await apiCall<string>(\"open_new_session\", { path });\n    } catch (error) {\n      console.error(\"Failed to open new session:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Reads the CLAUDE.md system prompt file\n   * @returns Promise resolving to the system prompt content\n   */\n  async getSystemPrompt(): Promise<string> {\n    try {\n      return await apiCall<string>(\"get_system_prompt\");\n    } catch (error) {\n      console.error(\"Failed to get system prompt:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Checks if Claude Code is installed and gets its version\n   * @returns Promise resolving to the version status\n   */\n  async checkClaudeVersion(): Promise<ClaudeVersionStatus> {\n    try {\n      return await apiCall<ClaudeVersionStatus>(\"check_claude_version\");\n    } catch (error) {\n      console.error(\"Failed to check Claude version:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Saves the CLAUDE.md system prompt file\n   * @param content - The new content for the system prompt\n   * @returns Promise resolving when the file is saved\n   */\n  async saveSystemPrompt(content: string): Promise<string> {\n    try {\n      return await apiCall<string>(\"save_system_prompt\", { content });\n    } catch (error) {\n      console.error(\"Failed to save system prompt:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Saves the Claude settings file\n   * @param settings - The settings object to save\n   * @returns Promise resolving when the settings are saved\n   */\n  async saveClaudeSettings(settings: ClaudeSettings): Promise<string> {\n    try {\n      return await apiCall<string>(\"save_claude_settings\", { settings });\n    } catch (error) {\n      console.error(\"Failed to save Claude settings:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Finds all CLAUDE.md files in a project directory\n   * @param projectPath - The absolute path to the project\n   * @returns Promise resolving to an array of CLAUDE.md files\n   */\n  async findClaudeMdFiles(projectPath: string): Promise<ClaudeMdFile[]> {\n    try {\n      return await apiCall<ClaudeMdFile[]>(\"find_claude_md_files\", { projectPath });\n    } catch (error) {\n      console.error(\"Failed to find CLAUDE.md files:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Reads a specific CLAUDE.md file\n   * @param filePath - The absolute path to the file\n   * @returns Promise resolving to the file content\n   */\n  async readClaudeMdFile(filePath: string): Promise<string> {\n    try {\n      return await apiCall<string>(\"read_claude_md_file\", { filePath });\n    } catch (error) {\n      console.error(\"Failed to read CLAUDE.md file:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Saves a specific CLAUDE.md file\n   * @param filePath - The absolute path to the file\n   * @param content - The new content for the file\n   * @returns Promise resolving when the file is saved\n   */\n  async saveClaudeMdFile(filePath: string, content: string): Promise<string> {\n    try {\n      return await apiCall<string>(\"save_claude_md_file\", { filePath, content });\n    } catch (error) {\n      console.error(\"Failed to save CLAUDE.md file:\", error);\n      throw error;\n    }\n  },\n\n  // Agent API methods\n  \n  /**\n   * Lists all CC agents\n   * @returns Promise resolving to an array of agents\n   */\n  async listAgents(): Promise<Agent[]> {\n    try {\n      return await apiCall<Agent[]>('list_agents');\n    } catch (error) {\n      console.error(\"Failed to list agents:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Creates a new agent\n   * @param name - The agent name\n   * @param icon - The icon identifier\n   * @param system_prompt - The system prompt for the agent\n   * @param default_task - Optional default task\n   * @param model - Optional model (defaults to 'sonnet')\n   * @param hooks - Optional hooks configuration as JSON string\n   * @returns Promise resolving to the created agent\n   */\n  async createAgent(\n    name: string, \n    icon: string, \n    system_prompt: string, \n    default_task?: string, \n    model?: string,\n    hooks?: string\n  ): Promise<Agent> {\n    try {\n      return await apiCall<Agent>('create_agent', { \n        name, \n        icon, \n        systemPrompt: system_prompt,\n        defaultTask: default_task,\n        model,\n        hooks\n      });\n    } catch (error) {\n      console.error(\"Failed to create agent:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Updates an existing agent\n   * @param id - The agent ID\n   * @param name - The updated name\n   * @param icon - The updated icon\n   * @param system_prompt - The updated system prompt\n   * @param default_task - Optional default task\n   * @param model - Optional model\n   * @param hooks - Optional hooks configuration as JSON string\n   * @returns Promise resolving to the updated agent\n   */\n  async updateAgent(\n    id: number, \n    name: string, \n    icon: string, \n    system_prompt: string, \n    default_task?: string, \n    model?: string,\n    hooks?: string\n  ): Promise<Agent> {\n    try {\n      return await apiCall<Agent>('update_agent', { \n        id, \n        name, \n        icon, \n        systemPrompt: system_prompt,\n        defaultTask: default_task,\n        model,\n        hooks\n      });\n    } catch (error) {\n      console.error(\"Failed to update agent:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Deletes an agent\n   * @param id - The agent ID to delete\n   * @returns Promise resolving when the agent is deleted\n   */\n  async deleteAgent(id: number): Promise<void> {\n    try {\n      return await apiCall('delete_agent', { id });\n    } catch (error) {\n      console.error(\"Failed to delete agent:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets a single agent by ID\n   * @param id - The agent ID\n   * @returns Promise resolving to the agent\n   */\n  async getAgent(id: number): Promise<Agent> {\n    try {\n      return await apiCall<Agent>('get_agent', { id });\n    } catch (error) {\n      console.error(\"Failed to get agent:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Exports a single agent to JSON format\n   * @param id - The agent ID to export\n   * @returns Promise resolving to the JSON string\n   */\n  async exportAgent(id: number): Promise<string> {\n    try {\n      return await apiCall<string>('export_agent', { id });\n    } catch (error) {\n      console.error(\"Failed to export agent:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Imports an agent from JSON data\n   * @param jsonData - The JSON string containing the agent export\n   * @returns Promise resolving to the imported agent\n   */\n  async importAgent(jsonData: string): Promise<Agent> {\n    try {\n      return await apiCall<Agent>('import_agent', { jsonData });\n    } catch (error) {\n      console.error(\"Failed to import agent:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Imports an agent from a file\n   * @param filePath - The path to the JSON file\n   * @returns Promise resolving to the imported agent\n   */\n  async importAgentFromFile(filePath: string): Promise<Agent> {\n    try {\n      return await apiCall<Agent>('import_agent_from_file', { filePath });\n    } catch (error) {\n      console.error(\"Failed to import agent from file:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Executes an agent\n   * @param agentId - The agent ID to execute\n   * @param projectPath - The project path to run the agent in\n   * @param task - The task description\n   * @param model - Optional model override\n   * @returns Promise resolving to the run ID when execution starts\n   */\n  async executeAgent(agentId: number, projectPath: string, task: string, model?: string): Promise<number> {\n    try {\n      return await apiCall<number>('execute_agent', { agentId, projectPath, task, model });\n    } catch (error) {\n      console.error(\"Failed to execute agent:\", error);\n      // Return a sentinel value to indicate error\n      throw new Error(`Failed to execute agent: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Lists agent runs without metrics (basic info only)\n   * @param agentId - Optional agent ID to filter runs\n   * @returns Promise resolving to an array of agent runs\n   */\n  async listAgentRuns(agentId?: number): Promise<AgentRunWithMetrics[]> {\n    try {\n      return await apiCall<AgentRunWithMetrics[]>('list_agent_runs', { agentId });\n    } catch (error) {\n      console.error(\"Failed to list agent runs:\", error);\n      // Return empty array instead of throwing to prevent UI crashes\n      return [];\n    }\n  },\n\n  /**\n   * Lists agent runs with metrics (includes token counts and duration)\n   * @param agentId - Optional agent ID to filter runs\n   * @returns Promise resolving to an array of agent runs with metrics\n   */\n  async listAgentRunsWithMetrics(agentId?: number): Promise<AgentRunWithMetrics[]> {\n    try {\n      return await apiCall<AgentRunWithMetrics[]>('list_agent_runs_with_metrics', { agentId });\n    } catch (error) {\n      console.error(\"Failed to list agent runs with metrics:\", error);\n      // Return empty array instead of throwing to prevent UI crashes\n      return [];\n    }\n  },\n\n  /**\n   * Gets a single agent run by ID with metrics\n   * @param id - The run ID\n   * @returns Promise resolving to the agent run with metrics\n   */\n  async getAgentRun(id: number): Promise<AgentRunWithMetrics> {\n    try {\n      return await apiCall<AgentRunWithMetrics>('get_agent_run', { id });\n    } catch (error) {\n      console.error(\"Failed to get agent run:\", error);\n      throw new Error(`Failed to get agent run: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Gets a single agent run by ID with real-time metrics from JSONL\n   * @param id - The run ID\n   * @returns Promise resolving to the agent run with metrics\n   */\n  async getAgentRunWithRealTimeMetrics(id: number): Promise<AgentRunWithMetrics> {\n    try {\n      return await apiCall<AgentRunWithMetrics>('get_agent_run_with_real_time_metrics', { id });\n    } catch (error) {\n      console.error(\"Failed to get agent run with real-time metrics:\", error);\n      throw new Error(`Failed to get agent run with real-time metrics: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Lists all currently running agent sessions\n   * @returns Promise resolving to list of running agent sessions\n   */\n  async listRunningAgentSessions(): Promise<AgentRun[]> {\n    try {\n      return await apiCall<AgentRun[]>('list_running_sessions');\n    } catch (error) {\n      console.error(\"Failed to list running agent sessions:\", error);\n      throw new Error(`Failed to list running agent sessions: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Kills a running agent session\n   * @param runId - The run ID to kill\n   * @returns Promise resolving to whether the session was successfully killed\n   */\n  async killAgentSession(runId: number): Promise<boolean> {\n    try {\n      return await apiCall<boolean>('kill_agent_session', { runId });\n    } catch (error) {\n      console.error(\"Failed to kill agent session:\", error);\n      throw new Error(`Failed to kill agent session: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Gets the status of a specific agent session\n   * @param runId - The run ID to check\n   * @returns Promise resolving to the session status or null if not found\n   */\n  async getSessionStatus(runId: number): Promise<string | null> {\n    try {\n      return await apiCall<string | null>('get_session_status', { runId });\n    } catch (error) {\n      console.error(\"Failed to get session status:\", error);\n      throw new Error(`Failed to get session status: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Cleanup finished processes and update their status\n   * @returns Promise resolving to list of run IDs that were cleaned up\n   */\n  async cleanupFinishedProcesses(): Promise<number[]> {\n    try {\n      return await apiCall<number[]>('cleanup_finished_processes');\n    } catch (error) {\n      console.error(\"Failed to cleanup finished processes:\", error);\n      throw new Error(`Failed to cleanup finished processes: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Get real-time output for a running session (with live output fallback)\n   * @param runId - The run ID to get output for\n   * @returns Promise resolving to the current session output (JSONL format)\n   */\n  async getSessionOutput(runId: number): Promise<string> {\n    try {\n      return await apiCall<string>('get_session_output', { runId });\n    } catch (error) {\n      console.error(\"Failed to get session output:\", error);\n      throw new Error(`Failed to get session output: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Get live output directly from process stdout buffer\n   * @param runId - The run ID to get live output for\n   * @returns Promise resolving to the current live output\n   */\n  async getLiveSessionOutput(runId: number): Promise<string> {\n    try {\n      return await apiCall<string>('get_live_session_output', { runId });\n    } catch (error) {\n      console.error(\"Failed to get live session output:\", error);\n      throw new Error(`Failed to get live session output: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Start streaming real-time output for a running session\n   * @param runId - The run ID to stream output for\n   * @returns Promise that resolves when streaming starts\n   */\n  async streamSessionOutput(runId: number): Promise<void> {\n    try {\n      return await apiCall<void>('stream_session_output', { runId });\n    } catch (error) {\n      console.error(\"Failed to start streaming session output:\", error);\n      throw new Error(`Failed to start streaming session output: ${error instanceof Error ? error.message : 'Unknown error'}`);\n    }\n  },\n\n  /**\n   * Loads the JSONL history for a specific session\n   */\n  async loadSessionHistory(sessionId: string, projectId: string): Promise<any[]> {\n    return apiCall(\"load_session_history\", { sessionId, projectId });\n  },\n\n  /**\n   * Loads the JSONL history for a specific agent session\n   * Similar to loadSessionHistory but searches across all project directories\n   * @param sessionId - The session ID (UUID)\n   * @returns Promise resolving to array of session messages\n   */\n  async loadAgentSessionHistory(sessionId: string): Promise<any[]> {\n    try {\n      return await apiCall<any[]>('load_agent_session_history', { sessionId });\n    } catch (error) {\n      console.error(\"Failed to load agent session history:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Executes a new interactive Claude Code session with streaming output\n   */\n  async executeClaudeCode(projectPath: string, prompt: string, model: string): Promise<void> {\n    return apiCall(\"execute_claude_code\", { projectPath, prompt, model });\n  },\n\n  /**\n   * Continues an existing Claude Code conversation with streaming output\n   */\n  async continueClaudeCode(projectPath: string, prompt: string, model: string): Promise<void> {\n    return apiCall(\"continue_claude_code\", { projectPath, prompt, model });\n  },\n\n  /**\n   * Resumes an existing Claude Code session by ID with streaming output\n   */\n  async resumeClaudeCode(projectPath: string, sessionId: string, prompt: string, model: string): Promise<void> {\n    return apiCall(\"resume_claude_code\", { projectPath, sessionId, prompt, model });\n  },\n\n  /**\n   * Cancels the currently running Claude Code execution\n   * @param sessionId - Optional session ID to cancel a specific session\n   */\n  async cancelClaudeExecution(sessionId?: string): Promise<void> {\n    return apiCall(\"cancel_claude_execution\", { sessionId });\n  },\n\n  /**\n   * Lists all currently running Claude sessions\n   * @returns Promise resolving to list of running Claude sessions\n   */\n  async listRunningClaudeSessions(): Promise<any[]> {\n    return apiCall(\"list_running_claude_sessions\");\n  },\n\n  /**\n   * Gets live output from a Claude session\n   * @param sessionId - The session ID to get output for\n   * @returns Promise resolving to the current live output\n   */\n  async getClaudeSessionOutput(sessionId: string): Promise<string> {\n    return apiCall(\"get_claude_session_output\", { sessionId });\n  },\n\n  /**\n   * Lists files and directories in a given path\n   */\n  async listDirectoryContents(directoryPath: string): Promise<FileEntry[]> {\n    return apiCall(\"list_directory_contents\", { directoryPath });\n  },\n\n  /**\n   * Searches for files and directories matching a pattern\n   */\n  async searchFiles(basePath: string, query: string): Promise<FileEntry[]> {\n    return apiCall(\"search_files\", { basePath, query });\n  },\n\n  /**\n   * Gets overall usage statistics\n   * @returns Promise resolving to usage statistics\n   */\n  async getUsageStats(): Promise<UsageStats> {\n    try {\n      return await apiCall<UsageStats>(\"get_usage_stats\");\n    } catch (error) {\n      console.error(\"Failed to get usage stats:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets usage statistics filtered by date range\n   * @param startDate - Start date (ISO format)\n   * @param endDate - End date (ISO format)\n   * @returns Promise resolving to usage statistics\n   */\n  async getUsageByDateRange(startDate: string, endDate: string): Promise<UsageStats> {\n    try {\n      return await apiCall<UsageStats>(\"get_usage_by_date_range\", { startDate, endDate });\n    } catch (error) {\n      console.error(\"Failed to get usage by date range:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets usage statistics grouped by session\n   * @param since - Optional start date (YYYYMMDD)\n   * @param until - Optional end date (YYYYMMDD)\n   * @param order - Optional sort order ('asc' or 'desc')\n   * @returns Promise resolving to an array of session usage data\n   */\n  async getSessionStats(\n    since?: string,\n    until?: string,\n    order?: \"asc\" | \"desc\"\n  ): Promise<ProjectUsage[]> {\n    try {\n      return await apiCall<ProjectUsage[]>(\"get_session_stats\", {\n        since,\n        until,\n        order,\n      });\n    } catch (error) {\n      console.error(\"Failed to get session stats:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets detailed usage entries with optional filtering\n   * @param limit - Optional limit for number of entries\n   * @returns Promise resolving to array of usage entries\n   */\n  async getUsageDetails(limit?: number): Promise<UsageEntry[]> {\n    try {\n      return await apiCall<UsageEntry[]>(\"get_usage_details\", { limit });\n    } catch (error) {\n      console.error(\"Failed to get usage details:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Creates a checkpoint for the current session state\n   */\n  async createCheckpoint(\n    sessionId: string,\n    projectId: string,\n    projectPath: string,\n    messageIndex?: number,\n    description?: string\n  ): Promise<CheckpointResult> {\n    return apiCall(\"create_checkpoint\", {\n      sessionId,\n      projectId,\n      projectPath,\n      messageIndex,\n      description\n    });\n  },\n\n  /**\n   * Restores a session to a specific checkpoint\n   */\n  async restoreCheckpoint(\n    checkpointId: string,\n    sessionId: string,\n    projectId: string,\n    projectPath: string\n  ): Promise<CheckpointResult> {\n    return apiCall(\"restore_checkpoint\", {\n      checkpointId,\n      sessionId,\n      projectId,\n      projectPath\n    });\n  },\n\n  /**\n   * Lists all checkpoints for a session\n   */\n  async listCheckpoints(\n    sessionId: string,\n    projectId: string,\n    projectPath: string\n  ): Promise<Checkpoint[]> {\n    return apiCall(\"list_checkpoints\", {\n      sessionId,\n      projectId,\n      projectPath\n    });\n  },\n\n  /**\n   * Forks a new timeline branch from a checkpoint\n   */\n  async forkFromCheckpoint(\n    checkpointId: string,\n    sessionId: string,\n    projectId: string,\n    projectPath: string,\n    newSessionId: string,\n    description?: string\n  ): Promise<CheckpointResult> {\n    return apiCall(\"fork_from_checkpoint\", {\n      checkpointId,\n      sessionId,\n      projectId,\n      projectPath,\n      newSessionId,\n      description\n    });\n  },\n\n  /**\n   * Gets the timeline for a session\n   */\n  async getSessionTimeline(\n    sessionId: string,\n    projectId: string,\n    projectPath: string\n  ): Promise<SessionTimeline> {\n    return apiCall(\"get_session_timeline\", {\n      sessionId,\n      projectId,\n      projectPath\n    });\n  },\n\n  /**\n   * Updates checkpoint settings for a session\n   */\n  async updateCheckpointSettings(\n    sessionId: string,\n    projectId: string,\n    projectPath: string,\n    autoCheckpointEnabled: boolean,\n    checkpointStrategy: CheckpointStrategy\n  ): Promise<void> {\n    return apiCall(\"update_checkpoint_settings\", {\n      sessionId,\n      projectId,\n      projectPath,\n      autoCheckpointEnabled,\n      checkpointStrategy\n    });\n  },\n\n  /**\n   * Gets diff between two checkpoints\n   */\n  async getCheckpointDiff(\n    fromCheckpointId: string,\n    toCheckpointId: string,\n    sessionId: string,\n    projectId: string\n  ): Promise<CheckpointDiff> {\n    try {\n      return await apiCall<CheckpointDiff>(\"get_checkpoint_diff\", {\n        fromCheckpointId,\n        toCheckpointId,\n        sessionId,\n        projectId\n      });\n    } catch (error) {\n      console.error(\"Failed to get checkpoint diff:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Tracks a message for checkpointing\n   */\n  async trackCheckpointMessage(\n    sessionId: string,\n    projectId: string,\n    projectPath: string,\n    message: string\n  ): Promise<void> {\n    try {\n      await apiCall(\"track_checkpoint_message\", {\n        sessionId,\n        projectId,\n        projectPath,\n        message\n      });\n    } catch (error) {\n      console.error(\"Failed to track checkpoint message:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Checks if auto-checkpoint should be triggered\n   */\n  async checkAutoCheckpoint(\n    sessionId: string,\n    projectId: string,\n    projectPath: string,\n    message: string\n  ): Promise<boolean> {\n    try {\n      return await apiCall<boolean>(\"check_auto_checkpoint\", {\n        sessionId,\n        projectId,\n        projectPath,\n        message\n      });\n    } catch (error) {\n      console.error(\"Failed to check auto checkpoint:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Triggers cleanup of old checkpoints\n   */\n  async cleanupOldCheckpoints(\n    sessionId: string,\n    projectId: string,\n    projectPath: string,\n    keepCount: number\n  ): Promise<number> {\n    try {\n      return await apiCall<number>(\"cleanup_old_checkpoints\", {\n        sessionId,\n        projectId,\n        projectPath,\n        keepCount\n      });\n    } catch (error) {\n      console.error(\"Failed to cleanup old checkpoints:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets checkpoint settings for a session\n   */\n  async getCheckpointSettings(\n    sessionId: string,\n    projectId: string,\n    projectPath: string\n  ): Promise<{\n    auto_checkpoint_enabled: boolean;\n    checkpoint_strategy: CheckpointStrategy;\n    total_checkpoints: number;\n    current_checkpoint_id?: string;\n  }> {\n    try {\n      return await apiCall(\"get_checkpoint_settings\", {\n        sessionId,\n        projectId,\n        projectPath\n      });\n    } catch (error) {\n      console.error(\"Failed to get checkpoint settings:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Clears checkpoint manager for a session (cleanup on session end)\n   */\n  async clearCheckpointManager(sessionId: string): Promise<void> {\n    try {\n      await apiCall(\"clear_checkpoint_manager\", { sessionId });\n    } catch (error) {\n      console.error(\"Failed to clear checkpoint manager:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Tracks a batch of messages for a session for checkpointing\n   */\n  trackSessionMessages: (\n    sessionId: string, \n    projectId: string, \n    projectPath: string, \n    messages: string[]\n  ): Promise<void> =>\n    apiCall(\"track_session_messages\", { sessionId, projectId, projectPath, messages }),\n\n  /**\n   * Adds a new MCP server\n   */\n  async mcpAdd(\n    name: string,\n    transport: string,\n    command?: string,\n    args: string[] = [],\n    env: Record<string, string> = {},\n    url?: string,\n    scope: string = \"local\"\n  ): Promise<AddServerResult> {\n    try {\n      return await apiCall<AddServerResult>(\"mcp_add\", {\n        name,\n        transport,\n        command,\n        args,\n        env,\n        url,\n        scope\n      });\n    } catch (error) {\n      console.error(\"Failed to add MCP server:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Lists all configured MCP servers\n   */\n  async mcpList(): Promise<MCPServer[]> {\n    try {\n      console.log(\"API: Calling mcp_list...\");\n      const result = await apiCall<MCPServer[]>(\"mcp_list\");\n      console.log(\"API: mcp_list returned:\", result);\n      return result;\n    } catch (error) {\n      console.error(\"API: Failed to list MCP servers:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets details for a specific MCP server\n   */\n  async mcpGet(name: string): Promise<MCPServer> {\n    try {\n      return await apiCall<MCPServer>(\"mcp_get\", { name });\n    } catch (error) {\n      console.error(\"Failed to get MCP server:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Removes an MCP server\n   */\n  async mcpRemove(name: string): Promise<string> {\n    try {\n      return await apiCall<string>(\"mcp_remove\", { name });\n    } catch (error) {\n      console.error(\"Failed to remove MCP server:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Adds an MCP server from JSON configuration\n   */\n  async mcpAddJson(name: string, jsonConfig: string, scope: string = \"local\"): Promise<AddServerResult> {\n    try {\n      return await apiCall<AddServerResult>(\"mcp_add_json\", { name, jsonConfig, scope });\n    } catch (error) {\n      console.error(\"Failed to add MCP server from JSON:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Imports MCP servers from Claude Desktop\n   */\n  async mcpAddFromClaudeDesktop(scope: string = \"local\"): Promise<ImportResult> {\n    try {\n      return await apiCall<ImportResult>(\"mcp_add_from_claude_desktop\", { scope });\n    } catch (error) {\n      console.error(\"Failed to import from Claude Desktop:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Starts Claude Code as an MCP server\n   */\n  async mcpServe(): Promise<string> {\n    try {\n      return await apiCall<string>(\"mcp_serve\");\n    } catch (error) {\n      console.error(\"Failed to start MCP server:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Tests connection to an MCP server\n   */\n  async mcpTestConnection(name: string): Promise<string> {\n    try {\n      return await apiCall<string>(\"mcp_test_connection\", { name });\n    } catch (error) {\n      console.error(\"Failed to test MCP connection:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Resets project-scoped server approval choices\n   */\n  async mcpResetProjectChoices(): Promise<string> {\n    try {\n      return await apiCall<string>(\"mcp_reset_project_choices\");\n    } catch (error) {\n      console.error(\"Failed to reset project choices:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets the status of MCP servers\n   */\n  async mcpGetServerStatus(): Promise<Record<string, ServerStatus>> {\n    try {\n      return await apiCall<Record<string, ServerStatus>>(\"mcp_get_server_status\");\n    } catch (error) {\n      console.error(\"Failed to get server status:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Reads .mcp.json from the current project\n   */\n  async mcpReadProjectConfig(projectPath: string): Promise<MCPProjectConfig> {\n    try {\n      return await apiCall<MCPProjectConfig>(\"mcp_read_project_config\", { projectPath });\n    } catch (error) {\n      console.error(\"Failed to read project MCP config:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Saves .mcp.json to the current project\n   */\n  async mcpSaveProjectConfig(projectPath: string, config: MCPProjectConfig): Promise<string> {\n    try {\n      return await apiCall<string>(\"mcp_save_project_config\", { projectPath, config });\n    } catch (error) {\n      console.error(\"Failed to save project MCP config:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Get the stored Claude binary path from settings\n   * @returns Promise resolving to the path if set, null otherwise\n   */\n  async getClaudeBinaryPath(): Promise<string | null> {\n    try {\n      return await apiCall<string | null>(\"get_claude_binary_path\");\n    } catch (error) {\n      console.error(\"Failed to get Claude binary path:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Set the Claude binary path in settings\n   * @param path - The absolute path to the Claude binary\n   * @returns Promise resolving when the path is saved\n   */\n  async setClaudeBinaryPath(path: string): Promise<void> {\n    try {\n      return await apiCall<void>(\"set_claude_binary_path\", { path });\n    } catch (error) {\n      console.error(\"Failed to set Claude binary path:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * List all available Claude installations on the system\n   * @returns Promise resolving to an array of Claude installations\n   */\n  async listClaudeInstallations(): Promise<ClaudeInstallation[]> {\n    try {\n      return await apiCall<ClaudeInstallation[]>(\"list_claude_installations\");\n    } catch (error) {\n      console.error(\"Failed to list Claude installations:\", error);\n      throw error;\n    }\n  },\n\n  // Storage API methods\n\n  /**\n   * Lists all tables in the SQLite database\n   * @returns Promise resolving to an array of table information\n   */\n  async storageListTables(): Promise<any[]> {\n    try {\n      return await apiCall<any[]>(\"storage_list_tables\");\n    } catch (error) {\n      console.error(\"Failed to list tables:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Reads table data with pagination\n   * @param tableName - Name of the table to read\n   * @param page - Page number (1-indexed)\n   * @param pageSize - Number of rows per page\n   * @param searchQuery - Optional search query\n   * @returns Promise resolving to table data with pagination info\n   */\n  async storageReadTable(\n    tableName: string,\n    page: number,\n    pageSize: number,\n    searchQuery?: string\n  ): Promise<any> {\n    try {\n      return await apiCall<any>(\"storage_read_table\", {\n        tableName,\n        page,\n        pageSize,\n        searchQuery,\n      });\n    } catch (error) {\n      console.error(\"Failed to read table:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Updates a row in a table\n   * @param tableName - Name of the table\n   * @param primaryKeyValues - Map of primary key column names to values\n   * @param updates - Map of column names to new values\n   * @returns Promise resolving when the row is updated\n   */\n  async storageUpdateRow(\n    tableName: string,\n    primaryKeyValues: Record<string, any>,\n    updates: Record<string, any>\n  ): Promise<void> {\n    try {\n      return await apiCall<void>(\"storage_update_row\", {\n        tableName,\n        primaryKeyValues,\n        updates,\n      });\n    } catch (error) {\n      console.error(\"Failed to update row:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Deletes a row from a table\n   * @param tableName - Name of the table\n   * @param primaryKeyValues - Map of primary key column names to values\n   * @returns Promise resolving when the row is deleted\n   */\n  async storageDeleteRow(\n    tableName: string,\n    primaryKeyValues: Record<string, any>\n  ): Promise<void> {\n    try {\n      return await apiCall<void>(\"storage_delete_row\", {\n        tableName,\n        primaryKeyValues,\n      });\n    } catch (error) {\n      console.error(\"Failed to delete row:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Inserts a new row into a table\n   * @param tableName - Name of the table\n   * @param values - Map of column names to values\n   * @returns Promise resolving to the last insert row ID\n   */\n  async storageInsertRow(\n    tableName: string,\n    values: Record<string, any>\n  ): Promise<number> {\n    try {\n      return await apiCall<number>(\"storage_insert_row\", {\n        tableName,\n        values,\n      });\n    } catch (error) {\n      console.error(\"Failed to insert row:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Executes a raw SQL query\n   * @param query - SQL query string\n   * @returns Promise resolving to query result\n   */\n  async storageExecuteSql(query: string): Promise<any> {\n    try {\n      return await apiCall<any>(\"storage_execute_sql\", { query });\n    } catch (error) {\n      console.error(\"Failed to execute SQL:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Resets the entire database\n   * @returns Promise resolving when the database is reset\n   */\n  async storageResetDatabase(): Promise<void> {\n    try {\n      return await apiCall<void>(\"storage_reset_database\");\n    } catch (error) {\n      console.error(\"Failed to reset database:\", error);\n      throw error;\n    }\n  },\n\n  // Theme settings helpers\n\n  /**\n   * Gets a setting from the app_settings table\n   * @param key - The setting key to retrieve\n   * @returns Promise resolving to the setting value or null if not found\n   */\n  async getSetting(key: string): Promise<string | null> {\n    try {\n      // Fast path: check localStorage mirror to avoid startup flicker\n      if (typeof window !== 'undefined' && 'localStorage' in window) {\n        const cached = window.localStorage.getItem(`app_setting:${key}`);\n        if (cached !== null) {\n          return cached;\n        }\n      }\n      // Use storageReadTable to safely query the app_settings table\n      const result = await this.storageReadTable('app_settings', 1, 1000);\n      const setting = result?.data?.find((row: any) => row.key === key);\n      return setting?.value || null;\n    } catch (error) {\n      console.error(`Failed to get setting ${key}:`, error);\n      return null;\n    }\n  },\n\n  /**\n   * Saves a setting to the app_settings table (insert or update)\n   * @param key - The setting key\n   * @param value - The setting value\n   * @returns Promise resolving when the setting is saved\n   */\n  async saveSetting(key: string, value: string): Promise<void> {\n    try {\n      // Mirror to localStorage for instant availability on next startup\n      if (typeof window !== 'undefined' && 'localStorage' in window) {\n        try {\n          window.localStorage.setItem(`app_setting:${key}`, value);\n        } catch (_ignore) {\n          // best-effort; continue to persist in DB\n        }\n      }\n      // Try to update first\n      try {\n        await this.storageUpdateRow(\n          'app_settings',\n          { key },\n          { value }\n        );\n      } catch (updateError) {\n        // If update fails (row doesn't exist), insert new row\n        await this.storageInsertRow('app_settings', { key, value });\n      }\n    } catch (error) {\n      console.error(`Failed to save setting ${key}:`, error);\n      throw error;\n    }\n  },\n\n  /**\n   * Get hooks configuration for a specific scope\n   * @param scope - The configuration scope: 'user', 'project', or 'local'\n   * @param projectPath - Project path (required for project and local scopes)\n   * @returns Promise resolving to the hooks configuration\n   */\n  async getHooksConfig(scope: 'user' | 'project' | 'local', projectPath?: string): Promise<HooksConfiguration> {\n    try {\n      return await apiCall<HooksConfiguration>(\"get_hooks_config\", { scope, projectPath });\n    } catch (error) {\n      console.error(\"Failed to get hooks config:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Update hooks configuration for a specific scope\n   * @param scope - The configuration scope: 'user', 'project', or 'local'\n   * @param hooks - The hooks configuration to save\n   * @param projectPath - Project path (required for project and local scopes)\n   * @returns Promise resolving to success message\n   */\n  async updateHooksConfig(\n    scope: 'user' | 'project' | 'local',\n    hooks: HooksConfiguration,\n    projectPath?: string\n  ): Promise<string> {\n    try {\n      return await apiCall<string>(\"update_hooks_config\", { scope, projectPath, hooks });\n    } catch (error) {\n      console.error(\"Failed to update hooks config:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Validate a hook command syntax\n   * @param command - The shell command to validate\n   * @returns Promise resolving to validation result\n   */\n  async validateHookCommand(command: string): Promise<{ valid: boolean; message: string }> {\n    try {\n      return await apiCall<{ valid: boolean; message: string }>(\"validate_hook_command\", { command });\n    } catch (error) {\n      console.error(\"Failed to validate hook command:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Get merged hooks configuration (respecting priority)\n   * @param projectPath - The project path\n   * @returns Promise resolving to merged hooks configuration\n   */\n  async getMergedHooksConfig(projectPath: string): Promise<HooksConfiguration> {\n    try {\n      const [userHooks, projectHooks, localHooks] = await Promise.all([\n        this.getHooksConfig('user'),\n        this.getHooksConfig('project', projectPath),\n        this.getHooksConfig('local', projectPath)\n      ]);\n\n      // Import HooksManager for merging\n      const { HooksManager } = await import('@/lib/hooksManager');\n      return HooksManager.mergeConfigs(userHooks, projectHooks, localHooks);\n    } catch (error) {\n      console.error(\"Failed to get merged hooks config:\", error);\n      throw error;\n    }\n  },\n\n  // Slash Commands API methods\n\n  /**\n   * Lists all available slash commands\n   * @param projectPath - Optional project path to include project-specific commands\n   * @returns Promise resolving to array of slash commands\n   */\n  async slashCommandsList(projectPath?: string): Promise<SlashCommand[]> {\n    try {\n      return await apiCall<SlashCommand[]>(\"slash_commands_list\", { projectPath });\n    } catch (error) {\n      console.error(\"Failed to list slash commands:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Gets a single slash command by ID\n   * @param commandId - Unique identifier of the command\n   * @returns Promise resolving to the slash command\n   */\n  async slashCommandGet(commandId: string): Promise<SlashCommand> {\n    try {\n      return await apiCall<SlashCommand>(\"slash_command_get\", { commandId });\n    } catch (error) {\n      console.error(\"Failed to get slash command:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Creates or updates a slash command\n   * @param scope - Command scope: \"project\" or \"user\"\n   * @param name - Command name (without prefix)\n   * @param namespace - Optional namespace for organization\n   * @param content - Markdown content of the command\n   * @param description - Optional description\n   * @param allowedTools - List of allowed tools for this command\n   * @param projectPath - Required for project scope commands\n   * @returns Promise resolving to the saved command\n   */\n  async slashCommandSave(\n    scope: string,\n    name: string,\n    namespace: string | undefined,\n    content: string,\n    description: string | undefined,\n    allowedTools: string[],\n    projectPath?: string\n  ): Promise<SlashCommand> {\n    try {\n      return await apiCall<SlashCommand>(\"slash_command_save\", {\n        scope,\n        name,\n        namespace,\n        content,\n        description,\n        allowedTools,\n        projectPath\n      });\n    } catch (error) {\n      console.error(\"Failed to save slash command:\", error);\n      throw error;\n    }\n  },\n\n  /**\n   * Deletes a slash command\n   * @param commandId - Unique identifier of the command to delete\n   * @param projectPath - Optional project path for deleting project commands\n   * @returns Promise resolving to deletion message\n   */\n  async slashCommandDelete(commandId: string, projectPath?: string): Promise<string> {\n    try {\n      return await apiCall<string>(\"slash_command_delete\", { commandId, projectPath });\n    } catch (error) {\n      console.error(\"Failed to delete slash command:\", error);\n      throw error;\n    }\n  },\n\n};\n"
  },
  {
    "path": "src/lib/apiAdapter.ts",
    "content": "/**\n * API Adapter - Compatibility layer for Tauri vs Web environments\n * \n * This module detects whether we're running in Tauri (desktop app) or web browser\n * and provides a unified interface that switches between:\n * - Tauri invoke calls (for desktop)\n * - REST API calls (for web/phone browser)\n */\n\nimport { invoke } from \"@tauri-apps/api/core\";\n\n// Extend Window interface for Tauri\ndeclare global {\n  interface Window {\n    __TAURI__?: any;\n    __TAURI_METADATA__?: any;\n    __TAURI_INTERNALS__?: any;\n  }\n}\n\n// Environment detection\nlet isTauriEnvironment: boolean | null = null;\n\n/**\n * Detect if we're running in Tauri environment\n */\nfunction detectEnvironment(): boolean {\n  if (isTauriEnvironment !== null) {\n    return isTauriEnvironment;\n  }\n\n  // Check if we're in a browser environment first\n  if (typeof window === 'undefined') {\n    isTauriEnvironment = false;\n    return false;\n  }\n\n  // Check for Tauri-specific indicators\n  const isTauri = !!(\n    window.__TAURI__ || \n    window.__TAURI_METADATA__ ||\n    window.__TAURI_INTERNALS__ ||\n    // Check user agent for Tauri\n    navigator.userAgent.includes('Tauri')\n  );\n\n  console.log('[detectEnvironment] isTauri:', isTauri, 'userAgent:', navigator.userAgent);\n  \n  isTauriEnvironment = isTauri;\n  return isTauri;\n}\n\n/**\n * Response wrapper for REST API calls\n */\ninterface ApiResponse<T> {\n  success: boolean;\n  data?: T;\n  error?: string;\n}\n\n/**\n * Make a REST API call to our web server\n */\nasync function restApiCall<T>(endpoint: string, params?: any): Promise<T> {\n  // First handle path parameters in the endpoint string\n  let processedEndpoint = endpoint;\n  console.log(`[REST API] Original endpoint: ${endpoint}, params:`, params);\n  \n  if (params) {\n    Object.keys(params).forEach(key => {\n      // Try different case variations for the placeholder\n      const placeholders = [\n        `{${key}}`,\n        `{${key.charAt(0).toLowerCase() + key.slice(1)}}`,\n        `{${key.charAt(0).toUpperCase() + key.slice(1)}}`\n      ];\n      \n      placeholders.forEach(placeholder => {\n        if (processedEndpoint.includes(placeholder)) {\n          console.log(`[REST API] Replacing ${placeholder} with ${params[key]}`);\n          processedEndpoint = processedEndpoint.replace(placeholder, encodeURIComponent(String(params[key])));\n        }\n      });\n    });\n  }\n  \n  console.log(`[REST API] Processed endpoint: ${processedEndpoint}`);\n  \n  const url = new URL(processedEndpoint, window.location.origin);\n  \n  // Add remaining params as query parameters for GET requests (if no placeholders remain)\n  if (params && !processedEndpoint.includes('{')) {\n    Object.keys(params).forEach(key => {\n      // Only add as query param if it wasn't used as a path param\n      if (!endpoint.includes(`{${key}}`) && \n          !endpoint.includes(`{${key.charAt(0).toLowerCase() + key.slice(1)}}`) &&\n          !endpoint.includes(`{${key.charAt(0).toUpperCase() + key.slice(1)}}`) &&\n          params[key] !== undefined && \n          params[key] !== null) {\n        url.searchParams.append(key, String(params[key]));\n      }\n    });\n  }\n\n  try {\n    const response = await fetch(url.toString(), {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n\n    const result: ApiResponse<T> = await response.json();\n    \n    if (!result.success) {\n      throw new Error(result.error || 'API call failed');\n    }\n\n    return result.data as T;\n  } catch (error) {\n    console.error(`REST API call failed for ${endpoint}:`, error);\n    throw error;\n  }\n}\n\n/**\n * Unified API adapter that works in both Tauri and web environments\n */\nexport async function apiCall<T>(command: string, params?: any): Promise<T> {\n  const isWeb = !detectEnvironment();\n  \n  if (!isWeb) {\n    // Tauri environment - try invoke\n    console.log(`[Tauri] Calling: ${command}`, params);\n    try {\n      return await invoke<T>(command, params);\n    } catch (error) {\n      console.warn(`[Tauri] invoke failed, falling back to web mode:`, error);\n      // Fall through to web mode\n    }\n  }\n  \n  // Web environment - use REST API\n  console.log(`[Web] Calling: ${command}`, params);\n  \n  // Special handling for commands that use streaming/events\n  const streamingCommands = ['execute_claude_code', 'continue_claude_code', 'resume_claude_code'];\n  if (streamingCommands.includes(command)) {\n    return handleStreamingCommand<T>(command, params);\n  }\n  \n  // Map Tauri commands to REST endpoints\n  const endpoint = mapCommandToEndpoint(command, params);\n  return await restApiCall<T>(endpoint, params);\n}\n\n/**\n * Map Tauri command names to REST API endpoints\n */\nfunction mapCommandToEndpoint(command: string, _params?: any): string {\n  const commandToEndpoint: Record<string, string> = {\n    // Project and session commands\n    'list_projects': '/api/projects',\n    'get_project_sessions': '/api/projects/{projectId}/sessions',\n    \n    // Agent commands\n    'list_agents': '/api/agents',\n    'fetch_github_agents': '/api/agents/github',\n    'fetch_github_agent_content': '/api/agents/github/content',\n    'import_agent_from_github': '/api/agents/import/github',\n    'create_agent': '/api/agents',\n    'update_agent': '/api/agents/{id}',\n    'delete_agent': '/api/agents/{id}',\n    'get_agent': '/api/agents/{id}',\n    'export_agent': '/api/agents/{id}/export',\n    'import_agent': '/api/agents/import',\n    'import_agent_from_file': '/api/agents/import/file',\n    'execute_agent': '/api/agents/{agentId}/execute',\n    'list_agent_runs': '/api/agents/runs',\n    'get_agent_run': '/api/agents/runs/{id}',\n    'get_agent_run_with_real_time_metrics': '/api/agents/runs/{id}/metrics',\n    'list_running_sessions': '/api/sessions/running',\n    'kill_agent_session': '/api/agents/sessions/{runId}/kill',\n    'get_session_status': '/api/agents/sessions/{runId}/status',\n    'cleanup_finished_processes': '/api/agents/sessions/cleanup',\n    'get_session_output': '/api/agents/sessions/{runId}/output',\n    'get_live_session_output': '/api/agents/sessions/{runId}/output/live',\n    'stream_session_output': '/api/agents/sessions/{runId}/output/stream',\n    'load_agent_session_history': '/api/agents/sessions/{sessionId}/history',\n    \n    // Usage commands\n    'get_usage_stats': '/api/usage',\n    'get_usage_by_date_range': '/api/usage/range',\n    'get_session_stats': '/api/usage/sessions',\n    'get_usage_details': '/api/usage/details',\n    \n    // Settings and configuration\n    'get_claude_settings': '/api/settings/claude',\n    'save_claude_settings': '/api/settings/claude',\n    'get_system_prompt': '/api/settings/system-prompt',\n    'save_system_prompt': '/api/settings/system-prompt',\n    'check_claude_version': '/api/settings/claude/version',\n    'find_claude_md_files': '/api/claude-md',\n    'read_claude_md_file': '/api/claude-md/read',\n    'save_claude_md_file': '/api/claude-md/save',\n    \n    // Session management\n    'open_new_session': '/api/sessions/new',\n    'load_session_history': '/api/sessions/{sessionId}/history/{projectId}',\n    'list_running_claude_sessions': '/api/sessions/running',\n    'execute_claude_code': '/api/sessions/execute',\n    'continue_claude_code': '/api/sessions/continue',\n    'resume_claude_code': '/api/sessions/resume',\n    'cancel_claude_execution': '/api/sessions/{sessionId}/cancel',\n    'get_claude_session_output': '/api/sessions/{sessionId}/output',\n    \n    // MCP commands\n    'mcp_add': '/api/mcp/servers',\n    'mcp_list': '/api/mcp/servers',\n    'mcp_get': '/api/mcp/servers/{name}',\n    'mcp_remove': '/api/mcp/servers/{name}',\n    'mcp_add_json': '/api/mcp/servers/json',\n    'mcp_add_from_claude_desktop': '/api/mcp/import/claude-desktop',\n    'mcp_serve': '/api/mcp/serve',\n    'mcp_test_connection': '/api/mcp/servers/{name}/test',\n    'mcp_reset_project_choices': '/api/mcp/reset-choices',\n    'mcp_get_server_status': '/api/mcp/status',\n    'mcp_read_project_config': '/api/mcp/project-config',\n    'mcp_save_project_config': '/api/mcp/project-config',\n    \n    // Binary and installation management\n    'get_claude_binary_path': '/api/settings/claude/binary-path',\n    'set_claude_binary_path': '/api/settings/claude/binary-path',\n    'list_claude_installations': '/api/settings/claude/installations',\n    \n    // Storage commands\n    'storage_list_tables': '/api/storage/tables',\n    'storage_read_table': '/api/storage/tables/{tableName}',\n    'storage_update_row': '/api/storage/tables/{tableName}/rows/{id}',\n    'storage_delete_row': '/api/storage/tables/{tableName}/rows/{id}',\n    'storage_insert_row': '/api/storage/tables/{tableName}/rows',\n    'storage_execute_sql': '/api/storage/sql',\n    'storage_reset_database': '/api/storage/reset',\n    \n    // Hooks configuration\n    'get_hooks_config': '/api/hooks/config',\n    'update_hooks_config': '/api/hooks/config',\n    'validate_hook_command': '/api/hooks/validate',\n    \n    // Slash commands\n    'slash_commands_list': '/api/slash-commands',\n    'slash_command_get': '/api/slash-commands/{commandId}',\n    'slash_command_save': '/api/slash-commands',\n    'slash_command_delete': '/api/slash-commands/{commandId}',\n  };\n\n  const endpoint = commandToEndpoint[command];\n  if (!endpoint) {\n    console.warn(`Unknown command: ${command}, falling back to generic endpoint`);\n    return `/api/unknown/${command}`;\n  }\n\n  return endpoint;\n}\n\n/**\n * Get environment info for debugging\n */\nexport function getEnvironmentInfo() {\n  return {\n    isTauri: detectEnvironment(),\n    userAgent: navigator.userAgent,\n    location: window.location.href,\n  };\n}\n\n/**\n * Handle streaming commands via WebSocket in web mode\n */\nasync function handleStreamingCommand<T>(command: string, params?: any): Promise<T> {\n  return new Promise((resolve, reject) => {\n    // Use wss:// for HTTPS connections (e.g., ngrok), ws:// for HTTP (localhost)\n    const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n    const wsUrl = `${wsProtocol}//${window.location.host}/ws/claude`;\n    console.log(`[TRACE] handleStreamingCommand called:`);\n    console.log(`[TRACE]   command: ${command}`);\n    console.log(`[TRACE]   params:`, params);\n    console.log(`[TRACE]   WebSocket URL: ${wsUrl}`);\n    \n    const ws = new WebSocket(wsUrl);\n    \n    ws.onopen = () => {\n      console.log(`[TRACE] WebSocket opened successfully`);\n      \n      // Send execution request\n      const request = {\n        command_type: command.replace('_claude_code', ''), // execute, continue, resume\n        project_path: params?.projectPath || '',\n        prompt: params?.prompt || '',\n        model: params?.model || 'claude-3-5-sonnet-20241022',\n        session_id: params?.sessionId,\n      };\n      \n      console.log(`[TRACE] Sending WebSocket request:`, request);\n      console.log(`[TRACE] Request JSON:`, JSON.stringify(request));\n      \n      ws.send(JSON.stringify(request));\n      console.log(`[TRACE] WebSocket request sent`);\n    };\n    \n    ws.onmessage = (event) => {\n      console.log(`[TRACE] WebSocket message received:`, event.data);\n      try {\n        const message = JSON.parse(event.data);\n        console.log(`[TRACE] Parsed WebSocket message:`, message);\n        \n        if (message.type === 'start') {\n          console.log(`[TRACE] Start message: ${message.message}`);\n        } else if (message.type === 'output') {\n          console.log(`[TRACE] Output message, content length: ${message.content?.length || 0}`);\n          console.log(`[TRACE] Raw content:`, message.content);\n          \n          // The backend sends Claude output as a JSON string in the content field\n          // We need to parse this to get the actual Claude message\n          try {\n            const claudeMessage = typeof message.content === 'string' \n              ? JSON.parse(message.content) \n              : message.content;\n            console.log(`[TRACE] Parsed Claude message:`, claudeMessage);\n            \n            // Simulate Tauri event for compatibility with existing UI\n            const customEvent = new CustomEvent('claude-output', {\n              detail: claudeMessage\n            });\n            console.log(`[TRACE] Dispatching claude-output event:`, customEvent.detail);\n            console.log(`[TRACE] Event type:`, customEvent.type);\n            window.dispatchEvent(customEvent);\n          } catch (e) {\n            console.error(`[TRACE] Failed to parse Claude output content:`, e);\n            console.error(`[TRACE] Content that failed to parse:`, message.content);\n          }\n        } else if (message.type === 'completion') {\n          console.log(`[TRACE] Completion message:`, message);\n          \n          // Dispatch claude-complete event for UI state management\n          const completeEvent = new CustomEvent('claude-complete', {\n            detail: message.status === 'success'\n          });\n          console.log(`[TRACE] Dispatching claude-complete event:`, completeEvent.detail);\n          window.dispatchEvent(completeEvent);\n          \n          ws.close();\n          if (message.status === 'success') {\n            console.log(`[TRACE] Resolving promise with success`);\n            resolve({} as T); // Return empty object for now\n          } else {\n            console.log(`[TRACE] Rejecting promise with error: ${message.error}`);\n            reject(new Error(message.error || 'Execution failed'));\n          }\n        } else if (message.type === 'error') {\n          console.log(`[TRACE] Error message:`, message);\n          \n          // Dispatch claude-error event for UI error handling\n          const errorEvent = new CustomEvent('claude-error', {\n            detail: message.message || 'Unknown error'\n          });\n          console.log(`[TRACE] Dispatching claude-error event:`, errorEvent.detail);\n          window.dispatchEvent(errorEvent);\n          \n          reject(new Error(message.message || 'Unknown error'));\n        } else {\n          console.log(`[TRACE] Unknown message type: ${message.type}`);\n        }\n      } catch (e) {\n        console.error('[TRACE] Failed to parse WebSocket message:', e);\n        console.error('[TRACE] Raw message:', event.data);\n      }\n    };\n    \n    ws.onerror = (error) => {\n      console.error('[TRACE] WebSocket error:', error);\n      \n      // Dispatch claude-error event for connection errors\n      const errorEvent = new CustomEvent('claude-error', {\n        detail: 'WebSocket connection failed'\n      });\n      console.log(`[TRACE] Dispatching claude-error event for WebSocket error`);\n      window.dispatchEvent(errorEvent);\n      \n      reject(new Error('WebSocket connection failed'));\n    };\n    \n    ws.onclose = (event) => {\n      console.log(`[TRACE] WebSocket closed - code: ${event.code}, reason: ${event.reason}`);\n      \n      // If connection closed unexpectedly (not a normal close), dispatch cancelled event\n      if (event.code !== 1000 && event.code !== 1001) {\n        const cancelEvent = new CustomEvent('claude-complete', {\n          detail: false // false indicates cancellation/failure\n        });\n        console.log(`[TRACE] Dispatching claude-complete event for unexpected close`);\n        window.dispatchEvent(cancelEvent);\n      }\n    };\n  });\n}\n\n/**\n * Initialize web mode compatibility\n * Sets up mocks for Tauri APIs when running in web mode\n */\nexport function initializeWebMode() {\n  if (!detectEnvironment()) {\n    // Mock Tauri event system for web mode\n    if (!window.__TAURI__) {\n      window.__TAURI__ = {\n        event: {\n          listen: (eventName: string, callback: (event: any) => void) => {\n            // Listen for custom events that simulate Tauri events\n            const handler = (e: any) => callback({ payload: e.detail });\n            window.addEventListener(`${eventName}`, handler);\n            return Promise.resolve(() => {\n              window.removeEventListener(`${eventName}`, handler);\n            });\n          },\n          emit: () => Promise.resolve(),\n        },\n        invoke: () => Promise.reject(new Error('Tauri invoke not available in web mode')),\n        // Mock the core module that includes transformCallback\n        core: {\n          invoke: () => Promise.reject(new Error('Tauri invoke not available in web mode')),\n          transformCallback: () => {\n            throw new Error('Tauri transformCallback not available in web mode');\n          }\n        }\n      };\n    }\n  }\n}"
  },
  {
    "path": "src/lib/claudeSyntaxTheme.ts",
    "content": "import { ThemeMode } from '@/contexts/ThemeContext';\n\n/**\n * Claude-themed syntax highlighting theme factory\n * Returns different syntax themes based on the current theme mode\n * \n * @param theme - The current theme mode\n * @returns Prism syntax highlighting theme object\n */\nexport const getClaudeSyntaxTheme = (theme: ThemeMode): any => {\n  const themes = {\n    dark: {\n      base: '#e3e8f0',\n      background: 'transparent',\n      comment: '#6b7280',\n      punctuation: '#9ca3af',\n      property: '#f59e0b', // Amber/Orange\n      tag: '#8b5cf6', // Violet\n      string: '#10b981', // Emerald Green\n      function: '#818cf8', // Indigo\n      keyword: '#c084fc', // Light Violet\n      variable: '#a78bfa', // Light Purple\n      operator: '#9ca3af',\n    },\n    gray: {\n      base: '#e3e8f0',\n      background: 'transparent',\n      comment: '#71717a',\n      punctuation: '#a1a1aa',\n      property: '#fbbf24', // Yellow\n      tag: '#a78bfa', // Light Purple\n      string: '#34d399', // Green\n      function: '#93bbfc', // Light Blue\n      keyword: '#d8b4fe', // Light Purple\n      variable: '#c084fc', // Purple\n      operator: '#a1a1aa',\n    },\n    light: {\n      base: '#1f2937',\n      background: 'transparent',\n      comment: '#9ca3af',\n      punctuation: '#6b7280',\n      property: '#dc2626', // Red\n      tag: '#7c3aed', // Purple\n      string: '#059669', // Green\n      function: '#2563eb', // Blue\n      keyword: '#9333ea', // Purple\n      variable: '#8b5cf6', // Violet\n      operator: '#6b7280',\n    },\n    white: {\n      base: '#000000',\n      background: 'transparent',\n      comment: '#6b7280',\n      punctuation: '#374151',\n      property: '#dc2626', // Red\n      tag: '#5b21b6', // Deep Purple\n      string: '#047857', // Dark Green\n      function: '#1e40af', // Dark Blue\n      keyword: '#6b21a8', // Dark Purple\n      variable: '#6d28d9', // Dark Violet\n      operator: '#374151',\n    },\n    custom: {\n      // Default to dark theme colors for custom\n      base: '#e3e8f0',\n      background: 'transparent',\n      comment: '#6b7280',\n      punctuation: '#9ca3af',\n      property: '#f59e0b',\n      tag: '#8b5cf6',\n      string: '#10b981',\n      function: '#818cf8',\n      keyword: '#c084fc',\n      variable: '#a78bfa',\n      operator: '#9ca3af',\n    }\n  };\n\n  const colors = themes[theme] || themes.dark;\n\n  return {\n    'code[class*=\"language-\"]': {\n      color: colors.base,\n      background: colors.background,\n      textShadow: 'none',\n      fontFamily: 'var(--font-mono)',\n      fontSize: '0.875em',\n      textAlign: 'left',\n      whiteSpace: 'pre',\n      wordSpacing: 'normal',\n      wordBreak: 'normal',\n      wordWrap: 'normal',\n      lineHeight: '1.5',\n      MozTabSize: '4',\n      OTabSize: '4',\n      tabSize: '4',\n      WebkitHyphens: 'none',\n      MozHyphens: 'none',\n      msHyphens: 'none',\n      hyphens: 'none',\n    },\n    'pre[class*=\"language-\"]': {\n      color: colors.base,\n      background: colors.background,\n      textShadow: 'none',\n      fontFamily: 'var(--font-mono)',\n      fontSize: '0.875em',\n      textAlign: 'left',\n      whiteSpace: 'pre',\n      wordSpacing: 'normal',\n      wordBreak: 'normal',\n      wordWrap: 'normal',\n      lineHeight: '1.5',\n      MozTabSize: '4',\n      OTabSize: '4',\n      tabSize: '4',\n      WebkitHyphens: 'none',\n      MozHyphens: 'none',\n      msHyphens: 'none',\n      hyphens: 'none',\n      padding: '1em',\n      margin: '0',\n      overflow: 'auto',\n    },\n    ':not(pre) > code[class*=\"language-\"]': {\n      background: theme === 'light' \n        ? 'rgba(139, 92, 246, 0.1)' \n        : 'rgba(139, 92, 246, 0.1)',\n      padding: '0.1em 0.3em',\n      borderRadius: '0.3em',\n      whiteSpace: 'normal',\n    },\n    'comment': {\n      color: colors.comment,\n      fontStyle: 'italic',\n    },\n    'prolog': {\n      color: colors.comment,\n    },\n    'doctype': {\n      color: colors.comment,\n    },\n    'cdata': {\n      color: colors.comment,\n    },\n    'punctuation': {\n      color: colors.punctuation,\n    },\n    'namespace': {\n      opacity: '0.7',\n    },\n    'property': {\n      color: colors.property,\n    },\n    'tag': {\n      color: colors.tag,\n    },\n    'boolean': {\n      color: colors.property,\n    },\n    'number': {\n      color: colors.property,\n    },\n    'constant': {\n      color: colors.property,\n    },\n    'symbol': {\n      color: colors.property,\n    },\n    'deleted': {\n      color: '#ef4444',\n    },\n    'selector': {\n      color: colors.variable,\n    },\n    'attr-name': {\n      color: colors.variable,\n    },\n    'string': {\n      color: colors.string,\n    },\n    'char': {\n      color: colors.string,\n    },\n    'builtin': {\n      color: colors.tag,\n    },\n    'url': {\n      color: colors.string,\n    },\n    'inserted': {\n      color: colors.string,\n    },\n    'entity': {\n      color: colors.variable,\n      cursor: 'help',\n    },\n    'atrule': {\n      color: colors.keyword,\n    },\n    'attr-value': {\n      color: colors.string,\n    },\n    'keyword': {\n      color: colors.keyword,\n    },\n    'function': {\n      color: colors.function,\n    },\n    'class-name': {\n      color: colors.property,\n    },\n    'regex': {\n      color: '#06b6d4', // Cyan\n    },\n    'important': {\n      color: colors.property,\n      fontWeight: 'bold',\n    },\n    'variable': {\n      color: colors.variable,\n    },\n    'bold': {\n      fontWeight: 'bold',\n    },\n    'italic': {\n      fontStyle: 'italic',\n    },\n    'operator': {\n      color: colors.operator,\n    },\n    'script': {\n      color: colors.base,\n    },\n    'parameter': {\n      color: colors.property,\n    },\n    'method': {\n      color: colors.function,\n    },\n    'field': {\n      color: colors.property,\n    },\n    'annotation': {\n      color: colors.comment,\n    },\n    'type': {\n      color: colors.variable,\n    },\n    'module': {\n      color: colors.tag,\n    },\n  };\n};\n\n// Export default dark theme for backward compatibility\nexport const claudeSyntaxTheme = getClaudeSyntaxTheme('dark');"
  },
  {
    "path": "src/lib/date-utils.ts",
    "content": "/**\n * Formats a Unix timestamp to a human-readable date string\n * @param timestamp - Unix timestamp in seconds\n * @returns Formatted date string\n * \n * @example\n * formatUnixTimestamp(1735555200) // \"Dec 30, 2024\"\n */\nexport function formatUnixTimestamp(timestamp: number): string {\n  const date = new Date(timestamp * 1000);\n  const now = new Date();\n  \n  // If it's today, show time\n  if (isToday(date)) {\n    return formatTime(date);\n  }\n  \n  // If it's yesterday\n  if (isYesterday(date)) {\n    return `Yesterday, ${formatTime(date)}`;\n  }\n  \n  // If it's within the last week, show day of week\n  if (isWithinWeek(date)) {\n    return `${getDayName(date)}, ${formatTime(date)}`;\n  }\n  \n  // If it's this year, don't show year\n  if (date.getFullYear() === now.getFullYear()) {\n    return date.toLocaleDateString('en-US', { \n      month: 'short', \n      day: 'numeric' \n    });\n  }\n  \n  // Otherwise show full date\n  return date.toLocaleDateString('en-US', { \n    month: 'short', \n    day: 'numeric',\n    year: 'numeric'\n  });\n}\n\n/**\n * Formats an ISO timestamp string to a human-readable date\n * @param isoString - ISO timestamp string\n * @returns Formatted date string\n * \n * @example\n * formatISOTimestamp(\"2025-01-04T10:13:29.000Z\") // \"Jan 4, 2025\"\n */\nexport function formatISOTimestamp(isoString: string): string {\n  const date = new Date(isoString);\n  return formatUnixTimestamp(Math.floor(date.getTime() / 1000));\n}\n\n/**\n * Truncates text to a specified length with ellipsis\n * @param text - Text to truncate\n * @param maxLength - Maximum length\n * @returns Truncated text\n */\nexport function truncateText(text: string, maxLength: number): string {\n  if (text.length <= maxLength) return text;\n  return text.slice(0, maxLength - 3) + '...';\n}\n\n/**\n * Gets the first line of text\n * @param text - Text to process\n * @returns First line of text\n */\nexport function getFirstLine(text: string): string {\n  const lines = text.split('\\n');\n  return lines[0] || '';\n}\n\n// Helper functions\nfunction formatTime(date: Date): string {\n  return date.toLocaleTimeString('en-US', { \n    hour: 'numeric', \n    minute: '2-digit',\n    hour12: true \n  });\n}\n\nfunction isToday(date: Date): boolean {\n  const today = new Date();\n  return date.toDateString() === today.toDateString();\n}\n\nfunction isYesterday(date: Date): boolean {\n  const yesterday = new Date();\n  yesterday.setDate(yesterday.getDate() - 1);\n  return date.toDateString() === yesterday.toDateString();\n}\n\nfunction isWithinWeek(date: Date): boolean {\n  const weekAgo = new Date();\n  weekAgo.setDate(weekAgo.getDate() - 7);\n  return date > weekAgo;\n}\n\nfunction getDayName(date: Date): string {\n  return date.toLocaleDateString('en-US', { weekday: 'long' });\n}\n\n/**\n * Formats a timestamp to a relative time string (e.g., \"2 hours ago\", \"3 days ago\")\n * @param timestamp - Unix timestamp in milliseconds\n * @returns Relative time string\n * \n * @example\n * formatTimeAgo(Date.now() - 3600000) // \"1 hour ago\"\n * formatTimeAgo(Date.now() - 86400000) // \"1 day ago\"\n */\nexport function formatTimeAgo(timestamp: number): string {\n  const now = Date.now();\n  const diff = now - timestamp;\n  \n  const seconds = Math.floor(diff / 1000);\n  const minutes = Math.floor(seconds / 60);\n  const hours = Math.floor(minutes / 60);\n  const days = Math.floor(hours / 24);\n  const weeks = Math.floor(days / 7);\n  const months = Math.floor(days / 30);\n  const years = Math.floor(days / 365);\n  \n  if (years > 0) {\n    return years === 1 ? '1 year ago' : `${years} years ago`;\n  }\n  if (months > 0) {\n    return months === 1 ? '1 month ago' : `${months} months ago`;\n  }\n  if (weeks > 0) {\n    return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;\n  }\n  if (days > 0) {\n    return days === 1 ? '1 day ago' : `${days} days ago`;\n  }\n  if (hours > 0) {\n    return hours === 1 ? '1 hour ago' : `${hours} hours ago`;\n  }\n  if (minutes > 0) {\n    return minutes === 1 ? '1 minute ago' : `${minutes} minutes ago`;\n  }\n  if (seconds > 0) {\n    return seconds === 1 ? '1 second ago' : `${seconds} seconds ago`;\n  }\n  \n  return 'just now';\n} \n"
  },
  {
    "path": "src/lib/hooksManager.ts",
    "content": "/**\n * Hooks configuration manager for Claude Code hooks\n */\n\nimport {\n  HooksConfiguration,\n  HookMatcher,\n  HookValidationResult,\n  HookValidationError,\n  HookValidationWarning,\n  HookCommand,\n} from '@/types/hooks';\n\nexport class HooksManager {\n  /**\n   * Merge hooks configurations with proper priority\n   * Priority: local > project > user\n   */\n  static mergeConfigs(\n    user: HooksConfiguration,\n    project: HooksConfiguration,\n    local: HooksConfiguration\n  ): HooksConfiguration {\n    const merged: HooksConfiguration = {};\n    \n    // Events with matchers (tool-related)\n    const matcherEvents: (keyof HooksConfiguration)[] = ['PreToolUse', 'PostToolUse'];\n    \n    // Events without matchers (non-tool-related)\n    const directEvents: (keyof HooksConfiguration)[] = ['Notification', 'Stop', 'SubagentStop'];\n\n    // Merge events with matchers\n    for (const event of matcherEvents) {\n      // Start with user hooks\n      let matchers = [...((user[event] as HookMatcher[] | undefined) || [])];\n      \n      // Add project hooks (may override by matcher pattern)\n      if (project[event]) {\n        matchers = this.mergeMatchers(matchers, project[event] as HookMatcher[]);\n      }\n      \n      // Add local hooks (highest priority)\n      if (local[event]) {\n        matchers = this.mergeMatchers(matchers, local[event] as HookMatcher[]);\n      }\n      \n      if (matchers.length > 0) {\n        (merged as any)[event] = matchers;\n      }\n    }\n    \n    // Merge events without matchers\n    for (const event of directEvents) {\n      // Combine all hooks from all levels (local takes precedence)\n      const hooks: HookCommand[] = [];\n      \n      // Add user hooks\n      if (user[event]) {\n        hooks.push(...(user[event] as HookCommand[]));\n      }\n      \n      // Add project hooks\n      if (project[event]) {\n        hooks.push(...(project[event] as HookCommand[]));\n      }\n      \n      // Add local hooks (highest priority)\n      if (local[event]) {\n        hooks.push(...(local[event] as HookCommand[]));\n      }\n      \n      if (hooks.length > 0) {\n        (merged as any)[event] = hooks;\n      }\n    }\n    \n    return merged;\n  }\n\n  /**\n   * Merge matcher arrays, with later items taking precedence\n   */\n  private static mergeMatchers(\n    base: HookMatcher[],\n    override: HookMatcher[]\n  ): HookMatcher[] {\n    const result = [...base];\n    \n    for (const overrideMatcher of override) {\n      const existingIndex = result.findIndex(\n        m => m.matcher === overrideMatcher.matcher\n      );\n      \n      if (existingIndex >= 0) {\n        // Replace existing matcher\n        result[existingIndex] = overrideMatcher;\n      } else {\n        // Add new matcher\n        result.push(overrideMatcher);\n      }\n    }\n    \n    return result;\n  }\n\n  /**\n   * Validate hooks configuration\n   */\n  static async validateConfig(hooks: HooksConfiguration): Promise<HookValidationResult> {\n    const errors: HookValidationError[] = [];\n    const warnings: HookValidationWarning[] = [];\n\n    // Guard against undefined or null hooks\n    if (!hooks) {\n      return { valid: true, errors, warnings };\n    }\n\n    // Events with matchers\n    const matcherEvents = ['PreToolUse', 'PostToolUse'] as const;\n    \n    // Events without matchers\n    const directEvents = ['Notification', 'Stop', 'SubagentStop'] as const;\n\n    // Validate events with matchers\n    for (const event of matcherEvents) {\n      const matchers = hooks[event];\n      if (!matchers || !Array.isArray(matchers)) continue;\n\n      for (const matcher of matchers) {\n        // Validate regex pattern if provided\n        if (matcher.matcher) {\n          try {\n            new RegExp(matcher.matcher);\n          } catch (e) {\n            errors.push({\n              event,\n              matcher: matcher.matcher,\n              message: `Invalid regex pattern: ${e instanceof Error ? e.message : 'Unknown error'}`\n            });\n          }\n        }\n\n        // Validate commands\n        if (matcher.hooks && Array.isArray(matcher.hooks)) {\n          for (const hook of matcher.hooks) {\n            if (!hook.command || !hook.command.trim()) {\n              errors.push({\n                event,\n                matcher: matcher.matcher,\n                message: 'Empty command'\n              });\n            }\n\n            // Check for dangerous patterns\n            const dangers = this.checkDangerousPatterns(hook.command || '');\n            warnings.push(...dangers.map(d => ({\n              event,\n              matcher: matcher.matcher,\n              command: hook.command || '',\n              message: d\n            })));\n          }\n        }\n      }\n    }\n\n    // Validate events without matchers\n    for (const event of directEvents) {\n      const directHooks = hooks[event];\n      if (!directHooks || !Array.isArray(directHooks)) continue;\n\n      for (const hook of directHooks) {\n        if (!hook.command || !hook.command.trim()) {\n          errors.push({\n            event,\n            message: 'Empty command'\n          });\n        }\n\n        // Check for dangerous patterns\n        const dangers = this.checkDangerousPatterns(hook.command || '');\n        warnings.push(...dangers.map(d => ({\n          event,\n          command: hook.command || '',\n          message: d\n        })));\n      }\n    }\n\n    return { valid: errors.length === 0, errors, warnings };\n  }\n\n  /**\n   * Check for potentially dangerous command patterns\n   */\n  public static checkDangerousPatterns(command: string): string[] {\n    const warnings: string[] = [];\n    \n    // Guard against undefined or null commands\n    if (!command || typeof command !== 'string') {\n      return warnings;\n    }\n    \n    const patterns = [\n      { pattern: /rm\\s+-rf\\s+\\/(?:\\s|$)/, message: 'Destructive command on root directory' },\n      { pattern: /rm\\s+-rf\\s+~/, message: 'Destructive command on home directory' },\n      { pattern: /:\\s*\\(\\s*\\)\\s*\\{.*\\}\\s*;/, message: 'Fork bomb pattern detected' },\n      { pattern: /curl.*\\|\\s*(?:bash|sh)/, message: 'Downloading and executing remote code' },\n      { pattern: /wget.*\\|\\s*(?:bash|sh)/, message: 'Downloading and executing remote code' },\n      { pattern: />\\/dev\\/sda/, message: 'Direct disk write operation' },\n      { pattern: /sudo\\s+/, message: 'Elevated privileges required' },\n      { pattern: /dd\\s+.*of=\\/dev\\//, message: 'Dangerous disk operation' },\n      { pattern: /mkfs\\./, message: 'Filesystem formatting command' },\n      { pattern: /:(){ :|:& };:/, message: 'Fork bomb detected' },\n    ];\n\n    for (const { pattern, message } of patterns) {\n      if (pattern.test(command)) {\n        warnings.push(message);\n      }\n    }\n\n    // Check for unescaped variables that could lead to code injection\n    if (command.includes('$') && !command.includes('\"$')) {\n      warnings.push('Unquoted shell variable detected - potential code injection risk');\n    }\n\n    return warnings;\n  }\n\n  /**\n   * Escape a command for safe shell execution\n   */\n  static escapeCommand(command: string): string {\n    // Basic shell escaping - in production, use a proper shell escaping library\n    return command\n      .replace(/\\\\/g, '\\\\\\\\')\n      .replace(/\"/g, '\\\\\"')\n      .replace(/\\$/g, '\\\\$')\n      .replace(/`/g, '\\\\`');\n  }\n\n  /**\n   * Generate a unique ID for hooks/matchers/commands\n   */\n  static generateId(): string {\n    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n  }\n} \n"
  },
  {
    "path": "src/lib/linkDetector.tsx",
    "content": "/**\n * URL Detection utility for terminal output\n * Detects various URL formats including localhost addresses\n */\n\nimport React from 'react';\n\n// URL regex pattern that matches:\n// - http:// and https:// URLs\n// - localhost URLs with ports\n// - IP addresses with ports\n// - URLs with paths and query parameters\nconst URL_REGEX = /(?:https?:\\/\\/)?(?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[[0-9a-fA-F:]+\\]|(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,})(?::[0-9]+)?(?:\\/[^\\s]*)?/gi;\n\n// More specific localhost pattern for better accuracy\nconst LOCALHOST_REGEX = /(?:https?:\\/\\/)?(?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\])(?::[0-9]+)?(?:\\/[^\\s]*)?/gi;\n\nexport interface DetectedLink {\n  url: string;\n  fullUrl: string; // URL with protocol\n  isLocalhost: boolean;\n  startIndex: number;\n  endIndex: number;\n}\n\n/**\n * Detects URLs in the given text\n * @param text - The text to search for URLs\n * @returns Array of detected links\n */\nexport function detectLinks(text: string): DetectedLink[] {\n  const links: DetectedLink[] = [];\n  const seenUrls = new Set<string>();\n  \n  // Reset regex lastIndex\n  URL_REGEX.lastIndex = 0;\n  \n  let match;\n  while ((match = URL_REGEX.exec(text)) !== null) {\n    const url = match[0];\n    \n    // Skip if we've already seen this URL\n    if (seenUrls.has(url)) continue;\n    seenUrls.add(url);\n    \n    // Ensure the URL has a protocol\n    let fullUrl = url;\n    if (!url.match(/^https?:\\/\\//)) {\n      // Default to http for localhost, https for others\n      const isLocalhost = LOCALHOST_REGEX.test(url);\n      fullUrl = `${isLocalhost ? 'http' : 'https'}://${url}`;\n    }\n    \n    // Validate the URL\n    try {\n      new URL(fullUrl);\n    } catch {\n      // Invalid URL, skip\n      continue;\n    }\n    \n    links.push({\n      url,\n      fullUrl,\n      isLocalhost: LOCALHOST_REGEX.test(url),\n      startIndex: match.index,\n      endIndex: match.index + url.length\n    });\n  }\n  \n  return links;\n}\n\n/**\n * Checks if a text contains any URLs\n * @param text - The text to check\n * @returns True if URLs are found\n */\nexport function hasLinks(text: string): boolean {\n  URL_REGEX.lastIndex = 0;\n  return URL_REGEX.test(text);\n}\n\n/**\n * Extracts the first URL from text\n * @param text - The text to search\n * @returns The first detected link or null\n */\nexport function getFirstLink(text: string): DetectedLink | null {\n  const links = detectLinks(text);\n  return links.length > 0 ? links[0] : null;\n}\n\n/**\n * Makes URLs in text clickable by wrapping them in a callback\n * @param text - The text containing URLs\n * @param onLinkClick - Callback when a link is clicked\n * @returns React elements with clickable links\n */\nexport function makeLinksClickable(\n  text: string,\n  onLinkClick: (url: string) => void\n): React.ReactNode[] {\n  const links = detectLinks(text);\n  \n  if (links.length === 0) {\n    return [text];\n  }\n  \n  const elements: React.ReactNode[] = [];\n  let lastIndex = 0;\n  \n  links.forEach((link, index) => {\n    // Add text before the link\n    if (link.startIndex > lastIndex) {\n      elements.push(text.substring(lastIndex, link.startIndex));\n    }\n    \n    // Add the clickable link\n    elements.push(\n      <a\n        key={`link-${index}`}\n        href={link.fullUrl}\n        onClick={(e) => {\n          e.preventDefault();\n          onLinkClick(link.fullUrl);\n        }}\n        className=\"text-primary underline hover:text-primary/80 cursor-pointer\"\n        title={link.fullUrl}\n      >\n        {link.url}\n      </a>\n    );\n    \n    lastIndex = link.endIndex;\n  });\n  \n  // Add remaining text\n  if (lastIndex < text.length) {\n    elements.push(text.substring(lastIndex));\n  }\n  \n  return elements;\n} "
  },
  {
    "path": "src/lib/outputCache.tsx",
    "content": "import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';\nimport { api } from './api';\n\n// Use the same message interface as AgentExecution for consistency\nexport interface ClaudeStreamMessage {\n  type: \"system\" | \"assistant\" | \"user\" | \"result\";\n  subtype?: string;\n  message?: {\n    content?: any[];\n    usage?: {\n      input_tokens: number;\n      output_tokens: number;\n    };\n  };\n  usage?: {\n    input_tokens: number;\n    output_tokens: number;\n  };\n  [key: string]: any;\n}\n\ninterface CachedSessionOutput {\n  output: string;\n  messages: ClaudeStreamMessage[];\n  lastUpdated: number;\n  status: string;\n}\n\ninterface OutputCacheContextType {\n  getCachedOutput: (sessionId: number) => CachedSessionOutput | null;\n  setCachedOutput: (sessionId: number, data: CachedSessionOutput) => void;\n  updateSessionStatus: (sessionId: number, status: string) => void;\n  clearCache: (sessionId?: number) => void;\n  isPolling: boolean;\n  startBackgroundPolling: () => void;\n  stopBackgroundPolling: () => void;\n}\n\nconst OutputCacheContext = createContext<OutputCacheContextType | null>(null);\n\nexport function useOutputCache() {\n  const context = useContext(OutputCacheContext);\n  if (!context) {\n    throw new Error('useOutputCache must be used within an OutputCacheProvider');\n  }\n  return context;\n}\n\ninterface OutputCacheProviderProps {\n  children: React.ReactNode;\n}\n\nexport function OutputCacheProvider({ children }: OutputCacheProviderProps) {\n  const [cache, setCache] = useState<Map<number, CachedSessionOutput>>(new Map());\n  const [isPolling, setIsPolling] = useState(false);\n  const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(null);\n\n  const getCachedOutput = useCallback((sessionId: number): CachedSessionOutput | null => {\n    return cache.get(sessionId) || null;\n  }, [cache]);\n\n  const setCachedOutput = useCallback((sessionId: number, data: CachedSessionOutput) => {\n    setCache(prev => new Map(prev.set(sessionId, data)));\n  }, []);\n\n  const updateSessionStatus = useCallback((sessionId: number, status: string) => {\n    setCache(prev => {\n      const existing = prev.get(sessionId);\n      if (existing) {\n        const updated = new Map(prev);\n        updated.set(sessionId, { ...existing, status });\n        return updated;\n      }\n      return prev;\n    });\n  }, []);\n\n  const clearCache = useCallback((sessionId?: number) => {\n    if (sessionId) {\n      setCache(prev => {\n        const updated = new Map(prev);\n        updated.delete(sessionId);\n        return updated;\n      });\n    } else {\n      setCache(new Map());\n    }\n  }, []);\n\n  const parseOutput = useCallback((rawOutput: string): ClaudeStreamMessage[] => {\n    if (!rawOutput) return [];\n\n    const lines = rawOutput.split('\\n').filter(line => line.trim());\n    const parsedMessages: ClaudeStreamMessage[] = [];\n\n    for (const line of lines) {\n      try {\n        const message = JSON.parse(line) as ClaudeStreamMessage;\n        parsedMessages.push(message);\n      } catch (err) {\n        console.error(\"Failed to parse message:\", err, line);\n        // Add a fallback message for unparseable content\n        parsedMessages.push({\n          type: 'result',\n          subtype: 'error',\n          error: 'Failed to parse message',\n          raw_content: line\n        });\n      }\n    }\n\n    return parsedMessages;\n  }, []);\n\n  const updateSessionCache = useCallback(async (sessionId: number, status: string) => {\n    try {\n      const rawOutput = await api.getSessionOutput(sessionId);\n      const messages = parseOutput(rawOutput);\n      \n      setCachedOutput(sessionId, {\n        output: rawOutput,\n        messages,\n        lastUpdated: Date.now(),\n        status\n      });\n    } catch (error) {\n      console.warn(`Failed to update cache for session ${sessionId}:`, error);\n    }\n  }, [parseOutput, setCachedOutput]);\n\n  const pollRunningSessions = useCallback(async () => {\n    try {\n      const runningSessions = await api.listRunningAgentSessions();\n      \n      // Update cache for all running sessions\n      for (const session of runningSessions) {\n        if (session.id && session.status === 'running') {\n          await updateSessionCache(session.id, session.status);\n        }\n      }\n\n      // Clean up cache for sessions that are no longer running\n      const runningIds = new Set(runningSessions.map(s => s.id).filter(Boolean));\n      setCache(prev => {\n        const updated = new Map();\n        for (const [sessionId, data] of prev) {\n          if (runningIds.has(sessionId) || data.status !== 'running') {\n            updated.set(sessionId, data);\n          }\n        }\n        return updated;\n      });\n    } catch (error) {\n      console.warn('Failed to poll running sessions:', error);\n    }\n  }, [updateSessionCache]);\n\n  const startBackgroundPolling = useCallback(() => {\n    if (pollingInterval) return;\n\n    setIsPolling(true);\n    const interval = setInterval(pollRunningSessions, 3000); // Poll every 3 seconds\n    setPollingInterval(interval);\n  }, [pollingInterval, pollRunningSessions]);\n\n  const stopBackgroundPolling = useCallback(() => {\n    if (pollingInterval) {\n      clearInterval(pollingInterval);\n      setPollingInterval(null);\n    }\n    setIsPolling(false);\n  }, [pollingInterval]);\n\n  // Auto-start polling when provider mounts\n  useEffect(() => {\n    startBackgroundPolling();\n    return () => stopBackgroundPolling();\n  }, [startBackgroundPolling, stopBackgroundPolling]);\n\n  const value: OutputCacheContextType = {\n    getCachedOutput,\n    setCachedOutput,\n    updateSessionStatus,\n    clearCache,\n    isPolling,\n    startBackgroundPolling,\n    stopBackgroundPolling,\n  };\n\n  return (\n    <OutputCacheContext.Provider value={value}>\n      {children}\n    </OutputCacheContext.Provider>\n  );\n}"
  },
  {
    "path": "src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Combines multiple class values into a single string using clsx and tailwind-merge.\n * This utility function helps manage dynamic class names and prevents Tailwind CSS conflicts.\n * \n * @param inputs - Array of class values that can be strings, objects, arrays, etc.\n * @returns A merged string of class names with Tailwind conflicts resolved\n * \n * @example\n * cn(\"px-2 py-1\", condition && \"bg-blue-500\", { \"text-white\": isActive })\n * // Returns: \"px-2 py-1 bg-blue-500 text-white\" (when condition and isActive are true)\n */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n} "
  },
  {
    "path": "src/main.tsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"./App\";\nimport { ErrorBoundary } from \"./components/ErrorBoundary\";\nimport { AnalyticsErrorBoundary } from \"./components/AnalyticsErrorBoundary\";\nimport { analytics, resourceMonitor } from \"./lib/analytics\";\nimport { PostHogProvider } from \"posthog-js/react\";\nimport \"./assets/shimmer.css\";\nimport \"./styles.css\";\nimport AppIcon from \"./assets/nfo/asterisk-logo.png\";\n\n// Initialize analytics before rendering\nanalytics.initialize();\n\n// Start resource monitoring (check every 2 minutes)\nresourceMonitor.startMonitoring(120000);\n\n// Add a macOS-specific class to the <html> element to enable platform-specific styling\n// Browser-safe detection using navigator properties (works in Tauri and web preview)\n(() => {\n  const isMacLike = typeof navigator !== \"undefined\" &&\n    (navigator.platform?.toLowerCase().includes(\"mac\") ||\n      navigator.userAgent?.toLowerCase().includes(\"mac os x\"));\n  if (isMacLike) {\n    document.documentElement.classList.add(\"is-macos\");\n  }\n})();\n\n// Set favicon to the new app icon (avoids needing /public)\n(() => {\n  try {\n    const existing = document.querySelector<HTMLLinkElement>('link[rel=\"icon\"]');\n    const link = existing ?? document.createElement(\"link\");\n    link.rel = \"icon\";\n    link.type = \"image/png\";\n    link.href = AppIcon;\n    if (!existing) {\n      document.head.appendChild(link);\n    }\n  } catch (_) {\n    // Non-fatal if document/head is not available\n  }\n})();\n\nReactDOM.createRoot(document.getElementById(\"root\") as HTMLElement).render(\n  <React.StrictMode>\n    <PostHogProvider\n      apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}\n      options={{\n        api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,\n        defaults: '2025-05-24',\n        capture_exceptions: true,\n        debug: import.meta.env.MODE === \"development\",\n      }}\n    >\n      <ErrorBoundary>\n        <AnalyticsErrorBoundary>\n          <App />\n        </AnalyticsErrorBoundary>\n      </ErrorBoundary>\n    </PostHogProvider>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "src/services/sessionPersistence.ts",
    "content": "/**\n * Session Persistence Service\n * Handles saving and restoring session data for chat tabs\n */\n\nimport { api, type Session } from '@/lib/api';\n\nconst STORAGE_KEY_PREFIX = 'opcode_session_';\nconst SESSION_INDEX_KEY = 'opcode_session_index';\n\nexport interface SessionRestoreData {\n  sessionId: string;\n  projectId: string;\n  projectPath: string;\n  lastMessageCount?: number;\n  scrollPosition?: number;\n  timestamp: number;\n}\n\nexport class SessionPersistenceService {\n  /**\n   * Save session data for later restoration\n   */\n  static saveSession(sessionId: string, projectId: string, projectPath: string, messageCount?: number, scrollPosition?: number): void {\n    try {\n      const sessionData: SessionRestoreData = {\n        sessionId,\n        projectId,\n        projectPath,\n        lastMessageCount: messageCount,\n        scrollPosition,\n        timestamp: Date.now()\n      };\n\n      // Save individual session data\n      localStorage.setItem(`${STORAGE_KEY_PREFIX}${sessionId}`, JSON.stringify(sessionData));\n\n      // Update session index\n      const index = this.getSessionIndex();\n      if (!index.includes(sessionId)) {\n        index.push(sessionId);\n        localStorage.setItem(SESSION_INDEX_KEY, JSON.stringify(index));\n      }\n    } catch (error) {\n      console.error('Failed to save session data:', error);\n    }\n  }\n\n  /**\n   * Load session data for restoration\n   */\n  static loadSession(sessionId: string): SessionRestoreData | null {\n    try {\n      const data = localStorage.getItem(`${STORAGE_KEY_PREFIX}${sessionId}`);\n      if (!data) return null;\n\n      const sessionData = JSON.parse(data) as SessionRestoreData;\n      \n      // Validate the data\n      if (!sessionData.sessionId || !sessionData.projectId || !sessionData.projectPath) {\n        return null;\n      }\n\n      return sessionData;\n    } catch (error) {\n      console.error('Failed to load session data:', error);\n      return null;\n    }\n  }\n\n  /**\n   * Remove session data from storage\n   */\n  static removeSession(sessionId: string): void {\n    try {\n      // Remove session data\n      localStorage.removeItem(`${STORAGE_KEY_PREFIX}${sessionId}`);\n\n      // Update session index\n      const index = this.getSessionIndex();\n      const newIndex = index.filter(id => id !== sessionId);\n      localStorage.setItem(SESSION_INDEX_KEY, JSON.stringify(newIndex));\n    } catch (error) {\n      console.error('Failed to remove session data:', error);\n    }\n  }\n\n  /**\n   * Get all saved session IDs\n   */\n  static getSessionIndex(): string[] {\n    try {\n      const index = localStorage.getItem(SESSION_INDEX_KEY);\n      return index ? JSON.parse(index) : [];\n    } catch (error) {\n      console.error('Failed to get session index:', error);\n      return [];\n    }\n  }\n\n  /**\n   * Clear all session data\n   */\n  static clearAllSessions(): void {\n    try {\n      const index = this.getSessionIndex();\n      \n      // Remove all session data\n      index.forEach(sessionId => {\n        localStorage.removeItem(`${STORAGE_KEY_PREFIX}${sessionId}`);\n      });\n\n      // Clear the index\n      localStorage.removeItem(SESSION_INDEX_KEY);\n    } catch (error) {\n      console.error('Failed to clear session data:', error);\n    }\n  }\n\n  /**\n   * Clean up old session data (older than 30 days)\n   */\n  static cleanupOldSessions(): void {\n    try {\n      const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);\n      const index = this.getSessionIndex();\n      const activeIndex: string[] = [];\n\n      index.forEach(sessionId => {\n        const data = this.loadSession(sessionId);\n        if (data && data.timestamp > thirtyDaysAgo) {\n          activeIndex.push(sessionId);\n        } else {\n          localStorage.removeItem(`${STORAGE_KEY_PREFIX}${sessionId}`);\n        }\n      });\n\n      localStorage.setItem(SESSION_INDEX_KEY, JSON.stringify(activeIndex));\n    } catch (error) {\n      console.error('Failed to cleanup old sessions:', error);\n    }\n  }\n\n  /**\n   * Check if session exists on disk and is restorable\n   */\n  static async isSessionRestorable(sessionId: string, projectId: string): Promise<boolean> {\n    try {\n      // First check if we have the session metadata\n      const sessionData = this.loadSession(sessionId);\n      if (!sessionData) return false;\n\n      // Try to verify the session exists on disk by loading its history\n      const history = await api.loadSessionHistory(sessionId, projectId);\n      return history && history.length > 0;\n    } catch (error) {\n      console.error('Failed to check session restorability:', error);\n      return false;\n    }\n  }\n\n  /**\n   * Create Session object from restore data\n   */\n  static createSessionFromRestoreData(data: SessionRestoreData): Session {\n    return {\n      id: data.sessionId,\n      project_id: data.projectId,\n      project_path: data.projectPath,\n      created_at: data.timestamp / 1000, // Convert to seconds\n      first_message: \"Restored session\"\n    };\n  }\n}\n"
  },
  {
    "path": "src/services/tabPersistence.ts",
    "content": "/**\n * Tab Persistence Service\n * Handles saving and restoring tab state to/from localStorage\n */\n\nimport type { Tab } from '@/contexts/TabContext';\n\nconst STORAGE_KEY = 'opcode_tabs_v2';\nconst ACTIVE_TAB_KEY = 'opcode_active_tab_v2';\nconst PERSISTENCE_ENABLED_KEY = 'opcode_tab_persistence_enabled';\n\ninterface SerializedTab {\n  id: string;\n  type: Tab['type'];\n  title: string;\n  sessionId?: string;\n  agentRunId?: string;\n  claudeFileId?: string;\n  initialProjectPath?: string;\n  projectPath?: string;\n  status: Tab['status'];\n  hasUnsavedChanges: boolean;\n  order: number;\n  icon?: string;\n  createdAt: string;\n  updatedAt: string;\n  // Note: We don't persist sessionData or agentData as they're complex objects\n}\n\nexport class TabPersistenceService {\n  /**\n   * Check if tab persistence is enabled\n   */\n  static isEnabled(): boolean {\n    const enabled = localStorage.getItem(PERSISTENCE_ENABLED_KEY);\n    // Default to true if not set\n    return enabled === null || enabled === 'true';\n  }\n\n  /**\n   * Enable or disable tab persistence\n   */\n  static setEnabled(enabled: boolean): void {\n    localStorage.setItem(PERSISTENCE_ENABLED_KEY, String(enabled));\n    if (!enabled) {\n      // Clear saved tabs when disabling persistence\n      this.clearTabs();\n    }\n  }\n  /**\n   * Save tabs to localStorage\n   */\n  static saveTabs(tabs: Tab[], activeTabId: string | null): void {\n    // Don't save if persistence is disabled\n    if (!this.isEnabled()) return;\n    \n    try {\n      // Filter out tabs that shouldn't be persisted\n      const persistableTabs = tabs.filter(tab => {\n        // Don't persist tabs with running status (they're likely stale)\n        if (tab.status === 'running') return false;\n        \n        // Don't persist create/import agent tabs (they're temporary)\n        if (tab.type === 'create-agent' || tab.type === 'import-agent') return false;\n        \n        return true;\n      });\n\n      // Serialize tabs (excluding complex objects)\n      const serializedTabs: SerializedTab[] = persistableTabs.map(tab => ({\n        id: tab.id,\n        type: tab.type,\n        title: tab.title,\n        sessionId: tab.sessionId,\n        agentRunId: tab.agentRunId,\n        claudeFileId: tab.claudeFileId,\n        initialProjectPath: tab.initialProjectPath,\n        projectPath: tab.projectPath,\n        status: tab.status === 'running' ? 'idle' : tab.status, // Reset running status\n        hasUnsavedChanges: false, // Reset unsaved changes\n        order: tab.order,\n        icon: tab.icon,\n        createdAt: tab.createdAt instanceof Date ? tab.createdAt.toISOString() : tab.createdAt,\n        updatedAt: tab.updatedAt instanceof Date ? tab.updatedAt.toISOString() : tab.updatedAt\n      }));\n\n      localStorage.setItem(STORAGE_KEY, JSON.stringify(serializedTabs));\n      \n      // Save active tab ID\n      if (activeTabId && persistableTabs.some(tab => tab.id === activeTabId)) {\n        localStorage.setItem(ACTIVE_TAB_KEY, activeTabId);\n      }\n    } catch (error) {\n      console.error('Failed to save tabs:', error);\n    }\n  }\n\n  /**\n   * Load tabs from localStorage\n   */\n  static loadTabs(): { tabs: Tab[], activeTabId: string | null } {\n    // Don't load if persistence is disabled\n    if (!this.isEnabled()) {\n      return { tabs: [], activeTabId: null };\n    }\n    \n    try {\n      const savedTabsJson = localStorage.getItem(STORAGE_KEY);\n      const savedActiveTabId = localStorage.getItem(ACTIVE_TAB_KEY);\n      \n      if (!savedTabsJson) {\n        return { tabs: [], activeTabId: null };\n      }\n\n      const serializedTabs: SerializedTab[] = JSON.parse(savedTabsJson);\n      \n      // Deserialize tabs\n      const tabs: Tab[] = serializedTabs.map(serialized => ({\n        ...serialized,\n        createdAt: new Date(serialized.createdAt),\n        updatedAt: new Date(serialized.updatedAt),\n        sessionData: undefined, // Will be loaded when tab is activated\n        agentData: undefined, // Will be loaded when tab is activated\n        status: serialized.status === 'running' ? 'idle' : serialized.status // Ensure no running status\n      }));\n\n      // Validate and filter out any invalid tabs\n      const validTabs = tabs.filter(tab => {\n        // Basic validation\n        if (!tab.id || !tab.type || !tab.title) return false;\n        \n        // Type-specific validation\n        switch (tab.type) {\n          case 'chat':\n            // Chat tabs without sessionId or projectPath might be invalid\n            // But we'll keep them as they might be new sessions\n            return true;\n          case 'agent':\n            // Agent tabs need an agentRunId\n            return !!tab.agentRunId;\n          case 'agent-execution':\n            // Agent execution tabs without agentData are invalid\n            // We'll filter these out as they can't be restored properly\n            return false;\n          case 'claude-file':\n            // Claude file tabs need a file ID\n            return !!tab.claudeFileId;\n          default:\n            // Other tab types (projects, agents, usage, etc.) are always valid\n            return true;\n        }\n      });\n\n      // Ensure proper ordering\n      const orderedTabs = validTabs\n        .sort((a, b) => a.order - b.order)\n        .map((tab, index) => ({ ...tab, order: index }));\n\n      // Validate active tab ID\n      const activeTabId = savedActiveTabId && orderedTabs.some(tab => tab.id === savedActiveTabId)\n        ? savedActiveTabId\n        : orderedTabs.length > 0 ? orderedTabs[0].id : null;\n\n      return { tabs: orderedTabs, activeTabId };\n    } catch (error) {\n      console.error('Failed to load tabs:', error);\n      // Clear corrupted data\n      localStorage.removeItem(STORAGE_KEY);\n      localStorage.removeItem(ACTIVE_TAB_KEY);\n      return { tabs: [], activeTabId: null };\n    }\n  }\n\n  /**\n   * Clear saved tabs\n   */\n  static clearTabs(): void {\n    localStorage.removeItem(STORAGE_KEY);\n    localStorage.removeItem(ACTIVE_TAB_KEY);\n  }\n\n  /**\n   * Migrate from old storage format if needed\n   */\n  static migrateFromOldFormat(): void {\n    try {\n      const oldKey = 'opcode_tabs';\n      const oldData = localStorage.getItem(oldKey);\n      \n      if (oldData && !localStorage.getItem(STORAGE_KEY)) {\n        // Attempt to migrate old data\n        localStorage.setItem(STORAGE_KEY, oldData);\n        localStorage.removeItem(oldKey);\n        console.log('Migrated tab data from old format');\n      }\n    } catch (error) {\n      console.error('Failed to migrate old tab data:', error);\n    }\n  }\n}\n"
  },
  {
    "path": "src/stores/README.md",
    "content": "# Store Implementation Notes\n\nThe store files (`sessionStore.ts` and `agentStore.ts`) provide examples of how to implement global state management with Zustand for the opcode application.\n\n## Key Benefits:\n- Eliminates prop drilling across components\n- Centralizes state management\n- Provides optimized selectors for performance\n- Handles real-time updates efficiently\n\n## Implementation Status:\nThese stores are example implementations that would need to be adapted to match the actual API interface. The current API in `lib/api.ts` has different method names and signatures than what was assumed in the store implementations.\n\n## To Complete Implementation:\n1. Update the store methods to match actual API methods\n2. Add proper TypeScript types from the API\n3. Implement WebSocket/SSE for real-time updates\n4. Connect stores to components using the custom selectors\n\n## Example Usage:\n```typescript\nimport { useSessionStore } from '@/stores/sessionStore';\n\nfunction MyComponent() {\n  const { sessions, fetchSessions } = useSessionStore();\n  \n  useEffect(() => {\n    fetchSessions();\n  }, []);\n  \n  return <div>{sessions.length} sessions</div>;\n}\n```\n"
  },
  {
    "path": "src/stores/agentStore.ts",
    "content": "import { create } from 'zustand';\nimport { subscribeWithSelector } from 'zustand/middleware';\nimport type { StateCreator } from 'zustand';\nimport { api } from '@/lib/api';\nimport type { AgentRunWithMetrics } from '@/lib/api';\n\ninterface AgentState {\n  // Agent runs data\n  agentRuns: AgentRunWithMetrics[];\n  runningAgents: Set<string>;\n  sessionOutputs: Record<string, string>;\n  \n  // UI state\n  isLoadingRuns: boolean;\n  isLoadingOutput: boolean;\n  error: string | null;\n  lastFetchTime: number;\n  \n  // Actions\n  fetchAgentRuns: (forceRefresh?: boolean) => Promise<void>;\n  fetchSessionOutput: (runId: number) => Promise<void>;\n  createAgentRun: (data: { agentId: number; projectPath: string; task: string; model?: string }) => Promise<AgentRunWithMetrics>;\n  cancelAgentRun: (runId: number) => Promise<void>;\n  deleteAgentRun: (runId: number) => Promise<void>;\n  clearError: () => void;\n  \n  // Real-time updates\n  handleAgentRunUpdate: (run: AgentRunWithMetrics) => void;\n  \n  // Polling management\n  startPolling: (interval?: number) => void;\n  stopPolling: () => void;\n  pollingInterval: NodeJS.Timeout | null;\n}\n\nconst agentStore: StateCreator<\n  AgentState,\n  [],\n  [['zustand/subscribeWithSelector', never]],\n  AgentState\n> = (set, get) => ({\n    // Initial state\n    agentRuns: [],\n    runningAgents: new Set(),\n    sessionOutputs: {},\n    isLoadingRuns: false,\n    isLoadingOutput: false,\n    error: null,\n    lastFetchTime: 0,\n    pollingInterval: null,\n    \n    // Fetch agent runs with caching\n    fetchAgentRuns: async (forceRefresh = false) => {\n      const now = Date.now();\n      const { lastFetchTime } = get();\n      \n      // Cache for 5 seconds unless forced\n      if (!forceRefresh && now - lastFetchTime < 5000) {\n        return;\n      }\n      \n      set({ isLoadingRuns: true, error: null });\n      \n      try {\n        const runs = await api.listAgentRuns();\n        const runningIds = runs\n          .filter((r) => r.status === 'running' || r.status === 'pending')\n          .map((r) => r.id?.toString() || '')\n          .filter(Boolean);\n        \n        set({\n          agentRuns: runs,\n          runningAgents: new Set(runningIds),\n          isLoadingRuns: false,\n          lastFetchTime: now\n        });\n      } catch (error) {\n        set({\n          error: error instanceof Error ? error.message : 'Failed to fetch agent runs',\n          isLoadingRuns: false\n        });\n      }\n    },\n    \n    // Fetch session output for a specific run\n    fetchSessionOutput: async (runId: number) => {\n      set({ isLoadingOutput: true, error: null });\n      \n      try {\n        const output = await api.getAgentRunWithRealTimeMetrics(runId).then(run => run.output || '');\n        set((state) => ({\n          sessionOutputs: {\n            ...state.sessionOutputs,\n            [runId]: output\n          },\n          isLoadingOutput: false\n        }));\n      } catch (error) {\n        set({\n          error: error instanceof Error ? error.message : 'Failed to fetch session output',\n          isLoadingOutput: false\n        });\n      }\n    },\n    \n    // Create a new agent run\n    createAgentRun: async (data: { agentId: number; projectPath: string; task: string; model?: string }) => {\n      try {\n        const runId = await api.executeAgent(data.agentId, data.projectPath, data.task, data.model);\n        \n        // Fetch the created run details\n        const run = await api.getAgentRun(runId);\n        \n        // Update local state immediately\n        set((state) => ({\n          agentRuns: [run, ...state.agentRuns],\n          runningAgents: new Set([...state.runningAgents, runId.toString()])\n        }));\n        \n        return run;\n      } catch (error) {\n        set({\n          error: error instanceof Error ? error.message : 'Failed to create agent run'\n        });\n        throw error;\n      }\n    },\n    \n    // Cancel an agent run\n    cancelAgentRun: async (runId: number) => {\n      try {\n        await api.killAgentSession(runId);\n        \n        // Update local state\n        set((state) => ({\n          agentRuns: state.agentRuns.map((r) =>\n            r.id === runId ? { ...r, status: 'cancelled' } : r\n          ),\n          runningAgents: new Set(\n            [...state.runningAgents].filter(id => id !== runId.toString())\n          )\n        }));\n      } catch (error) {\n        set({\n          error: error instanceof Error ? error.message : 'Failed to cancel agent run'\n        });\n        throw error;\n      }\n    },\n    \n    // Delete an agent run\n    deleteAgentRun: async (runId: number) => {\n      try {\n        // First ensure the run is cancelled if it's still running\n        const run = get().agentRuns.find((r) => r.id === runId);\n        if (run && (run.status === 'running' || run.status === 'pending')) {\n          await api.killAgentSession(runId);\n        }\n        \n        // Note: There's no deleteAgentRun API method, so we just remove from local state\n        // The run will remain in the database but won't be shown in the UI\n        \n        // Update local state\n        set((state) => ({\n          agentRuns: state.agentRuns.filter((r) => r.id !== runId),\n          runningAgents: new Set(\n            [...state.runningAgents].filter(id => id !== runId.toString())\n          ),\n          sessionOutputs: Object.fromEntries(\n            Object.entries(state.sessionOutputs).filter(([id]) => id !== runId.toString())\n          )\n        }));\n      } catch (error) {\n        set({\n          error: error instanceof Error ? error.message : 'Failed to delete agent run'\n        });\n        throw error;\n      }\n    },\n    \n    // Clear error\n    clearError: () => set({ error: null }),\n    \n    // Handle real-time agent run updates\n    handleAgentRunUpdate: (run: AgentRunWithMetrics) => {\n      set((state) => {\n        const existingIndex = state.agentRuns.findIndex((r) => r.id === run.id);\n        const updatedRuns = [...state.agentRuns];\n        \n        if (existingIndex >= 0) {\n          updatedRuns[existingIndex] = run;\n        } else {\n          updatedRuns.unshift(run);\n        }\n        \n        const runningIds = updatedRuns\n          .filter((r) => r.status === 'running' || r.status === 'pending')\n          .map((r) => r.id?.toString() || '')\n          .filter(Boolean);\n        \n        return {\n          agentRuns: updatedRuns,\n          runningAgents: new Set(runningIds)\n        };\n      });\n    },\n    \n    // Start polling for updates\n    startPolling: (interval = 3000) => {\n      const { pollingInterval, stopPolling } = get();\n      \n      // Clear existing interval\n      if (pollingInterval) {\n        stopPolling();\n      }\n      \n      // Start new interval\n      const newInterval = setInterval(() => {\n        const { runningAgents } = get();\n        if (runningAgents.size > 0) {\n          get().fetchAgentRuns();\n        }\n      }, interval);\n      \n      set({ pollingInterval: newInterval });\n    },\n    \n    // Stop polling\n    stopPolling: () => {\n      const { pollingInterval } = get();\n      if (pollingInterval) {\n        clearInterval(pollingInterval);\n        set({ pollingInterval: null });\n      }\n    }\n  });\n\nexport const useAgentStore = create<AgentState>()(\n  subscribeWithSelector(agentStore)\n);"
  },
  {
    "path": "src/stores/sessionStore.ts",
    "content": "import { create } from 'zustand';\nimport { subscribeWithSelector } from 'zustand/middleware';\nimport type { StateCreator } from 'zustand';\nimport { api } from '@/lib/api';\nimport type { Session, Project } from '@/lib/api';\n\ninterface SessionState {\n  // Projects and sessions data\n  projects: Project[];\n  sessions: Record<string, Session[]>; // Keyed by projectId\n  currentSessionId: string | null;\n  currentSession: Session | null;\n  sessionOutputs: Record<string, string>; // Keyed by sessionId\n  \n  // UI state\n  isLoadingProjects: boolean;\n  isLoadingSessions: boolean;\n  isLoadingOutputs: boolean;\n  error: string | null;\n  \n  // Actions\n  fetchProjects: () => Promise<void>;\n  fetchProjectSessions: (projectId: string) => Promise<void>;\n  setCurrentSession: (sessionId: string | null) => void;\n  fetchSessionOutput: (sessionId: string) => Promise<void>;\n  deleteSession: (sessionId: string, projectId: string) => Promise<void>;\n  clearError: () => void;\n  \n  // Real-time updates\n  handleSessionUpdate: (session: Session) => void;\n  handleOutputUpdate: (sessionId: string, output: string) => void;\n}\n\nconst sessionStore: StateCreator<\n  SessionState,\n  [],\n  [['zustand/subscribeWithSelector', never]],\n  SessionState\n> = (set, get) => ({\n    // Initial state\n    projects: [],\n    sessions: {},\n    currentSessionId: null,\n    currentSession: null,\n    sessionOutputs: {},\n    isLoadingProjects: false,\n    isLoadingSessions: false,\n    isLoadingOutputs: false,\n    error: null,\n    \n    // Fetch all projects\n    fetchProjects: async () => {\n      set({ isLoadingProjects: true, error: null });\n      try {\n        const projects = await api.listProjects();\n        set({ projects, isLoadingProjects: false });\n      } catch (error) {\n        set({ \n          error: error instanceof Error ? error.message : 'Failed to fetch projects',\n          isLoadingProjects: false \n        });\n      }\n    },\n    \n    // Fetch sessions for a specific project\n    fetchProjectSessions: async (projectId: string) => {\n      set({ isLoadingSessions: true, error: null });\n      try {\n        const projectSessions = await api.getProjectSessions(projectId);\n        set((state) => ({\n          sessions: {\n            ...state.sessions,\n            [projectId]: projectSessions\n          },\n          isLoadingSessions: false\n        }));\n      } catch (error) {\n        set({ \n          error: error instanceof Error ? error.message : 'Failed to fetch sessions',\n          isLoadingSessions: false \n        });\n      }\n    },\n    \n    // Set current session\n    setCurrentSession: (sessionId: string | null) => {\n      const { sessions } = get();\n      let currentSession: Session | null = null;\n      \n      if (sessionId) {\n        // Find session across all projects\n        for (const projectSessions of Object.values(sessions)) {\n          const found = projectSessions.find((s) => s.id === sessionId);\n          if (found) {\n            currentSession = found;\n            break;\n          }\n        }\n      }\n      \n      set({ currentSessionId: sessionId, currentSession });\n    },\n    \n    // Fetch session output\n    fetchSessionOutput: async (sessionId: string) => {\n      set({ isLoadingOutputs: true, error: null });\n      try {\n        const output = await api.getClaudeSessionOutput(sessionId);\n        set((state) => ({\n          sessionOutputs: {\n            ...state.sessionOutputs,\n            [sessionId]: output\n          },\n          isLoadingOutputs: false\n        }));\n      } catch (error) {\n        set({ \n          error: error instanceof Error ? error.message : 'Failed to fetch session output',\n          isLoadingOutputs: false \n        });\n      }\n    },\n    \n    // Delete session\n    deleteSession: async (sessionId: string, projectId: string) => {\n      try {\n        // Note: API doesn't have a deleteSession method, so this is a placeholder\n        console.warn('deleteSession not implemented in API');\n        \n        // Update local state\n        set((state) => ({\n          sessions: {\n            ...state.sessions,\n            [projectId]: state.sessions[projectId]?.filter((s) => s.id !== sessionId) || []\n          },\n          currentSessionId: state.currentSessionId === sessionId ? null : state.currentSessionId,\n          currentSession: state.currentSession?.id === sessionId ? null : state.currentSession,\n          sessionOutputs: Object.fromEntries(\n            Object.entries(state.sessionOutputs).filter(([id]) => id !== sessionId)\n          )\n        }));\n      } catch (error) {\n        set({ \n          error: error instanceof Error ? error.message : 'Failed to delete session'\n        });\n        throw error;\n      }\n    },\n    \n    // Clear error\n    clearError: () => set({ error: null }),\n    \n    // Handle session update\n    handleSessionUpdate: (session: Session) => {\n      set(state => {\n        const projectId = session.project_id;\n        const projectSessions = state.sessions[projectId] || [];\n        const existingIndex = projectSessions.findIndex((s) => s.id === session.id);\n        \n        let updatedSessions;\n        if (existingIndex >= 0) {\n          updatedSessions = [...projectSessions];\n          updatedSessions[existingIndex] = session;\n        } else {\n          updatedSessions = [session, ...projectSessions];\n        }\n        \n        return {\n          sessions: {\n            ...state.sessions,\n            [projectId]: updatedSessions\n          },\n          currentSession: state.currentSessionId === session.id ? session : state.currentSession\n        };\n      });\n    },\n    \n    // Handle output update\n    handleOutputUpdate: (sessionId: string, output: string) => {\n      set((state) => ({\n        sessionOutputs: {\n          ...state.sessionOutputs,\n          [sessionId]: output\n        }\n      }));\n    }\n  });\n\nexport const useSessionStore = create<SessionState>()(\n  subscribeWithSelector(sessionStore)\n);"
  },
  {
    "path": "src/styles.css",
    "content": "@import \"tailwindcss\";\n\n/* Tauri Transparent Window with Rounded Corners - Official Approach */\nhtml, body {\n  height: 100%;\n  width: 100%;\n  margin: 0;\n  padding: 0;\n  /* Transparent background for window transparency */\n  background-color: rgba(0, 0, 0, 0);\n}\n\n/* Tauri drag region helpers */\n.tauri-drag {\n  -webkit-app-region: drag;\n}\n\n.tauri-no-drag {\n  -webkit-app-region: no-drag;\n}\n\n/* Apply rounded corners and background to body */\nbody {\n  border-radius: var(--radius-lg);\n  overflow: hidden;\n  background-color: var(--color-background);\n}\n\n#root {\n  height: 100%;\n  width: 100%;\n  /* Ensure root also clips fixed-position descendants */\n  border-radius: inherit;\n  overflow: hidden;\n}\n\n/* Ensure the viewport itself clips fixed-position elements */\nhtml {\n  border-radius: var(--radius-lg);\n  overflow: hidden;\n  /* Robust clipping for fixed descendants and backdrop filters */\n  clip-path: inset(0 round var(--radius-lg));\n}\n\n/* Inter Font - Local */\n@font-face {\n  font-family: 'Inter';\n  font-style: normal;\n  font-weight: 100 900;\n  font-display: swap;\n  src: url('/src/assets/fonts/inter/Inter.ttf') format('truetype-variations');\n}\n\n/* Custom scrollbar hiding */\n.scrollbar-hide {\n  scrollbar-width: none;\n  -ms-overflow-style: none;\n}\n\n.scrollbar-hide::-webkit-scrollbar {\n  display: none;\n}\n\n/* Thin scrollbar for textarea */\n.scrollbar-thin {\n  scrollbar-width: thin;\n  scrollbar-color: var(--color-border) transparent;\n}\n\n.scrollbar-thin::-webkit-scrollbar {\n  width: 6px;\n  height: 6px;\n}\n\n.scrollbar-thin::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.scrollbar-thin::-webkit-scrollbar-thumb {\n  background-color: var(--color-border);\n  border-radius: 3px;\n}\n\n.scrollbar-thin::-webkit-scrollbar-thumb:hover {\n  background-color: var(--color-muted-foreground);\n}\n\n/* Dark theme configuration */\n@theme {\n  /* Colors */\n  --color-background: oklch(0.10 0.01 240);\n  --color-foreground: oklch(0.95 0.01 240);\n  --color-card: oklch(0.14 0.01 240);\n  --color-card-foreground: oklch(0.95 0.01 240);\n  --color-popover: oklch(0.13 0.01 240);\n  --color-popover-foreground: oklch(0.95 0.01 240);\n  --color-primary: oklch(0.95 0.01 240);\n  --color-primary-foreground: oklch(0.14 0.01 240);\n  --color-secondary: oklch(0.18 0.01 240);\n  --color-secondary-foreground: oklch(0.95 0.01 240);\n  --color-muted: oklch(0.16 0.01 240);\n  --color-muted-foreground: oklch(0.65 0.01 240);\n  --color-accent: oklch(0.18 0.01 240);\n  --color-accent-foreground: oklch(0.95 0.01 240);\n  --color-destructive: oklch(0.6 0.2 25);\n  --color-destructive-foreground: oklch(0.98 0.01 240);\n  --color-border: oklch(0.20 0.01 240);\n  --color-input: oklch(0.20 0.01 240);\n  --color-ring: oklch(0.50 0.015 240);\n  \n  /* Additional colors for status messages */\n  --color-green-500: oklch(0.72 0.20 142);\n  --color-green-600: oklch(0.64 0.22 142);\n\n  /* Border radius */\n  --radius-sm: 0.25rem;\n  --radius-base: 0.375rem;\n  --radius-md: 0.5rem;\n  --radius-lg: 0.75rem;\n  --radius-xl: 1rem;\n\n  /* Fonts */\n  --font-sans: \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  --font-mono: ui-monospace, SFMono-Regular, \"SF Mono\", Consolas, \"Liberation Mono\", Menlo, monospace;\n  \n  /* Font Weights */\n  --font-weight-thin: 100;\n  --font-weight-extralight: 200;\n  --font-weight-light: 300;\n  --font-weight-normal: 400;\n  --font-weight-medium: 500;\n  --font-weight-semibold: 600;\n  --font-weight-bold: 700;\n  --font-weight-extrabold: 800;\n  --font-weight-black: 900;\n  \n  /* Font Sizes */\n  --text-xs: 0.75rem;      /* 12px */\n  --text-sm: 0.875rem;     /* 14px */\n  --text-base: 1rem;       /* 16px */\n  --text-lg: 1.125rem;     /* 18px */\n  --text-xl: 1.25rem;      /* 20px */\n  --text-2xl: 1.5rem;      /* 24px */\n  --text-3xl: 1.875rem;    /* 30px */\n  --text-4xl: 2.25rem;     /* 36px */\n  --text-5xl: 3rem;        /* 48px */\n  \n  /* Line Heights */\n  --leading-none: 1;\n  --leading-tight: 1.25;\n  --leading-snug: 1.375;\n  --leading-normal: 1.5;\n  --leading-relaxed: 1.625;\n  --leading-loose: 1.75;\n  \n  /* Letter Spacing */\n  --tracking-tighter: -0.05em;\n  --tracking-tight: -0.025em;\n  --tracking-normal: 0;\n  --tracking-wide: 0.025em;\n  --tracking-wider: 0.05em;\n  --tracking-widest: 0.1em;\n\n  /* Transitions */\n  --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);\n  --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);\n}\n\n/* Theme Variations */\n/* Default is dark theme - already defined above */\n\n/* Light Theme */\n.theme-light {\n  --color-background: oklch(0.98 0.01 240);\n  --color-foreground: oklch(0.12 0.01 240);\n  --color-card: oklch(0.96 0.01 240);\n  --color-card-foreground: oklch(0.12 0.01 240);\n  --color-popover: oklch(0.98 0.01 240);\n  --color-popover-foreground: oklch(0.12 0.01 240);\n  --color-primary: oklch(0.12 0.01 240);\n  --color-primary-foreground: oklch(0.98 0.01 240);\n  --color-secondary: oklch(0.94 0.01 240);\n  --color-secondary-foreground: oklch(0.12 0.01 240);\n  --color-muted: oklch(0.94 0.01 240);\n  --color-muted-foreground: oklch(0.45 0.01 240);\n  --color-accent: oklch(0.94 0.01 240);\n  --color-accent-foreground: oklch(0.12 0.01 240);\n  --color-destructive: oklch(0.6 0.2 25);\n  --color-destructive-foreground: oklch(0.98 0.01 240);\n  --color-border: oklch(0.90 0.01 240);\n  --color-input: oklch(0.90 0.01 240);\n  --color-ring: oklch(0.52 0.015 240);\n  \n  /* Additional colors for status messages */\n  --color-green-500: oklch(0.62 0.20 142);\n  --color-green-600: oklch(0.54 0.22 142);\n}\n\n/* Gray Theme */\n.theme-gray {\n  --color-background: oklch(0.18 0.01 240);\n  --color-foreground: oklch(0.95 0.01 240);\n  --color-card: oklch(0.23 0.01 240);\n  --color-card-foreground: oklch(0.95 0.01 240);\n  --color-popover: oklch(0.21 0.01 240);\n  --color-popover-foreground: oklch(0.95 0.01 240);\n  --color-primary: oklch(0.95 0.01 240);\n  --color-primary-foreground: oklch(0.23 0.01 240);\n  --color-secondary: oklch(0.27 0.01 240);\n  --color-secondary-foreground: oklch(0.95 0.01 240);\n  --color-muted: oklch(0.27 0.01 240);\n  --color-muted-foreground: oklch(0.65 0.01 240);\n  --color-accent: oklch(0.27 0.01 240);\n  --color-accent-foreground: oklch(0.95 0.01 240);\n  --color-destructive: oklch(0.6 0.2 25);\n  --color-destructive-foreground: oklch(0.98 0.01 240);\n  --color-border: oklch(0.32 0.01 240);\n  --color-input: oklch(0.32 0.01 240);\n  --color-ring: oklch(0.55 0.015 240);\n  \n  /* Additional colors for status messages */\n  --color-green-500: oklch(0.72 0.20 142);\n  --color-green-600: oklch(0.64 0.22 142);\n}\n\n/* White Theme (High Contrast Light) */\n.theme-white {\n  --color-background: oklch(0.99 0 240);\n  --color-foreground: oklch(0.10 0 240);\n  --color-card: oklch(1.0 0 240);\n  --color-card-foreground: oklch(0.10 0 240);\n  --color-popover: oklch(1.0 0 240);\n  --color-popover-foreground: oklch(0.10 0 240);\n  --color-primary: oklch(0.10 0 240);\n  --color-primary-foreground: oklch(1.0 0 240);\n  --color-secondary: oklch(0.95 0.01 240);\n  --color-secondary-foreground: oklch(0.10 0 240);\n  --color-muted: oklch(0.93 0.01 240);\n  --color-muted-foreground: oklch(0.40 0.01 240);\n  --color-accent: oklch(0.95 0.01 240);\n  --color-accent-foreground: oklch(0.10 0 240);\n  --color-destructive: oklch(0.55 0.25 25);\n  --color-destructive-foreground: oklch(1.0 0 240);\n  --color-border: oklch(0.88 0.01 240);\n  --color-input: oklch(0.88 0.01 240);\n  --color-ring: oklch(0.45 0.015 240);\n  \n  /* Additional colors for status messages */\n  --color-green-500: oklch(0.55 0.25 142);\n  --color-green-600: oklch(0.47 0.27 142);\n}\n\n/* Custom Theme - CSS variables will be set dynamically by ThemeContext */\n.theme-custom {\n  /* Custom theme variables are applied dynamically via JavaScript */\n}\n\n/* Reset and base styles */\n* {\n  border-color: var(--color-border);\n}\n\nhtml {\n  color-scheme: dark;\n}\n\nbody {\n  background-color: var(--color-background);\n  color: var(--color-foreground);\n  font-family: var(--font-sans);\n}\n\n/* Placeholder text styling */\ninput::placeholder,\ntextarea::placeholder {\n  color: var(--color-muted-foreground);\n  opacity: 0.6;\n}\n\n/* Cursor pointer for all interactive elements */\nbutton,\na,\n[role=\"button\"],\n[role=\"link\"],\n[role=\"menuitem\"],\n[role=\"tab\"],\n[tabindex]:not([tabindex=\"-1\"]),\n.cursor-pointer {\n  cursor: pointer;\n}\n\n/* Ensure disabled elements don't have pointer cursor */\nbutton:disabled,\n[disabled],\n.disabled {\n  cursor: not-allowed !important;\n}\n\n/* Remove all focus styles globally */\n* {\n  outline: none !important;\n  outline-offset: 0 !important;\n}\n\n*:focus,\n*:focus-visible,\n*:focus-within {\n  outline: none !important;\n  box-shadow: none !important;\n}\n\n/* Specifically remove focus styles from form elements */\ninput:focus,\ninput:focus-visible,\ntextarea:focus,\ntextarea:focus-visible,\nselect:focus,\nselect:focus-visible,\nbutton:focus,\nbutton:focus-visible,\n[role=\"button\"]:focus,\n[role=\"button\"]:focus-visible,\n[role=\"combobox\"]:focus,\n[role=\"combobox\"]:focus-visible {\n  outline: none !important;\n  box-shadow: none !important;\n  border-color: var(--color-input) !important;\n}\n\n/* Remove ring styles */\n.ring-0,\n.ring-1,\n.ring-2,\n.ring,\n.ring-offset-0,\n.ring-offset-1,\n.ring-offset-2,\n.ring-offset {\n  box-shadow: none !important;\n}\n\n/* macOS-only: subtle window outline for transparent, custom-decorated window */\nhtml.is-macos body {\n  /* Maintain rounded-corner shape and add a gentle 1px inner outline */\n  box-shadow: inset 0 0 0 1px var(--color-border);\n}\n\n/* Typography Utility Classes */\n.text-display-1 {\n  font-size: var(--text-5xl);\n  font-weight: var(--font-weight-bold);\n  line-height: var(--leading-tight);\n  letter-spacing: var(--tracking-tight);\n}\n\n.text-display-2 {\n  font-size: var(--text-4xl);\n  font-weight: var(--font-weight-bold);\n  line-height: var(--leading-tight);\n  letter-spacing: var(--tracking-tight);\n}\n\n.text-heading-1 {\n  font-size: var(--text-3xl);\n  font-weight: var(--font-weight-semibold);\n  line-height: var(--leading-tight);\n  letter-spacing: var(--tracking-tight);\n}\n\n.text-heading-2 {\n  font-size: var(--text-2xl);\n  font-weight: var(--font-weight-semibold);\n  line-height: var(--leading-snug);\n}\n\n.text-heading-3 {\n  font-size: var(--text-xl);\n  font-weight: var(--font-weight-semibold);\n  line-height: var(--leading-snug);\n}\n\n.text-heading-4 {\n  font-size: var(--text-lg);\n  font-weight: var(--font-weight-medium);\n  line-height: var(--leading-normal);\n}\n\n.text-body-large {\n  font-size: var(--text-lg);\n  font-weight: var(--font-weight-normal);\n  line-height: var(--leading-relaxed);\n}\n\n.text-body {\n  font-size: var(--text-base);\n  font-weight: var(--font-weight-normal);\n  line-height: var(--leading-normal);\n}\n\n.text-body-small {\n  font-size: var(--text-sm);\n  font-weight: var(--font-weight-normal);\n  line-height: var(--leading-normal);\n}\n\n.text-caption {\n  font-size: var(--text-xs);\n  font-weight: var(--font-weight-normal);\n  line-height: var(--leading-normal);\n}\n\n.text-label {\n  font-size: var(--text-sm);\n  font-weight: var(--font-weight-medium);\n  line-height: var(--leading-tight);\n  letter-spacing: var(--tracking-wide);\n}\n\n.text-button {\n  font-size: var(--text-sm);\n  font-weight: var(--font-weight-medium);\n  line-height: var(--leading-tight);\n  letter-spacing: var(--tracking-wide);\n}\n\n.text-overline {\n  font-size: var(--text-xs);\n  font-weight: var(--font-weight-semibold);\n  line-height: var(--leading-tight);\n  letter-spacing: var(--tracking-wider);\n  text-transform: uppercase;\n}\n\n/* Custom utilities */\n@utility animate-in {\n  animation-name: enter;\n  animation-duration: 150ms;\n  animation-fill-mode: both;\n}\n\n@utility animate-out {\n  animation-name: exit;\n  animation-duration: 150ms;\n  animation-fill-mode: both;\n}\n\n@utility line-clamp-2 {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n}\n\n@keyframes enter {\n  from {\n    opacity: var(--tw-enter-opacity, 1);\n    transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0));\n  }\n}\n\n@keyframes exit {\n  to {\n    opacity: var(--tw-exit-opacity, 1);\n    transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0));\n  }\n}\n\n/* Markdown Editor Theme-aware Styles */\n[data-color-mode=\"dark\"],\n.theme-dark [data-color-mode=\"dark\"],\n.theme-gray [data-color-mode=\"dark\"] {\n  --color-border-default: rgb(48, 54, 61);\n  --color-canvas-default: rgb(13, 17, 23);\n  --color-canvas-subtle: rgb(22, 27, 34);\n  --color-fg-default: rgb(201, 209, 217);\n  --color-fg-muted: rgb(139, 148, 158);\n  --color-fg-subtle: rgb(110, 118, 129);\n  --color-accent-fg: rgb(88, 166, 255);\n  --color-danger-fg: rgb(248, 81, 73);\n}\n\n[data-color-mode=\"light\"],\n.theme-light [data-color-mode=\"light\"],\n.theme-white [data-color-mode=\"light\"] {\n  --color-border-default: rgb(216, 222, 228);\n  --color-canvas-default: rgb(255, 255, 255);\n  --color-canvas-subtle: rgb(246, 248, 250);\n  --color-fg-default: rgb(31, 35, 40);\n  --color-fg-muted: rgb(101, 109, 118);\n  --color-fg-subtle: rgb(149, 157, 165);\n  --color-accent-fg: rgb(9, 105, 218);\n  --color-danger-fg: rgb(207, 34, 46);\n}\n\n.w-md-editor {\n  background-color: transparent !important;\n  color: var(--color-foreground) !important;\n}\n\n.w-md-editor.w-md-editor-focus {\n  box-shadow: none !important;\n  border-color: var(--color-border) !important;\n  outline: none !important;\n}\n\n.w-md-editor-toolbar {\n  background-color: var(--color-card) !important;\n  border-bottom: 1px solid var(--color-border) !important;\n}\n\n.w-md-editor-toolbar-divider {\n  background-color: var(--color-border) !important;\n}\n\n.w-md-editor-toolbar button {\n  color: var(--color-foreground) !important;\n}\n\n.w-md-editor-toolbar button:hover {\n  background-color: var(--color-accent) !important;\n  color: var(--color-accent-foreground) !important;\n}\n\n.w-md-editor-content {\n  background-color: var(--color-background) !important;\n}\n\n.w-md-editor-text-pre,\n.w-md-editor-text-input,\n.w-md-editor-text {\n  color: var(--color-foreground) !important;\n  background-color: transparent !important;\n}\n\n.w-md-editor-preview {\n  background-color: var(--color-background) !important;\n}\n\n.wmde-markdown {\n  background-color: transparent !important;\n  color: var(--color-foreground) !important;\n}\n\n/* Prose styles for markdown rendering */\n.prose {\n  color: var(--color-foreground);\n  max-width: 65ch;\n  font-size: 1rem;\n  line-height: 1.75;\n}\n\n.prose-sm {\n  font-size: 0.875rem;\n  line-height: 1.714;\n}\n\n.prose p {\n  margin-top: 1.25em;\n  margin-bottom: 1.25em;\n}\n\n.prose-sm p {\n  margin-top: 1.143em;\n  margin-bottom: 1.143em;\n}\n\n.prose [class~=\"lead\"] {\n  font-size: 1.25em;\n  line-height: 1.6;\n  margin-top: 1.2em;\n  margin-bottom: 1.2em;\n}\n\n.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {\n  margin-top: 0;\n  margin-bottom: 0.8888889em;\n  font-weight: 600;\n  line-height: 1.1111111;\n}\n\n.prose h1 {\n  font-size: 2.25em;\n}\n\n.prose h2 {\n  font-size: 1.5em;\n}\n\n.prose h3 {\n  font-size: 1.25em;\n}\n\n.prose h4 {\n  font-size: 1em;\n}\n\n.prose a {\n  color: var(--color-primary);\n  text-decoration: underline;\n  font-weight: 500;\n}\n\n.prose strong {\n  font-weight: 600;\n}\n\n.prose ol, .prose ul {\n  margin-top: 1.25em;\n  margin-bottom: 1.25em;\n  padding-left: 1.625em;\n}\n\n.prose li {\n  margin-top: 0.5em;\n  margin-bottom: 0.5em;\n}\n\n.prose > ul > li p {\n  margin-top: 0.75em;\n  margin-bottom: 0.75em;\n}\n\n.prose > ol > li > *:first-child {\n  margin-top: 1.25em;\n}\n\n.prose code {\n  font-weight: 600;\n  font-size: 0.875em;\n  background-color: var(--color-muted);\n  padding: 0.125em 0.375em;\n  border-radius: 0.25rem;\n}\n\n.prose pre {\n  overflow-x: auto;\n  font-size: 0.875em;\n  line-height: 1.714;\n  margin-top: 1.714em;\n  margin-bottom: 1.714em;\n  border-radius: 0.375rem;\n  padding: 0.857em 1.143em;\n  background-color: var(--color-card);\n}\n\n.prose pre code {\n  background-color: transparent;\n  border-width: 0;\n  border-radius: 0;\n  padding: 0;\n  font-weight: 400;\n  color: inherit;\n  font-size: inherit;\n  font-family: inherit;\n  line-height: inherit;\n}\n\n.prose blockquote {\n  font-weight: 500;\n  font-style: italic;\n  margin-top: 1.6em;\n  margin-bottom: 1.6em;\n  padding-left: 1em;\n  border-left: 0.25rem solid var(--color-border);\n}\n\n.prose hr {\n  margin-top: 3em;\n  margin-bottom: 3em;\n  border-color: var(--color-border);\n}\n\n.prose table {\n  width: 100%;\n  table-layout: auto;\n  text-align: left;\n  margin-top: 2em;\n  margin-bottom: 2em;\n  font-size: 0.875em;\n  line-height: 1.714;\n}\n\n.prose thead {\n  border-bottom-width: 1px;\n  border-bottom-color: var(--color-border);\n}\n\n.prose thead th {\n  vertical-align: bottom;\n  padding-right: 0.571em;\n  padding-bottom: 0.571em;\n  padding-left: 0.571em;\n  font-weight: 600;\n}\n\n.prose tbody tr {\n  border-bottom-width: 1px;\n  border-bottom-color: var(--color-border);\n}\n\n.prose tbody tr:last-child {\n  border-bottom-width: 0;\n}\n\n.prose tbody td {\n  vertical-align: baseline;\n  padding: 0.571em;\n}\n\n/* Dark mode adjustments */\n.prose.dark\\:prose-invert {\n  color: var(--color-foreground);\n}\n\n.prose.dark\\:prose-invert a {\n  color: var(--color-primary);\n}\n\n.prose.dark\\:prose-invert strong {\n  color: inherit;\n}\n\n.prose.dark\\:prose-invert code {\n  color: var(--color-foreground);\n  background-color: var(--color-muted);\n}\n\n.prose.dark\\:prose-invert pre {\n  /*background-color: rgb(13, 17, 23);*/\n  border: 1px solid var(--color-border);\n}\n\n.prose.dark\\:prose-invert thead {\n  border-bottom-color: var(--color-border);\n}\n\n.prose.dark\\:prose-invert tbody tr {\n  border-bottom-color: var(--color-border);\n}\n\n/* Remove maximum width constraint */\n.prose.max-w-none {\n  max-width: none;\n}\n\n/* Rotating symbol animation */\n@keyframes rotate-symbol {\n  0%   { content: \"◐\"; transform: scale(1); }\n  25%  { content: \"◓\"; transform: scale(1); }\n  50%  { content: \"◑\"; transform: scale(1); }\n  75%  { content: \"◒\"; transform: scale(1); }\n  100% { content: \"◐\"; transform: scale(1); }\n}\n\n@keyframes fade-in {\n  from {\n    opacity: 0;\n    transform: scale(0.8);\n  }\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n.rotating-symbol {\n  display: inline-block;\n  vertical-align: middle;\n  line-height: 1;\n  animation: fade-in 0.2s ease-out;\n  font-weight: normal;\n  font-size: 1.5rem; /* Make it bigger! */\n  position: relative;\n  top: -2px;\n}\n\n.rotating-symbol::before {\n  content: \"◐\";\n  animation: rotate-symbol 1.6s steps(4, end) infinite;\n  display: inline-block;\n  font-size: inherit;\n  line-height: 1;\n  vertical-align: baseline;\n  transform-origin: center;\n}\n\n/* Removed special font-weight for larger sizes to maintain consistency */\n\n/* Shimmer hover effect */\n@keyframes shimmer {\n  0% {\n    background-position: -200% 0;\n  }\n  100% {\n    background-position: 200% 0;\n  }\n}\n\n.shimmer-hover {\n  position: relative;\n  overflow: hidden;\n}\n\n.shimmer-hover::before {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  left: -100%;\n  width: 100%;\n  height: 100%;\n  background: linear-gradient(\n    90deg,\n    transparent,\n    rgba(255, 255, 255, 0.05),\n    transparent\n  );\n  transition: left 0.5s;\n}\n\n.shimmer-hover:hover::before {\n  left: 100%;\n  animation: shimmer 0.5s;\n}\n\n/* Trailing border effect */\n@property --angle {\n  syntax: \"<angle>\";\n  initial-value: 0deg;\n  inherits: false;\n}\n\n@keyframes trail-rotate {\n  to {\n    --angle: 360deg;\n  }\n}\n\n.trailing-border {\n  position: relative;\n  background: var(--color-card);\n  z-index: 0;\n  overflow: visible;\n}\n\n/* The correctly traveling border line */\n.trailing-border::after {\n  content: \"\";\n  position: absolute;\n  inset: -2px;\n  padding: 2px;\n  border-radius: inherit;\n  background: conic-gradient(\n    from var(--angle),\n    transparent 0%,\n    transparent 85%,\n    #d97757 90%,\n    #ff9a7a 92.5%,\n    #d97757 95%,\n    transparent 100%\n  );\n  -webkit-mask: \n    linear-gradient(#fff 0 0) content-box, \n    linear-gradient(#fff 0 0);\n  -webkit-mask-composite: xor;\n  mask-composite: exclude;\n  opacity: 0;\n  transition: opacity 0.3s ease;\n  z-index: -1;\n}\n\n.trailing-border:hover::after {\n  opacity: 1;\n  animation: trail-rotate 2s linear infinite;\n}\n\n/* Ensure the card content stays above the border effect */\n.trailing-border > * {\n  position: relative;\n  z-index: 1;\n}\n\n/* --- ELEGANT SCROLLBARS --- */\n\n/* Firefox - thin and minimal */\n* {\n  scrollbar-width: thin;\n  scrollbar-color: rgba(156, 163, 175, 0.3) transparent;\n}\n\n/* Global webkit scrollbar - ultra thin and elegant */\n::-webkit-scrollbar {\n  width: 3px;\n  height: 3px;\n}\n\n::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n::-webkit-scrollbar-thumb {\n  background-color: rgba(156, 163, 175, 0.5);\n  border-radius: 2px;\n  transition: background-color 0.2s ease;\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background-color: rgba(156, 163, 175, 0.6);\n}\n\n::-webkit-scrollbar-corner {\n  background: transparent;\n}\n\n/* Code blocks - slightly larger for better usability */\npre::-webkit-scrollbar,\n.w-md-editor-content::-webkit-scrollbar,\ncode::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\npre::-webkit-scrollbar-thumb,\n.w-md-editor-content::-webkit-scrollbar-thumb,\ncode::-webkit-scrollbar-thumb {\n  background-color: rgba(156, 163, 175, 0.4);\n  border-radius: 4px;\n}\n\npre::-webkit-scrollbar-thumb:hover,\n.w-md-editor-content::-webkit-scrollbar-thumb:hover,\ncode::-webkit-scrollbar-thumb:hover {\n  background-color: rgba(156, 163, 175, 0.6);\n}\n\n/* NFO Credits Scanlines Animation */\n@keyframes scanlines {\n  0% {\n    transform: translateY(-100%);\n  }\n  100% {\n    transform: translateY(100%);\n  }\n}\n\n.animate-scanlines {\n  animation: scanlines 8s linear infinite;\n}\n\n/* Screenshot Shutter Animation */\n@keyframes shutterFlash {\n  0% {\n    opacity: 0;\n  }\n  50% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 0;\n  }\n}\n\n.shutter-flash {\n  animation: shutterFlash 0.5s ease-in-out;\n}\n\n/* Image Move to Input Animation */\n@keyframes moveToInput {\n  0% {\n    transform: scale(1) translateY(0);\n    opacity: 1;\n  }\n  50% {\n    transform: scale(0.3) translateY(50%);\n    opacity: 0.8;\n  }\n  100% {\n    transform: scale(0.1) translateY(100vh);\n    opacity: 0;\n  }\n}\n\n.image-move-to-input {\n  animation: moveToInput 0.8s ease-in-out forwards;\n} \n"
  },
  {
    "path": "src/types/hooks.ts",
    "content": "/**\n * Types for Claude Code hooks configuration\n */\n\nexport interface HookCommand {\n  type: 'command';\n  command: string;\n  timeout?: number; // Optional timeout in seconds (default: 60)\n}\n\nexport interface HookMatcher {\n  matcher?: string; // Pattern to match tool names (regex supported)\n  hooks: HookCommand[];\n}\n\nexport interface HooksConfiguration {\n  PreToolUse?: HookMatcher[];\n  PostToolUse?: HookMatcher[];\n  Notification?: HookCommand[];\n  Stop?: HookCommand[];\n  SubagentStop?: HookCommand[];\n}\n\nexport type HookEvent = keyof HooksConfiguration;\n\nexport interface ClaudeSettingsWithHooks {\n  hooks?: HooksConfiguration;\n  [key: string]: any;\n}\n\nexport interface HookValidationError {\n  event: string;\n  matcher?: string;\n  command?: string;\n  message: string;\n}\n\nexport interface HookValidationWarning {\n  event: string;\n  matcher?: string;\n  command: string;\n  message: string;\n}\n\nexport interface HookValidationResult {\n  valid: boolean;\n  errors: HookValidationError[];\n  warnings: HookValidationWarning[];\n}\n\nexport type HookScope = 'user' | 'project' | 'local';\n\n// Common tool matchers for autocomplete\nexport const COMMON_TOOL_MATCHERS = [\n  'Task',\n  'Bash',\n  'Glob',\n  'Grep',\n  'Read',\n  'Edit',\n  'MultiEdit',\n  'Write',\n  'WebFetch',\n  'WebSearch',\n  'Notebook.*',\n  'Edit|Write',\n  'mcp__.*',\n  'mcp__memory__.*',\n  'mcp__filesystem__.*',\n  'mcp__github__.*',\n];\n\n// Hook templates\nexport interface HookTemplate {\n  id: string;\n  name: string;\n  description: string;\n  event: HookEvent;\n  matcher?: string;\n  commands: string[];\n}\n\nexport const HOOK_TEMPLATES: HookTemplate[] = [\n  {\n    id: 'log-bash-commands',\n    name: 'Log Shell Commands',\n    description: 'Log all bash commands to a file for auditing',\n    event: 'PreToolUse',\n    matcher: 'Bash',\n    commands: ['jq -r \\'\"\\(.tool_input.command) - \\(.tool_input.description // \"No description\")\"\\' >> ~/.claude/bash-command-log.txt']\n  },\n  {\n    id: 'format-on-save',\n    name: 'Auto-format Code',\n    description: 'Run code formatters after file modifications',\n    event: 'PostToolUse',\n    matcher: 'Write|Edit|MultiEdit',\n    commands: [\n      'if [[ \"$( jq -r .tool_input.file_path )\" =~ \\\\.(ts|tsx|js|jsx)$ ]]; then prettier --write \"$( jq -r .tool_input.file_path )\"; fi',\n      'if [[ \"$( jq -r .tool_input.file_path )\" =~ \\\\.go$ ]]; then gofmt -w \"$( jq -r .tool_input.file_path )\"; fi'\n    ]\n  },\n  {\n    id: 'git-commit-guard',\n    name: 'Protect Main Branch',\n    description: 'Prevent direct commits to main/master branch',\n    event: 'PreToolUse',\n    matcher: 'Bash',\n    commands: ['if [[ \"$(jq -r .tool_input.command)\" =~ \"git commit\" ]] && [[ \"$(git branch --show-current 2>/dev/null)\" =~ ^(main|master)$ ]]; then echo \"Direct commits to main/master branch are not allowed\"; exit 2; fi']\n  },\n  {\n    id: 'custom-notification',\n    name: 'Custom Notifications',\n    description: 'Send custom notifications when Claude needs attention',\n    event: 'Notification',\n    commands: ['osascript -e \"display notification \\\\\"$(jq -r .message)\\\\\" with title \\\\\"$(jq -r .title)\\\\\" sound name \\\\\"Glass\\\\\"\"']\n  },\n  {\n    id: 'continue-on-tests',\n    name: 'Auto-continue on Test Success',\n    description: 'Automatically continue when tests pass',\n    event: 'Stop',\n    commands: ['if grep -q \"All tests passed\" \"$( jq -r .transcript_path )\"; then echo \\'{\"decision\": \"block\", \"reason\": \"All tests passed. Continue with next task.\"}\\'; fi']\n  }\n]; \n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "src-tauri/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Generated by Tauri\n# will have schema files for capabilities auto-completion\n/gen/schemas\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "content": "[package]\nname = \"opcode\"\nversion = \"0.2.1\"\ndescription = \"GUI app and Toolkit for Claude Code\"\nauthors = [\"mufeedvh\", \"123vviekr\"]\nlicense = \"AGPL-3.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[[bin]]\nname = \"opcode\"\npath = \"src/main.rs\"\n\n[lib]\nname = \"opcode_lib\"\ncrate-type = [\"lib\", \"cdylib\", \"staticlib\"]\n\n[[bin]]\nname = \"opcode-web\"\npath = \"src/web_main.rs\"\n\n[build-dependencies]\ntauri-build = { version = \"2\", features = [] }\n\n[dependencies]\ntauri = { version = \"2\", features = [ \"macos-private-api\", \"protocol-asset\", \"tray-icon\", \"image-png\"] }\ntauri-plugin-shell = \"2\"\ntauri-plugin-dialog = \"2\"\ntauri-plugin-fs = \"2\"\ntauri-plugin-process = \"2\"\ntauri-plugin-updater = \"2\"\ntauri-plugin-notification = \"2\"\ntauri-plugin-clipboard-manager = \"2\"\ntauri-plugin-global-shortcut = \"2\"\ntauri-plugin-http = \"2\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\ntokio = { version = \"1\", features = [\"full\"] }\nrusqlite = { version = \"0.32\", features = [\"bundled\"] }\ndirs = \"5\"\nchrono = { version = \"0.4\", features = [\"serde\"] }\nanyhow = \"1\"\nlog = \"0.4\"\nenv_logger = \"0.11\"\nregex = \"1\"\nglob = \"0.3\"\nbase64 = \"0.22\"\nlibc = \"0.2\"\nreqwest = { version = \"0.12\", features = [\"json\", \"native-tls-vendored\"] }\nfutures = \"0.3\"\nasync-trait = \"0.1\"\ntempfile = \"3\"\nwhich = \"7\"\nsha2 = \"0.10\"\nzstd = \"0.13\"\nuuid = { version = \"1.6\", features = [\"v4\", \"serde\"] }\nwalkdir = \"2\"\nserde_yaml = \"0.9\"\naxum = { version = \"0.8\", features = [\"ws\"] }\ntower = \"0.5\"\ntower-http = { version = \"0.6\", features = [\"fs\", \"cors\"] }\nclap = { version = \"4.0\", features = [\"derive\"] }\nfutures-util = \"0.3\"\n# Pin image to avoid edition2024 requirement\nimage = \"=0.25.1\"\n\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\ntauri = { version = \"2\", features = [\"macos-private-api\"] }\nwindow-vibrancy = \"0.5\"\ncocoa = \"0.26\"\nobjc = \"0.2\"\n\n[features]\n# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!\ncustom-protocol = [\"tauri/custom-protocol\"]\n\n[profile.release]\nstrip = true\nopt-level = \"z\"\nlto = true\ncodegen-units = 1\n"
  },
  {
    "path": "src-tauri/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>NSRequiresAquaSystemAppearance</key>\n  <false/>\n  <key>LSMinimumSystemVersion</key>\n  <string>10.15</string>\n  <key>CFBundleShortVersionString</key>\n  <string>0.2.1</string>\n  <key>CFBundleName</key>\n  <string>opcode</string>\n  <key>CFBundleDisplayName</key>\n  <string>opcode</string>\n  <key>CFBundleIdentifier</key>\n  <string>opcode.asterisk.so</string>\n  <key>CFBundleDocumentTypes</key>\n  <array>\n    <dict>\n      <key>CFBundleTypeName</key>\n      <string>opcode Agent</string>\n      <key>CFBundleTypeRole</key>\n      <string>Editor</string>\n      <key>CFBundleTypeExtensions</key>\n      <array>\n        <string>opcode.json</string>\n      </array>\n      <key>CFBundleTypeIconFile</key>\n      <string>icon.icns</string>\n      <key>LSHandlerRank</key>\n      <string>Owner</string>\n    </dict>\n  </array>\n  <key>NSAppleEventsUsageDescription</key>\n  <string>opcode needs to send Apple Events to other applications.</string>\n  <key>NSAppleScriptEnabled</key>\n  <true/>\n  <key>NSCameraUsageDescription</key>\n  <string>opcode needs camera access for capturing images for AI processing.</string>\n  <key>NSMicrophoneUsageDescription</key>\n  <string>opcode needs microphone access for voice input features.</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "src-tauri/build.rs",
    "content": "fn main() {\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/capabilities/default.json",
    "content": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"default\",\n  \"description\": \"Capability for the main window\",\n  \"windows\": [\"main\"],\n  \"permissions\": [\n    \"core:default\",\n    \"dialog:default\",\n    \"dialog:allow-open\",\n    \"dialog:allow-save\",\n    \"shell:allow-execute\",\n    \"shell:allow-spawn\",\n    \"shell:allow-open\",\n    {\n      \"identifier\": \"shell:allow-execute\",\n      \"allow\": [\n        {\n          \"name\": \"claude\",\n          \"sidecar\": false,\n          \"args\": true\n        }\n      ]\n    },\n    {\n      \"identifier\": \"shell:allow-spawn\",\n      \"allow\": [\n        {\n          \"name\": \"claude\",\n          \"sidecar\": false,\n          \"args\": true\n        }\n      ]\n    },\n    \"fs:default\",\n    \"fs:allow-mkdir\",\n    \"fs:allow-read\",\n    \"fs:allow-write\",\n    \"fs:allow-remove\",\n    \"fs:allow-rename\",\n    \"fs:allow-exists\",\n    \"fs:allow-copy-file\",\n    \"fs:read-all\",\n    \"fs:write-all\",\n    \"fs:scope-app-recursive\",\n    \"fs:scope-home-recursive\",\n    \"http:default\",\n    \"http:allow-fetch\",\n    \"process:default\",\n    \"notification:default\",\n    \"clipboard-manager:default\",\n    \"global-shortcut:default\",\n    \"updater:default\",\n    \"core:window:allow-minimize\",\n    \"core:window:allow-maximize\",\n    \"core:window:allow-unmaximize\", \n    \"core:window:allow-close\",\n    \"core:window:allow-is-maximized\",\n    \"core:window:allow-start-dragging\"\n  ]\n}\n"
  },
  {
    "path": "src-tauri/entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <!-- Allow app to be run from Homebrew installation -->\n    <key>com.apple.security.app-sandbox</key>\n    <false/>\n    \n    <!-- Network access for Claude API and other services -->\n    <key>com.apple.security.network.client</key>\n    <true/>\n    <key>com.apple.security.network.server</key>\n    <true/>\n    \n    <!-- File system access -->\n    <key>com.apple.security.files.user-selected.read-write</key>\n    <true/>\n    <key>com.apple.security.files.downloads.read-write</key>\n    <true/>\n    \n    <!-- Allow spawning subprocesses (needed for shell commands) -->\n    <key>com.apple.security.inherit</key>\n    <true/>\n    \n    <!-- Allow automation for Apple Events -->\n    <key>com.apple.security.automation.apple-events</key>\n    <true/>\n    \n    <!-- Camera and microphone if needed -->\n    <key>com.apple.security.device.camera</key>\n    <true/>\n    <key>com.apple.security.device.microphone</key>\n    <true/>\n    \n    <!-- Printing -->\n    <key>com.apple.security.print</key>\n    <true/>\n    \n    <!-- Required for Hardened Runtime -->\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n    <key>com.apple.security.cs.disable-executable-page-protection</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "src-tauri/src/checkpoint/manager.rs",
    "content": "use anyhow::{Context, Result};\nuse chrono::{DateTime, TimeZone, Utc};\nuse log;\nuse std::collections::HashMap;\nuse std::fs;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse tokio::sync::RwLock;\n\nuse super::{\n    storage::{self, CheckpointStorage},\n    Checkpoint, CheckpointMetadata, CheckpointPaths, CheckpointResult, CheckpointStrategy,\n    FileSnapshot, FileState, FileTracker, SessionTimeline,\n};\n\n/// Manages checkpoint operations for a session\npub struct CheckpointManager {\n    project_id: String,\n    session_id: String,\n    project_path: PathBuf,\n    file_tracker: Arc<RwLock<FileTracker>>,\n    pub storage: Arc<CheckpointStorage>,\n    timeline: Arc<RwLock<SessionTimeline>>,\n    current_messages: Arc<RwLock<Vec<String>>>, // JSONL messages\n}\n\nimpl CheckpointManager {\n    /// Create a new checkpoint manager\n    pub async fn new(\n        project_id: String,\n        session_id: String,\n        project_path: PathBuf,\n        claude_dir: PathBuf,\n    ) -> Result<Self> {\n        let storage = Arc::new(CheckpointStorage::new(claude_dir.clone()));\n\n        // Initialize storage\n        storage.init_storage(&project_id, &session_id)?;\n\n        // Load or create timeline\n        let paths = CheckpointPaths::new(&claude_dir, &project_id, &session_id);\n        let timeline = if paths.timeline_file.exists() {\n            storage.load_timeline(&paths.timeline_file)?\n        } else {\n            SessionTimeline::new(session_id.clone())\n        };\n\n        let file_tracker = FileTracker {\n            tracked_files: HashMap::new(),\n        };\n\n        Ok(Self {\n            project_id,\n            session_id,\n            project_path,\n            file_tracker: Arc::new(RwLock::new(file_tracker)),\n            storage,\n            timeline: Arc::new(RwLock::new(timeline)),\n            current_messages: Arc::new(RwLock::new(Vec::new())),\n        })\n    }\n\n    /// Track a new message in the session\n    pub async fn track_message(&self, jsonl_message: String) -> Result<()> {\n        let mut messages = self.current_messages.write().await;\n        messages.push(jsonl_message.clone());\n\n        // Parse message to check for tool usage\n        if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&jsonl_message) {\n            if let Some(content) = msg.get(\"message\").and_then(|m| m.get(\"content\")) {\n                if let Some(content_array) = content.as_array() {\n                    for item in content_array {\n                        if item.get(\"type\").and_then(|t| t.as_str()) == Some(\"tool_use\") {\n                            if let Some(tool_name) = item.get(\"name\").and_then(|n| n.as_str()) {\n                                if let Some(input) = item.get(\"input\") {\n                                    self.track_tool_operation(tool_name, input).await?;\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        Ok(())\n    }\n\n    /// Track file operations from tool usage\n    async fn track_tool_operation(&self, tool: &str, input: &serde_json::Value) -> Result<()> {\n        match tool.to_lowercase().as_str() {\n            \"edit\" | \"write\" | \"multiedit\" => {\n                if let Some(file_path) = input.get(\"file_path\").and_then(|p| p.as_str()) {\n                    self.track_file_modification(file_path).await?;\n                }\n            }\n            \"bash\" => {\n                // Try to detect file modifications from bash commands\n                if let Some(command) = input.get(\"command\").and_then(|c| c.as_str()) {\n                    self.track_bash_side_effects(command).await?;\n                }\n            }\n            _ => {}\n        }\n        Ok(())\n    }\n\n    /// Track a file modification\n    pub async fn track_file_modification(&self, file_path: &str) -> Result<()> {\n        let mut tracker = self.file_tracker.write().await;\n        let full_path = self.project_path.join(file_path);\n\n        // Read current file state\n        let (hash, exists, _size, modified) = if full_path.exists() {\n            let content = fs::read_to_string(&full_path).unwrap_or_default();\n            let metadata = fs::metadata(&full_path)?;\n            let modified = metadata\n                .modified()\n                .ok()\n                .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())\n                .map(|d| {\n                    Utc.timestamp_opt(d.as_secs() as i64, d.subsec_nanos())\n                        .unwrap()\n                })\n                .unwrap_or_else(Utc::now);\n\n            (\n                storage::CheckpointStorage::calculate_file_hash(&content),\n                true,\n                metadata.len(),\n                modified,\n            )\n        } else {\n            (String::new(), false, 0, Utc::now())\n        };\n\n        // Check if file has actually changed\n        let is_modified =\n            if let Some(existing_state) = tracker.tracked_files.get(&PathBuf::from(file_path)) {\n                // File is modified if:\n                // 1. Hash has changed\n                // 2. Existence state has changed\n                // 3. It was already marked as modified\n                existing_state.last_hash != hash\n                    || existing_state.exists != exists\n                    || existing_state.is_modified\n            } else {\n                // New file is always considered modified\n                true\n            };\n\n        tracker.tracked_files.insert(\n            PathBuf::from(file_path),\n            FileState {\n                last_hash: hash,\n                is_modified,\n                last_modified: modified,\n                exists,\n            },\n        );\n\n        Ok(())\n    }\n\n    /// Track potential file changes from bash commands\n    async fn track_bash_side_effects(&self, command: &str) -> Result<()> {\n        // Common file-modifying commands\n        let file_commands = [\n            \"echo\", \"cat\", \"cp\", \"mv\", \"rm\", \"touch\", \"sed\", \"awk\", \"npm\", \"yarn\", \"pnpm\", \"bun\",\n            \"cargo\", \"make\", \"gcc\", \"g++\",\n        ];\n\n        // Simple heuristic: if command contains file-modifying operations\n        for cmd in &file_commands {\n            if command.contains(cmd) {\n                // Mark all tracked files as potentially modified\n                let mut tracker = self.file_tracker.write().await;\n                for (_, state) in tracker.tracked_files.iter_mut() {\n                    state.is_modified = true;\n                }\n                break;\n            }\n        }\n\n        Ok(())\n    }\n\n    /// Create a checkpoint\n    pub async fn create_checkpoint(\n        &self,\n        description: Option<String>,\n        parent_checkpoint_id: Option<String>,\n    ) -> Result<CheckpointResult> {\n        let messages = self.current_messages.read().await;\n        let message_index = messages.len().saturating_sub(1);\n\n        // Extract metadata from the last user message\n        let (user_prompt, model_used, total_tokens) =\n            self.extract_checkpoint_metadata(&messages).await?;\n\n        // Ensure every file in the project is tracked so new checkpoints include all files\n        // Recursively walk the project directory and track each file\n        fn collect_files(\n            dir: &std::path::Path,\n            base: &std::path::Path,\n            files: &mut Vec<std::path::PathBuf>,\n        ) -> Result<(), std::io::Error> {\n            for entry in std::fs::read_dir(dir)? {\n                let entry = entry?;\n                let path = entry.path();\n                if path.is_dir() {\n                    // Skip hidden directories like .git\n                    if let Some(name) = path.file_name().and_then(|n| n.to_str()) {\n                        if name.starts_with('.') {\n                            continue;\n                        }\n                    }\n                    collect_files(&path, base, files)?;\n                } else if path.is_file() {\n                    // Compute relative path from project root\n                    if let Ok(rel) = path.strip_prefix(base) {\n                        files.push(rel.to_path_buf());\n                    }\n                }\n            }\n            Ok(())\n        }\n        let mut all_files = Vec::new();\n        let project_dir = &self.project_path;\n        let _ = collect_files(project_dir.as_path(), project_dir.as_path(), &mut all_files);\n        for rel in all_files {\n            if let Some(p) = rel.to_str() {\n                // Track each file for snapshot\n                let _ = self.track_file_modification(p).await;\n            }\n        }\n\n        // Generate checkpoint ID early so snapshots reference it\n        let checkpoint_id = storage::CheckpointStorage::generate_checkpoint_id();\n\n        // Create file snapshots\n        let file_snapshots = self.create_file_snapshots(&checkpoint_id).await?;\n\n        // Generate checkpoint struct\n        let checkpoint = Checkpoint {\n            id: checkpoint_id.clone(),\n            session_id: self.session_id.clone(),\n            project_id: self.project_id.clone(),\n            message_index,\n            timestamp: Utc::now(),\n            description,\n            parent_checkpoint_id: {\n                if let Some(parent_id) = parent_checkpoint_id {\n                    Some(parent_id)\n                } else {\n                    // Perform an asynchronous read to avoid blocking within the runtime\n                    let timeline = self.timeline.read().await;\n                    timeline.current_checkpoint_id.clone()\n                }\n            },\n            metadata: CheckpointMetadata {\n                total_tokens,\n                model_used,\n                user_prompt,\n                file_changes: file_snapshots.len(),\n                snapshot_size: storage::CheckpointStorage::estimate_checkpoint_size(\n                    &messages.join(\"\\n\"),\n                    &file_snapshots,\n                ),\n            },\n        };\n\n        // Save checkpoint\n        let messages_content = messages.join(\"\\n\");\n        let result = self.storage.save_checkpoint(\n            &self.project_id,\n            &self.session_id,\n            &checkpoint,\n            file_snapshots,\n            &messages_content,\n        )?;\n\n        // Reload timeline from disk so in-memory timeline has updated nodes and total_checkpoints\n        let claude_dir = self.storage.claude_dir.clone();\n        let paths = CheckpointPaths::new(&claude_dir, &self.project_id, &self.session_id);\n        let updated_timeline = self.storage.load_timeline(&paths.timeline_file)?;\n        {\n            let mut timeline_lock = self.timeline.write().await;\n            *timeline_lock = updated_timeline;\n        }\n\n        // Update timeline (current checkpoint only)\n        let mut timeline = self.timeline.write().await;\n        timeline.current_checkpoint_id = Some(checkpoint_id);\n\n        // Reset file tracker\n        let mut tracker = self.file_tracker.write().await;\n        for (_, state) in tracker.tracked_files.iter_mut() {\n            state.is_modified = false;\n        }\n\n        Ok(result)\n    }\n\n    /// Extract metadata from messages for checkpoint\n    async fn extract_checkpoint_metadata(\n        &self,\n        messages: &[String],\n    ) -> Result<(String, String, u64)> {\n        let mut user_prompt = String::new();\n        let mut model_used = String::from(\"unknown\");\n        let mut total_tokens = 0u64;\n\n        // Iterate through messages in reverse to find the last user prompt\n        for msg_str in messages.iter().rev() {\n            if let Ok(msg) = serde_json::from_str::<serde_json::Value>(msg_str) {\n                // Check for user message\n                if msg.get(\"type\").and_then(|t| t.as_str()) == Some(\"user\") {\n                    if let Some(content) = msg\n                        .get(\"message\")\n                        .and_then(|m| m.get(\"content\"))\n                        .and_then(|c| c.as_array())\n                    {\n                        for item in content {\n                            if item.get(\"type\").and_then(|t| t.as_str()) == Some(\"text\") {\n                                if let Some(text) = item.get(\"text\").and_then(|t| t.as_str()) {\n                                    user_prompt = text.to_string();\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Extract model info\n                if let Some(model) = msg.get(\"model\").and_then(|m| m.as_str()) {\n                    model_used = model.to_string();\n                }\n\n                // Also check for model in message.model (assistant messages)\n                if let Some(message) = msg.get(\"message\") {\n                    if let Some(model) = message.get(\"model\").and_then(|m| m.as_str()) {\n                        model_used = model.to_string();\n                    }\n                }\n\n                // Count tokens - check both top-level and nested usage\n                // First check for usage in message.usage (assistant messages)\n                if let Some(message) = msg.get(\"message\") {\n                    if let Some(usage) = message.get(\"usage\") {\n                        if let Some(input) = usage.get(\"input_tokens\").and_then(|t| t.as_u64()) {\n                            total_tokens += input;\n                        }\n                        if let Some(output) = usage.get(\"output_tokens\").and_then(|t| t.as_u64()) {\n                            total_tokens += output;\n                        }\n                        // Also count cache tokens\n                        if let Some(cache_creation) = usage\n                            .get(\"cache_creation_input_tokens\")\n                            .and_then(|t| t.as_u64())\n                        {\n                            total_tokens += cache_creation;\n                        }\n                        if let Some(cache_read) = usage\n                            .get(\"cache_read_input_tokens\")\n                            .and_then(|t| t.as_u64())\n                        {\n                            total_tokens += cache_read;\n                        }\n                    }\n                }\n\n                // Then check for top-level usage (result messages)\n                if let Some(usage) = msg.get(\"usage\") {\n                    if let Some(input) = usage.get(\"input_tokens\").and_then(|t| t.as_u64()) {\n                        total_tokens += input;\n                    }\n                    if let Some(output) = usage.get(\"output_tokens\").and_then(|t| t.as_u64()) {\n                        total_tokens += output;\n                    }\n                    // Also count cache tokens\n                    if let Some(cache_creation) = usage\n                        .get(\"cache_creation_input_tokens\")\n                        .and_then(|t| t.as_u64())\n                    {\n                        total_tokens += cache_creation;\n                    }\n                    if let Some(cache_read) = usage\n                        .get(\"cache_read_input_tokens\")\n                        .and_then(|t| t.as_u64())\n                    {\n                        total_tokens += cache_read;\n                    }\n                }\n            }\n        }\n\n        Ok((user_prompt, model_used, total_tokens))\n    }\n\n    /// Create file snapshots for all tracked modified files\n    async fn create_file_snapshots(&self, checkpoint_id: &str) -> Result<Vec<FileSnapshot>> {\n        let tracker = self.file_tracker.read().await;\n        let mut snapshots = Vec::new();\n\n        for (rel_path, state) in &tracker.tracked_files {\n            // Skip files that haven't been modified\n            if !state.is_modified {\n                continue;\n            }\n\n            let full_path = self.project_path.join(rel_path);\n\n            let (content, exists, permissions, size, current_hash) = if full_path.exists() {\n                let content = fs::read_to_string(&full_path).unwrap_or_default();\n                let current_hash = storage::CheckpointStorage::calculate_file_hash(&content);\n\n                // Don't skip based on hash - if is_modified is true, we should snapshot it\n                // The hash check in track_file_modification already determined if it changed\n\n                let metadata = fs::metadata(&full_path)?;\n                let permissions = {\n                    #[cfg(unix)]\n                    {\n                        use std::os::unix::fs::PermissionsExt;\n                        Some(metadata.permissions().mode())\n                    }\n                    #[cfg(not(unix))]\n                    {\n                        None\n                    }\n                };\n                (content, true, permissions, metadata.len(), current_hash)\n            } else {\n                (String::new(), false, None, 0, String::new())\n            };\n\n            snapshots.push(FileSnapshot {\n                checkpoint_id: checkpoint_id.to_string(),\n                file_path: rel_path.clone(),\n                content,\n                hash: current_hash,\n                is_deleted: !exists,\n                permissions,\n                size,\n            });\n        }\n\n        Ok(snapshots)\n    }\n\n    /// Restore a checkpoint\n    pub async fn restore_checkpoint(&self, checkpoint_id: &str) -> Result<CheckpointResult> {\n        // Load checkpoint data\n        let (checkpoint, file_snapshots, messages) =\n            self.storage\n                .load_checkpoint(&self.project_id, &self.session_id, checkpoint_id)?;\n\n        // First, collect all files currently in the project to handle deletions\n        fn collect_all_project_files(\n            dir: &std::path::Path,\n            base: &std::path::Path,\n            files: &mut Vec<std::path::PathBuf>,\n        ) -> Result<(), std::io::Error> {\n            for entry in std::fs::read_dir(dir)? {\n                let entry = entry?;\n                let path = entry.path();\n                if path.is_dir() {\n                    // Skip hidden directories like .git\n                    if let Some(name) = path.file_name().and_then(|n| n.to_str()) {\n                        if name.starts_with('.') {\n                            continue;\n                        }\n                    }\n                    collect_all_project_files(&path, base, files)?;\n                } else if path.is_file() {\n                    // Compute relative path from project root\n                    if let Ok(rel) = path.strip_prefix(base) {\n                        files.push(rel.to_path_buf());\n                    }\n                }\n            }\n            Ok(())\n        }\n\n        let mut current_files = Vec::new();\n        let _ =\n            collect_all_project_files(&self.project_path, &self.project_path, &mut current_files);\n\n        // Create a set of files that should exist after restore\n        let mut checkpoint_files = std::collections::HashSet::new();\n        for snapshot in &file_snapshots {\n            if !snapshot.is_deleted {\n                checkpoint_files.insert(snapshot.file_path.clone());\n            }\n        }\n\n        // Delete files that exist now but shouldn't exist in the checkpoint\n        let mut warnings = Vec::new();\n        let mut files_processed = 0;\n\n        for current_file in current_files {\n            if !checkpoint_files.contains(&current_file) {\n                // This file exists now but not in the checkpoint, so delete it\n                let full_path = self.project_path.join(&current_file);\n                match fs::remove_file(&full_path) {\n                    Ok(_) => {\n                        files_processed += 1;\n                        log::info!(\"Deleted file not in checkpoint: {:?}\", current_file);\n                    }\n                    Err(e) => {\n                        warnings.push(format!(\n                            \"Failed to delete {}: {}\",\n                            current_file.display(),\n                            e\n                        ));\n                    }\n                }\n            }\n        }\n\n        // Clean up empty directories\n        fn remove_empty_dirs(\n            dir: &std::path::Path,\n            base: &std::path::Path,\n        ) -> Result<bool, std::io::Error> {\n            if dir == base {\n                return Ok(false); // Don't remove the base directory\n            }\n\n            let mut is_empty = true;\n            for entry in fs::read_dir(dir)? {\n                let entry = entry?;\n                let path = entry.path();\n                if path.is_dir() {\n                    if !remove_empty_dirs(&path, base)? {\n                        is_empty = false;\n                    }\n                } else {\n                    is_empty = false;\n                }\n            }\n\n            if is_empty {\n                fs::remove_dir(dir)?;\n                Ok(true)\n            } else {\n                Ok(false)\n            }\n        }\n\n        // Clean up any empty directories left after file deletion\n        let _ = remove_empty_dirs(&self.project_path, &self.project_path);\n\n        // Restore files from checkpoint\n        for snapshot in &file_snapshots {\n            match self.restore_file_snapshot(snapshot).await {\n                Ok(_) => files_processed += 1,\n                Err(e) => warnings.push(format!(\n                    \"Failed to restore {}: {}\",\n                    snapshot.file_path.display(),\n                    e\n                )),\n            }\n        }\n\n        // Update current messages\n        let mut current_messages = self.current_messages.write().await;\n        current_messages.clear();\n        for line in messages.lines() {\n            current_messages.push(line.to_string());\n        }\n\n        // Update timeline\n        let mut timeline = self.timeline.write().await;\n        timeline.current_checkpoint_id = Some(checkpoint_id.to_string());\n\n        // Update file tracker\n        let mut tracker = self.file_tracker.write().await;\n        tracker.tracked_files.clear();\n        for snapshot in &file_snapshots {\n            if !snapshot.is_deleted {\n                tracker.tracked_files.insert(\n                    snapshot.file_path.clone(),\n                    FileState {\n                        last_hash: snapshot.hash.clone(),\n                        is_modified: false,\n                        last_modified: Utc::now(),\n                        exists: true,\n                    },\n                );\n            }\n        }\n\n        Ok(CheckpointResult {\n            checkpoint: checkpoint.clone(),\n            files_processed,\n            warnings,\n        })\n    }\n\n    /// Restore a single file from snapshot\n    async fn restore_file_snapshot(&self, snapshot: &FileSnapshot) -> Result<()> {\n        let full_path = self.project_path.join(&snapshot.file_path);\n\n        if snapshot.is_deleted {\n            // Delete the file if it exists\n            if full_path.exists() {\n                fs::remove_file(&full_path).context(\"Failed to delete file\")?;\n            }\n        } else {\n            // Create parent directories if needed\n            if let Some(parent) = full_path.parent() {\n                fs::create_dir_all(parent).context(\"Failed to create parent directories\")?;\n            }\n\n            // Write file content\n            fs::write(&full_path, &snapshot.content).context(\"Failed to write file\")?;\n\n            // Restore permissions if available\n            #[cfg(unix)]\n            if let Some(mode) = snapshot.permissions {\n                use std::os::unix::fs::PermissionsExt;\n                let permissions = std::fs::Permissions::from_mode(mode);\n                fs::set_permissions(&full_path, permissions)\n                    .context(\"Failed to set file permissions\")?;\n            }\n        }\n\n        Ok(())\n    }\n\n    /// Get the current timeline\n    pub async fn get_timeline(&self) -> SessionTimeline {\n        self.timeline.read().await.clone()\n    }\n\n    /// List all checkpoints\n    pub async fn list_checkpoints(&self) -> Vec<Checkpoint> {\n        let timeline = self.timeline.read().await;\n        let mut checkpoints = Vec::new();\n\n        if let Some(root) = &timeline.root_node {\n            Self::collect_checkpoints_from_node(root, &mut checkpoints);\n        }\n\n        checkpoints\n    }\n\n    /// Recursively collect checkpoints from timeline tree\n    fn collect_checkpoints_from_node(\n        node: &super::TimelineNode,\n        checkpoints: &mut Vec<Checkpoint>,\n    ) {\n        checkpoints.push(node.checkpoint.clone());\n        for child in &node.children {\n            Self::collect_checkpoints_from_node(child, checkpoints);\n        }\n    }\n\n    /// Fork from a checkpoint\n    pub async fn fork_from_checkpoint(\n        &self,\n        checkpoint_id: &str,\n        description: Option<String>,\n    ) -> Result<CheckpointResult> {\n        // Load the checkpoint to fork from\n        let (_base_checkpoint, _, _) =\n            self.storage\n                .load_checkpoint(&self.project_id, &self.session_id, checkpoint_id)?;\n\n        // Restore to that checkpoint first\n        self.restore_checkpoint(checkpoint_id).await?;\n\n        // Create a new checkpoint with the fork\n        let fork_description =\n            description.unwrap_or_else(|| format!(\"Fork from checkpoint {}\", &checkpoint_id[..8]));\n\n        self.create_checkpoint(Some(fork_description), Some(checkpoint_id.to_string()))\n            .await\n    }\n\n    /// Check if auto-checkpoint should be triggered\n    pub async fn should_auto_checkpoint(&self, message: &str) -> bool {\n        let timeline = self.timeline.read().await;\n\n        if !timeline.auto_checkpoint_enabled {\n            return false;\n        }\n\n        match timeline.checkpoint_strategy {\n            CheckpointStrategy::Manual => false,\n            CheckpointStrategy::PerPrompt => {\n                // Check if message is a user prompt\n                if let Ok(msg) = serde_json::from_str::<serde_json::Value>(message) {\n                    msg.get(\"type\").and_then(|t| t.as_str()) == Some(\"user\")\n                } else {\n                    false\n                }\n            }\n            CheckpointStrategy::PerToolUse => {\n                // Check if message contains tool use\n                if let Ok(msg) = serde_json::from_str::<serde_json::Value>(message) {\n                    if let Some(content) = msg\n                        .get(\"message\")\n                        .and_then(|m| m.get(\"content\"))\n                        .and_then(|c| c.as_array())\n                    {\n                        content.iter().any(|item| {\n                            item.get(\"type\").and_then(|t| t.as_str()) == Some(\"tool_use\")\n                        })\n                    } else {\n                        false\n                    }\n                } else {\n                    false\n                }\n            }\n            CheckpointStrategy::Smart => {\n                // Smart strategy: checkpoint after destructive operations\n                if let Ok(msg) = serde_json::from_str::<serde_json::Value>(message) {\n                    if let Some(content) = msg\n                        .get(\"message\")\n                        .and_then(|m| m.get(\"content\"))\n                        .and_then(|c| c.as_array())\n                    {\n                        content.iter().any(|item| {\n                            if item.get(\"type\").and_then(|t| t.as_str()) == Some(\"tool_use\") {\n                                let tool_name =\n                                    item.get(\"name\").and_then(|n| n.as_str()).unwrap_or(\"\");\n                                matches!(\n                                    tool_name.to_lowercase().as_str(),\n                                    \"write\" | \"edit\" | \"multiedit\" | \"bash\" | \"rm\" | \"delete\"\n                                )\n                            } else {\n                                false\n                            }\n                        })\n                    } else {\n                        false\n                    }\n                } else {\n                    false\n                }\n            }\n        }\n    }\n\n    /// Update checkpoint settings\n    pub async fn update_settings(\n        &self,\n        auto_checkpoint_enabled: bool,\n        checkpoint_strategy: CheckpointStrategy,\n    ) -> Result<()> {\n        let mut timeline = self.timeline.write().await;\n        timeline.auto_checkpoint_enabled = auto_checkpoint_enabled;\n        timeline.checkpoint_strategy = checkpoint_strategy;\n\n        // Save updated timeline\n        let claude_dir = self.storage.claude_dir.clone();\n        let paths = CheckpointPaths::new(&claude_dir, &self.project_id, &self.session_id);\n        self.storage\n            .save_timeline(&paths.timeline_file, &timeline)?;\n\n        Ok(())\n    }\n\n    /// Get files modified since a given timestamp\n    pub async fn get_files_modified_since(&self, since: DateTime<Utc>) -> Vec<PathBuf> {\n        let tracker = self.file_tracker.read().await;\n        tracker\n            .tracked_files\n            .iter()\n            .filter(|(_, state)| state.last_modified > since && state.is_modified)\n            .map(|(path, _)| path.clone())\n            .collect()\n    }\n\n    /// Get the last modification time of any tracked file\n    pub async fn get_last_modification_time(&self) -> Option<DateTime<Utc>> {\n        let tracker = self.file_tracker.read().await;\n        tracker\n            .tracked_files\n            .values()\n            .map(|state| state.last_modified)\n            .max()\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/checkpoint/mod.rs",
    "content": "use chrono::{DateTime, Utc};\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse std::path::PathBuf;\n\npub mod manager;\npub mod state;\npub mod storage;\n\n/// Represents a checkpoint in the session timeline\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Checkpoint {\n    /// Unique identifier for the checkpoint\n    pub id: String,\n    /// Session ID this checkpoint belongs to\n    pub session_id: String,\n    /// Project ID for the session\n    pub project_id: String,\n    /// Index of the last message in this checkpoint\n    pub message_index: usize,\n    /// Timestamp when checkpoint was created\n    pub timestamp: DateTime<Utc>,\n    /// User-provided description\n    pub description: Option<String>,\n    /// Parent checkpoint ID for fork tracking\n    pub parent_checkpoint_id: Option<String>,\n    /// Metadata about the checkpoint\n    pub metadata: CheckpointMetadata,\n}\n\n/// Metadata associated with a checkpoint\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CheckpointMetadata {\n    /// Total tokens used up to this point\n    pub total_tokens: u64,\n    /// Model used for the last operation\n    pub model_used: String,\n    /// The user prompt that led to this state\n    pub user_prompt: String,\n    /// Number of file changes in this checkpoint\n    pub file_changes: usize,\n    /// Size of all file snapshots in bytes\n    pub snapshot_size: u64,\n}\n\n/// Represents a snapshot of a file at a checkpoint\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct FileSnapshot {\n    /// Checkpoint this snapshot belongs to\n    pub checkpoint_id: String,\n    /// Relative path from project root\n    pub file_path: PathBuf,\n    /// Full content of the file (will be compressed)\n    pub content: String,\n    /// SHA-256 hash for integrity verification\n    pub hash: String,\n    /// Whether this file was deleted at this checkpoint\n    pub is_deleted: bool,\n    /// File permissions (Unix mode)\n    pub permissions: Option<u32>,\n    /// File size in bytes\n    pub size: u64,\n}\n\n/// Represents a node in the timeline tree\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct TimelineNode {\n    /// The checkpoint at this node\n    pub checkpoint: Checkpoint,\n    /// Child nodes (for branches/forks)\n    pub children: Vec<TimelineNode>,\n    /// IDs of file snapshots associated with this checkpoint\n    pub file_snapshot_ids: Vec<String>,\n}\n\n/// The complete timeline for a session\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SessionTimeline {\n    /// Session ID this timeline belongs to\n    pub session_id: String,\n    /// Root node of the timeline tree\n    pub root_node: Option<TimelineNode>,\n    /// ID of the current active checkpoint\n    pub current_checkpoint_id: Option<String>,\n    /// Whether auto-checkpointing is enabled\n    pub auto_checkpoint_enabled: bool,\n    /// Strategy for automatic checkpoints\n    pub checkpoint_strategy: CheckpointStrategy,\n    /// Total number of checkpoints in timeline\n    pub total_checkpoints: usize,\n}\n\n/// Strategy for automatic checkpoint creation\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum CheckpointStrategy {\n    /// Only create checkpoints manually\n    Manual,\n    /// Create checkpoint after each user prompt\n    PerPrompt,\n    /// Create checkpoint after each tool use\n    PerToolUse,\n    /// Create checkpoint after destructive operations\n    Smart,\n}\n\n/// Tracks the state of files for checkpointing\n#[derive(Debug, Clone)]\npub struct FileTracker {\n    /// Map of file paths to their current state\n    pub tracked_files: HashMap<PathBuf, FileState>,\n}\n\n/// State of a tracked file\n#[derive(Debug, Clone)]\npub struct FileState {\n    /// Last known hash of the file\n    pub last_hash: String,\n    /// Whether the file has been modified since last checkpoint\n    pub is_modified: bool,\n    /// Last modification timestamp\n    pub last_modified: DateTime<Utc>,\n    /// Whether the file currently exists\n    pub exists: bool,\n}\n\n/// Result of a checkpoint operation\n#[derive(Debug, Serialize, Deserialize)]\npub struct CheckpointResult {\n    /// The created/restored checkpoint\n    pub checkpoint: Checkpoint,\n    /// Number of files snapshot/restored\n    pub files_processed: usize,\n    /// Any warnings during the operation\n    pub warnings: Vec<String>,\n}\n\n/// Diff between two checkpoints\n#[derive(Debug, Serialize, Deserialize)]\npub struct CheckpointDiff {\n    /// Source checkpoint ID\n    pub from_checkpoint_id: String,\n    /// Target checkpoint ID  \n    pub to_checkpoint_id: String,\n    /// Files that were modified\n    pub modified_files: Vec<FileDiff>,\n    /// Files that were added\n    pub added_files: Vec<PathBuf>,\n    /// Files that were deleted\n    pub deleted_files: Vec<PathBuf>,\n    /// Token usage difference\n    pub token_delta: i64,\n}\n\n/// Diff for a single file\n#[derive(Debug, Serialize, Deserialize)]\npub struct FileDiff {\n    /// File path\n    pub path: PathBuf,\n    /// Number of additions\n    pub additions: usize,\n    /// Number of deletions\n    pub deletions: usize,\n    /// Unified diff content (optional)\n    pub diff_content: Option<String>,\n}\n\nimpl Default for CheckpointStrategy {\n    fn default() -> Self {\n        CheckpointStrategy::Smart\n    }\n}\n\nimpl SessionTimeline {\n    /// Create a new empty timeline\n    pub fn new(session_id: String) -> Self {\n        Self {\n            session_id,\n            root_node: None,\n            current_checkpoint_id: None,\n            auto_checkpoint_enabled: false,\n            checkpoint_strategy: CheckpointStrategy::default(),\n            total_checkpoints: 0,\n        }\n    }\n\n    /// Find a checkpoint by ID in the timeline tree\n    pub fn find_checkpoint(&self, checkpoint_id: &str) -> Option<&TimelineNode> {\n        self.root_node\n            .as_ref()\n            .and_then(|root| Self::find_in_tree(root, checkpoint_id))\n    }\n\n    fn find_in_tree<'a>(node: &'a TimelineNode, checkpoint_id: &str) -> Option<&'a TimelineNode> {\n        if node.checkpoint.id == checkpoint_id {\n            return Some(node);\n        }\n\n        for child in &node.children {\n            if let Some(found) = Self::find_in_tree(child, checkpoint_id) {\n                return Some(found);\n            }\n        }\n\n        None\n    }\n}\n\n/// Checkpoint storage paths\npub struct CheckpointPaths {\n    pub timeline_file: PathBuf,\n    pub checkpoints_dir: PathBuf,\n    pub files_dir: PathBuf,\n}\n\nimpl CheckpointPaths {\n    pub fn new(claude_dir: &PathBuf, project_id: &str, session_id: &str) -> Self {\n        let base_dir = claude_dir\n            .join(\"projects\")\n            .join(project_id)\n            .join(\".timelines\")\n            .join(session_id);\n\n        Self {\n            timeline_file: base_dir.join(\"timeline.json\"),\n            checkpoints_dir: base_dir.join(\"checkpoints\"),\n            files_dir: base_dir.join(\"files\"),\n        }\n    }\n\n    pub fn checkpoint_dir(&self, checkpoint_id: &str) -> PathBuf {\n        self.checkpoints_dir.join(checkpoint_id)\n    }\n\n    pub fn checkpoint_metadata_file(&self, checkpoint_id: &str) -> PathBuf {\n        self.checkpoint_dir(checkpoint_id).join(\"metadata.json\")\n    }\n\n    pub fn checkpoint_messages_file(&self, checkpoint_id: &str) -> PathBuf {\n        self.checkpoint_dir(checkpoint_id).join(\"messages.jsonl\")\n    }\n\n    #[allow(dead_code)]\n    pub fn file_snapshot_path(&self, _checkpoint_id: &str, file_hash: &str) -> PathBuf {\n        // In content-addressable storage, files are stored by hash in the content pool\n        self.files_dir.join(\"content_pool\").join(file_hash)\n    }\n\n    #[allow(dead_code)]\n    pub fn file_reference_path(&self, checkpoint_id: &str, safe_filename: &str) -> PathBuf {\n        // References are stored per checkpoint\n        self.files_dir\n            .join(\"refs\")\n            .join(checkpoint_id)\n            .join(format!(\"{}.json\", safe_filename))\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/checkpoint/state.rs",
    "content": "use anyhow::Result;\nuse std::collections::HashMap;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse tokio::sync::RwLock;\n\nuse super::manager::CheckpointManager;\n\n/// Manages checkpoint managers for active sessions\n///\n/// This struct maintains a stateful collection of CheckpointManager instances,\n/// one per active session, to avoid recreating them on every command invocation.\n/// It provides thread-safe access to managers and handles their lifecycle.\n#[derive(Default, Clone)]\npub struct CheckpointState {\n    /// Map of session_id to CheckpointManager\n    /// Uses Arc<CheckpointManager> to allow sharing across async boundaries\n    managers: Arc<RwLock<HashMap<String, Arc<CheckpointManager>>>>,\n    /// The Claude directory path for consistent access\n    claude_dir: Arc<RwLock<Option<PathBuf>>>,\n}\n\nimpl CheckpointState {\n    /// Creates a new CheckpointState instance\n    pub fn new() -> Self {\n        Self {\n            managers: Arc::new(RwLock::new(HashMap::new())),\n            claude_dir: Arc::new(RwLock::new(None)),\n        }\n    }\n\n    /// Sets the Claude directory path\n    ///\n    /// This should be called once during application initialization\n    pub async fn set_claude_dir(&self, claude_dir: PathBuf) {\n        let mut dir = self.claude_dir.write().await;\n        *dir = Some(claude_dir);\n    }\n\n    /// Gets or creates a CheckpointManager for a session\n    ///\n    /// If a manager already exists for the session, it returns the existing one.\n    /// Otherwise, it creates a new manager and stores it for future use.\n    ///\n    /// # Arguments\n    /// * `session_id` - The session identifier\n    /// * `project_id` - The project identifier\n    /// * `project_path` - The path to the project directory\n    ///\n    /// # Returns\n    /// An Arc reference to the CheckpointManager for thread-safe sharing\n    pub async fn get_or_create_manager(\n        &self,\n        session_id: String,\n        project_id: String,\n        project_path: PathBuf,\n    ) -> Result<Arc<CheckpointManager>> {\n        let mut managers = self.managers.write().await;\n\n        // Check if manager already exists\n        if let Some(manager) = managers.get(&session_id) {\n            return Ok(Arc::clone(manager));\n        }\n\n        // Get Claude directory\n        let claude_dir = {\n            let dir = self.claude_dir.read().await;\n            dir.as_ref()\n                .ok_or_else(|| anyhow::anyhow!(\"Claude directory not set\"))?\n                .clone()\n        };\n\n        // Create new manager\n        let manager =\n            CheckpointManager::new(project_id, session_id.clone(), project_path, claude_dir)\n                .await?;\n\n        let manager_arc = Arc::new(manager);\n        managers.insert(session_id, Arc::clone(&manager_arc));\n\n        Ok(manager_arc)\n    }\n\n    /// Gets an existing CheckpointManager for a session\n    ///\n    /// Returns None if no manager exists for the session\n    #[allow(dead_code)]\n    pub async fn get_manager(&self, session_id: &str) -> Option<Arc<CheckpointManager>> {\n        let managers = self.managers.read().await;\n        managers.get(session_id).map(Arc::clone)\n    }\n\n    /// Removes a CheckpointManager for a session\n    ///\n    /// This should be called when a session ends to free resources\n    pub async fn remove_manager(&self, session_id: &str) -> Option<Arc<CheckpointManager>> {\n        let mut managers = self.managers.write().await;\n        managers.remove(session_id)\n    }\n\n    /// Clears all managers\n    ///\n    /// This is useful for cleanup during application shutdown\n    #[allow(dead_code)]\n    pub async fn clear_all(&self) {\n        let mut managers = self.managers.write().await;\n        managers.clear();\n    }\n\n    /// Gets the number of active managers\n    pub async fn active_count(&self) -> usize {\n        let managers = self.managers.read().await;\n        managers.len()\n    }\n\n    /// Lists all active session IDs\n    pub async fn list_active_sessions(&self) -> Vec<String> {\n        let managers = self.managers.read().await;\n        managers.keys().cloned().collect()\n    }\n\n    /// Checks if a session has an active manager\n    #[allow(dead_code)]\n    pub async fn has_active_manager(&self, session_id: &str) -> bool {\n        self.get_manager(session_id).await.is_some()\n    }\n\n    /// Clears all managers and returns the count that were cleared\n    #[allow(dead_code)]\n    pub async fn clear_all_and_count(&self) -> usize {\n        let count = self.active_count().await;\n        self.clear_all().await;\n        count\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tempfile::TempDir;\n\n    #[tokio::test]\n    async fn test_checkpoint_state_lifecycle() {\n        let state = CheckpointState::new();\n        let temp_dir = TempDir::new().unwrap();\n        let claude_dir = temp_dir.path().to_path_buf();\n\n        // Set Claude directory\n        state.set_claude_dir(claude_dir.clone()).await;\n\n        // Create a manager\n        let session_id = \"test-session-123\".to_string();\n        let project_id = \"test-project\".to_string();\n        let project_path = temp_dir.path().join(\"project\");\n        std::fs::create_dir_all(&project_path).unwrap();\n\n        let manager1 = state\n            .get_or_create_manager(session_id.clone(), project_id.clone(), project_path.clone())\n            .await\n            .unwrap();\n\n        // Getting the same session should return the same manager\n        let manager2 = state\n            .get_or_create_manager(session_id.clone(), project_id.clone(), project_path.clone())\n            .await\n            .unwrap();\n\n        assert!(Arc::ptr_eq(&manager1, &manager2));\n        assert_eq!(state.active_count().await, 1);\n\n        // Remove the manager\n        let removed = state.remove_manager(&session_id).await;\n        assert!(removed.is_some());\n        assert_eq!(state.active_count().await, 0);\n\n        // Getting after removal should create a new one\n        let manager3 = state\n            .get_or_create_manager(session_id.clone(), project_id, project_path)\n            .await\n            .unwrap();\n\n        assert!(!Arc::ptr_eq(&manager1, &manager3));\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/checkpoint/storage.rs",
    "content": "use anyhow::{Context, Result};\nuse sha2::{Digest, Sha256};\nuse std::fs;\nuse std::path::{Path, PathBuf};\nuse uuid::Uuid;\nuse zstd::stream::{decode_all, encode_all};\n\nuse super::{\n    Checkpoint, CheckpointPaths, CheckpointResult, FileSnapshot, SessionTimeline, TimelineNode,\n};\n\n/// Manages checkpoint storage operations\npub struct CheckpointStorage {\n    pub claude_dir: PathBuf,\n    compression_level: i32,\n}\n\nimpl CheckpointStorage {\n    /// Create a new checkpoint storage instance\n    pub fn new(claude_dir: PathBuf) -> Self {\n        Self {\n            claude_dir,\n            compression_level: 3, // Default zstd compression level\n        }\n    }\n\n    /// Initialize checkpoint storage for a session\n    pub fn init_storage(&self, project_id: &str, session_id: &str) -> Result<()> {\n        let paths = CheckpointPaths::new(&self.claude_dir, project_id, session_id);\n\n        // Create directory structure\n        fs::create_dir_all(&paths.checkpoints_dir)\n            .context(\"Failed to create checkpoints directory\")?;\n        fs::create_dir_all(&paths.files_dir).context(\"Failed to create files directory\")?;\n\n        // Initialize empty timeline if it doesn't exist\n        if !paths.timeline_file.exists() {\n            let timeline = SessionTimeline::new(session_id.to_string());\n            self.save_timeline(&paths.timeline_file, &timeline)?;\n        }\n\n        Ok(())\n    }\n\n    /// Save a checkpoint to disk\n    pub fn save_checkpoint(\n        &self,\n        project_id: &str,\n        session_id: &str,\n        checkpoint: &Checkpoint,\n        file_snapshots: Vec<FileSnapshot>,\n        messages: &str, // JSONL content up to checkpoint\n    ) -> Result<CheckpointResult> {\n        let paths = CheckpointPaths::new(&self.claude_dir, project_id, session_id);\n        let checkpoint_dir = paths.checkpoint_dir(&checkpoint.id);\n\n        // Create checkpoint directory\n        fs::create_dir_all(&checkpoint_dir).context(\"Failed to create checkpoint directory\")?;\n\n        // Save checkpoint metadata\n        let metadata_path = paths.checkpoint_metadata_file(&checkpoint.id);\n        let metadata_json = serde_json::to_string_pretty(checkpoint)\n            .context(\"Failed to serialize checkpoint metadata\")?;\n        fs::write(&metadata_path, metadata_json).context(\"Failed to write checkpoint metadata\")?;\n\n        // Save messages (compressed)\n        let messages_path = paths.checkpoint_messages_file(&checkpoint.id);\n        let compressed_messages = encode_all(messages.as_bytes(), self.compression_level)\n            .context(\"Failed to compress messages\")?;\n        fs::write(&messages_path, compressed_messages)\n            .context(\"Failed to write compressed messages\")?;\n\n        // Save file snapshots\n        let mut warnings = Vec::new();\n        let mut files_processed = 0;\n\n        for snapshot in &file_snapshots {\n            match self.save_file_snapshot(&paths, snapshot) {\n                Ok(_) => files_processed += 1,\n                Err(e) => warnings.push(format!(\n                    \"Failed to save {}: {}\",\n                    snapshot.file_path.display(),\n                    e\n                )),\n            }\n        }\n\n        // Update timeline\n        self.update_timeline_with_checkpoint(&paths.timeline_file, checkpoint, &file_snapshots)?;\n\n        Ok(CheckpointResult {\n            checkpoint: checkpoint.clone(),\n            files_processed,\n            warnings,\n        })\n    }\n\n    /// Save a single file snapshot\n    fn save_file_snapshot(&self, paths: &CheckpointPaths, snapshot: &FileSnapshot) -> Result<()> {\n        // Use content-addressable storage: store files by their hash\n        // This prevents duplication of identical file content across checkpoints\n        let content_pool_dir = paths.files_dir.join(\"content_pool\");\n        fs::create_dir_all(&content_pool_dir).context(\"Failed to create content pool directory\")?;\n\n        // Store the actual content in the content pool\n        let content_file = content_pool_dir.join(&snapshot.hash);\n\n        // Only write the content if it doesn't already exist\n        if !content_file.exists() {\n            // Compress and save file content\n            let compressed_content =\n                encode_all(snapshot.content.as_bytes(), self.compression_level)\n                    .context(\"Failed to compress file content\")?;\n            fs::write(&content_file, compressed_content)\n                .context(\"Failed to write file content to pool\")?;\n        }\n\n        // Create a reference in the checkpoint-specific directory\n        let checkpoint_refs_dir = paths.files_dir.join(\"refs\").join(&snapshot.checkpoint_id);\n        fs::create_dir_all(&checkpoint_refs_dir)\n            .context(\"Failed to create checkpoint refs directory\")?;\n\n        // Save file metadata with reference to content\n        let ref_metadata = serde_json::json!({\n            \"path\": snapshot.file_path,\n            \"hash\": snapshot.hash,\n            \"is_deleted\": snapshot.is_deleted,\n            \"permissions\": snapshot.permissions,\n            \"size\": snapshot.size,\n        });\n\n        // Use a sanitized filename for the reference\n        let safe_filename = snapshot\n            .file_path\n            .to_string_lossy()\n            .replace('/', \"_\")\n            .replace('\\\\', \"_\");\n        let ref_path = checkpoint_refs_dir.join(format!(\"{}.json\", safe_filename));\n\n        fs::write(&ref_path, serde_json::to_string_pretty(&ref_metadata)?)\n            .context(\"Failed to write file reference\")?;\n\n        Ok(())\n    }\n\n    /// Load a checkpoint from disk\n    pub fn load_checkpoint(\n        &self,\n        project_id: &str,\n        session_id: &str,\n        checkpoint_id: &str,\n    ) -> Result<(Checkpoint, Vec<FileSnapshot>, String)> {\n        let paths = CheckpointPaths::new(&self.claude_dir, project_id, session_id);\n\n        // Load checkpoint metadata\n        let metadata_path = paths.checkpoint_metadata_file(checkpoint_id);\n        let metadata_json =\n            fs::read_to_string(&metadata_path).context(\"Failed to read checkpoint metadata\")?;\n        let checkpoint: Checkpoint =\n            serde_json::from_str(&metadata_json).context(\"Failed to parse checkpoint metadata\")?;\n\n        // Load messages\n        let messages_path = paths.checkpoint_messages_file(checkpoint_id);\n        let compressed_messages =\n            fs::read(&messages_path).context(\"Failed to read compressed messages\")?;\n        let messages = String::from_utf8(\n            decode_all(&compressed_messages[..]).context(\"Failed to decompress messages\")?,\n        )\n        .context(\"Invalid UTF-8 in messages\")?;\n\n        // Load file snapshots\n        let file_snapshots = self.load_file_snapshots(&paths, checkpoint_id)?;\n\n        Ok((checkpoint, file_snapshots, messages))\n    }\n\n    /// Load all file snapshots for a checkpoint\n    fn load_file_snapshots(\n        &self,\n        paths: &CheckpointPaths,\n        checkpoint_id: &str,\n    ) -> Result<Vec<FileSnapshot>> {\n        let refs_dir = paths.files_dir.join(\"refs\").join(checkpoint_id);\n        if !refs_dir.exists() {\n            return Ok(Vec::new());\n        }\n\n        let content_pool_dir = paths.files_dir.join(\"content_pool\");\n        let mut snapshots = Vec::new();\n\n        // Read all reference files\n        for entry in fs::read_dir(&refs_dir)? {\n            let entry = entry?;\n            let path = entry.path();\n\n            // Skip non-JSON files\n            if path.extension().and_then(|e| e.to_str()) != Some(\"json\") {\n                continue;\n            }\n\n            // Load reference metadata\n            let ref_json = fs::read_to_string(&path).context(\"Failed to read file reference\")?;\n            let ref_metadata: serde_json::Value =\n                serde_json::from_str(&ref_json).context(\"Failed to parse file reference\")?;\n\n            let hash = ref_metadata[\"hash\"]\n                .as_str()\n                .ok_or_else(|| anyhow::anyhow!(\"Missing hash in reference\"))?;\n\n            // Load content from pool\n            let content_file = content_pool_dir.join(hash);\n            let content = if content_file.exists() {\n                let compressed_content =\n                    fs::read(&content_file).context(\"Failed to read file content from pool\")?;\n                String::from_utf8(\n                    decode_all(&compressed_content[..])\n                        .context(\"Failed to decompress file content\")?,\n                )\n                .context(\"Invalid UTF-8 in file content\")?\n            } else {\n                // Handle missing content gracefully\n                log::warn!(\"Content file missing for hash: {}\", hash);\n                String::new()\n            };\n\n            snapshots.push(FileSnapshot {\n                checkpoint_id: checkpoint_id.to_string(),\n                file_path: PathBuf::from(ref_metadata[\"path\"].as_str().unwrap_or(\"\")),\n                content,\n                hash: hash.to_string(),\n                is_deleted: ref_metadata[\"is_deleted\"].as_bool().unwrap_or(false),\n                permissions: ref_metadata[\"permissions\"].as_u64().map(|p| p as u32),\n                size: ref_metadata[\"size\"].as_u64().unwrap_or(0),\n            });\n        }\n\n        Ok(snapshots)\n    }\n\n    /// Save timeline to disk\n    pub fn save_timeline(&self, timeline_path: &Path, timeline: &SessionTimeline) -> Result<()> {\n        let timeline_json =\n            serde_json::to_string_pretty(timeline).context(\"Failed to serialize timeline\")?;\n        fs::write(timeline_path, timeline_json).context(\"Failed to write timeline\")?;\n        Ok(())\n    }\n\n    /// Load timeline from disk\n    pub fn load_timeline(&self, timeline_path: &Path) -> Result<SessionTimeline> {\n        let timeline_json = fs::read_to_string(timeline_path).context(\"Failed to read timeline\")?;\n        let timeline: SessionTimeline =\n            serde_json::from_str(&timeline_json).context(\"Failed to parse timeline\")?;\n        Ok(timeline)\n    }\n\n    /// Update timeline with a new checkpoint\n    fn update_timeline_with_checkpoint(\n        &self,\n        timeline_path: &Path,\n        checkpoint: &Checkpoint,\n        file_snapshots: &[FileSnapshot],\n    ) -> Result<()> {\n        let mut timeline = self.load_timeline(timeline_path)?;\n\n        let new_node = TimelineNode {\n            checkpoint: checkpoint.clone(),\n            children: Vec::new(),\n            file_snapshot_ids: file_snapshots.iter().map(|s| s.hash.clone()).collect(),\n        };\n\n        // If this is the first checkpoint\n        if timeline.root_node.is_none() {\n            timeline.root_node = Some(new_node);\n            timeline.current_checkpoint_id = Some(checkpoint.id.clone());\n        } else if let Some(parent_id) = &checkpoint.parent_checkpoint_id {\n            // Check if parent exists before modifying\n            let parent_exists = timeline.find_checkpoint(parent_id).is_some();\n\n            if parent_exists {\n                if let Some(root) = &mut timeline.root_node {\n                    Self::add_child_to_node(root, parent_id, new_node)?;\n                    timeline.current_checkpoint_id = Some(checkpoint.id.clone());\n                }\n            } else {\n                anyhow::bail!(\"Parent checkpoint not found: {}\", parent_id);\n            }\n        }\n\n        timeline.total_checkpoints += 1;\n        self.save_timeline(timeline_path, &timeline)?;\n\n        Ok(())\n    }\n\n    /// Recursively add a child node to the timeline tree\n    fn add_child_to_node(\n        node: &mut TimelineNode,\n        parent_id: &str,\n        child: TimelineNode,\n    ) -> Result<()> {\n        if node.checkpoint.id == parent_id {\n            node.children.push(child);\n            return Ok(());\n        }\n\n        for child_node in &mut node.children {\n            if Self::add_child_to_node(child_node, parent_id, child.clone()).is_ok() {\n                return Ok(());\n            }\n        }\n\n        anyhow::bail!(\"Parent checkpoint not found: {}\", parent_id)\n    }\n\n    /// Calculate hash of file content\n    pub fn calculate_file_hash(content: &str) -> String {\n        let mut hasher = Sha256::new();\n        hasher.update(content.as_bytes());\n        format!(\"{:x}\", hasher.finalize())\n    }\n\n    /// Generate a new checkpoint ID\n    pub fn generate_checkpoint_id() -> String {\n        Uuid::new_v4().to_string()\n    }\n\n    /// Estimate storage size for a checkpoint\n    pub fn estimate_checkpoint_size(messages: &str, file_snapshots: &[FileSnapshot]) -> u64 {\n        let messages_size = messages.len() as u64;\n        let files_size: u64 = file_snapshots.iter().map(|s| s.content.len() as u64).sum();\n\n        // Estimate compressed size (typically 20-30% of original for text)\n        (messages_size + files_size) / 4\n    }\n\n    /// Clean up old checkpoints based on retention policy\n    pub fn cleanup_old_checkpoints(\n        &self,\n        project_id: &str,\n        session_id: &str,\n        keep_count: usize,\n    ) -> Result<usize> {\n        let paths = CheckpointPaths::new(&self.claude_dir, project_id, session_id);\n        let timeline = self.load_timeline(&paths.timeline_file)?;\n\n        // Collect all checkpoint IDs in chronological order\n        let mut all_checkpoints = Vec::new();\n        if let Some(root) = &timeline.root_node {\n            Self::collect_checkpoints(root, &mut all_checkpoints);\n        }\n\n        // Sort by timestamp (oldest first)\n        all_checkpoints.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));\n\n        // Keep only the most recent checkpoints\n        let to_remove = all_checkpoints.len().saturating_sub(keep_count);\n        let mut removed_count = 0;\n\n        for checkpoint in all_checkpoints.into_iter().take(to_remove) {\n            if self.remove_checkpoint(&paths, &checkpoint.id).is_ok() {\n                removed_count += 1;\n            }\n        }\n\n        // Run garbage collection to clean up orphaned content\n        if removed_count > 0 {\n            match self.garbage_collect_content(project_id, session_id) {\n                Ok(gc_count) => {\n                    log::info!(\"Garbage collected {} orphaned content files\", gc_count);\n                }\n                Err(e) => {\n                    log::warn!(\"Failed to garbage collect content: {}\", e);\n                }\n            }\n        }\n\n        Ok(removed_count)\n    }\n\n    /// Collect all checkpoints from the tree in order\n    fn collect_checkpoints(node: &TimelineNode, checkpoints: &mut Vec<Checkpoint>) {\n        checkpoints.push(node.checkpoint.clone());\n        for child in &node.children {\n            Self::collect_checkpoints(child, checkpoints);\n        }\n    }\n\n    /// Remove a checkpoint and its associated files\n    fn remove_checkpoint(&self, paths: &CheckpointPaths, checkpoint_id: &str) -> Result<()> {\n        // Remove checkpoint metadata directory\n        let checkpoint_dir = paths.checkpoint_dir(checkpoint_id);\n        if checkpoint_dir.exists() {\n            fs::remove_dir_all(&checkpoint_dir).context(\"Failed to remove checkpoint directory\")?;\n        }\n\n        // Remove file references for this checkpoint\n        let refs_dir = paths.files_dir.join(\"refs\").join(checkpoint_id);\n        if refs_dir.exists() {\n            fs::remove_dir_all(&refs_dir).context(\"Failed to remove file references\")?;\n        }\n\n        // Note: We don't remove content from the pool here as it might be\n        // referenced by other checkpoints. Use garbage_collect_content() for that.\n\n        Ok(())\n    }\n\n    /// Garbage collect unreferenced content from the content pool\n    pub fn garbage_collect_content(&self, project_id: &str, session_id: &str) -> Result<usize> {\n        let paths = CheckpointPaths::new(&self.claude_dir, project_id, session_id);\n        let content_pool_dir = paths.files_dir.join(\"content_pool\");\n        let refs_dir = paths.files_dir.join(\"refs\");\n\n        if !content_pool_dir.exists() {\n            return Ok(0);\n        }\n\n        // Collect all referenced hashes\n        let mut referenced_hashes = std::collections::HashSet::new();\n\n        if refs_dir.exists() {\n            for checkpoint_entry in fs::read_dir(&refs_dir)? {\n                let checkpoint_dir = checkpoint_entry?.path();\n                if checkpoint_dir.is_dir() {\n                    for ref_entry in fs::read_dir(&checkpoint_dir)? {\n                        let ref_path = ref_entry?.path();\n                        if ref_path.extension().and_then(|e| e.to_str()) == Some(\"json\") {\n                            if let Ok(ref_json) = fs::read_to_string(&ref_path) {\n                                if let Ok(ref_metadata) =\n                                    serde_json::from_str::<serde_json::Value>(&ref_json)\n                                {\n                                    if let Some(hash) = ref_metadata[\"hash\"].as_str() {\n                                        referenced_hashes.insert(hash.to_string());\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        // Remove unreferenced content\n        let mut removed_count = 0;\n        for entry in fs::read_dir(&content_pool_dir)? {\n            let content_file = entry?.path();\n            if content_file.is_file() {\n                if let Some(hash) = content_file.file_name().and_then(|n| n.to_str()) {\n                    if !referenced_hashes.contains(hash) {\n                        if fs::remove_file(&content_file).is_ok() {\n                            removed_count += 1;\n                        }\n                    }\n                }\n            }\n        }\n\n        Ok(removed_count)\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/claude_binary.rs",
    "content": "use anyhow::Result;\nuse log::{debug, error, info, warn};\nuse serde::{Deserialize, Serialize};\nuse std::cmp::Ordering;\n/// Shared module for detecting Claude Code binary installations\n/// Supports NVM installations, aliased paths, and version-based selection\nuse std::path::PathBuf;\nuse std::process::Command;\nuse tauri::Manager;\n\n/// Type of Claude installation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\npub enum InstallationType {\n    /// System-installed binary\n    System,\n    /// Custom path specified by user\n    Custom,\n}\n\n/// Represents a Claude installation with metadata\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ClaudeInstallation {\n    /// Full path to the Claude binary\n    pub path: String,\n    /// Version string if available\n    pub version: Option<String>,\n    /// Source of discovery (e.g., \"nvm\", \"system\", \"homebrew\", \"which\")\n    pub source: String,\n    /// Type of installation\n    pub installation_type: InstallationType,\n}\n\n/// Main function to find the Claude binary\n/// Checks database first for stored path and preference, then prioritizes accordingly\npub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, String> {\n    info!(\"Searching for claude binary...\");\n\n    // First check if we have a stored path and preference in the database\n    if let Ok(app_data_dir) = app_handle.path().app_data_dir() {\n        let db_path = app_data_dir.join(\"agents.db\");\n        if db_path.exists() {\n            if let Ok(conn) = rusqlite::Connection::open(&db_path) {\n                // Check for stored path first\n                if let Ok(stored_path) = conn.query_row(\n                    \"SELECT value FROM app_settings WHERE key = 'claude_binary_path'\",\n                    [],\n                    |row| row.get::<_, String>(0),\n                ) {\n                    info!(\"Found stored claude path in database: {}\", stored_path);\n\n                    // Check if the path still exists\n                    let path_buf = PathBuf::from(&stored_path);\n                    if path_buf.exists() && path_buf.is_file() {\n                        return Ok(stored_path);\n                    } else {\n                        warn!(\"Stored claude path no longer exists: {}\", stored_path);\n                    }\n                }\n\n                // Check user preference\n                let preference = conn.query_row(\n                    \"SELECT value FROM app_settings WHERE key = 'claude_installation_preference'\",\n                    [],\n                    |row| row.get::<_, String>(0),\n                ).unwrap_or_else(|_| \"system\".to_string());\n\n                info!(\"User preference for Claude installation: {}\", preference);\n            }\n        }\n    }\n\n    // Discover all available system installations\n    let installations = discover_system_installations();\n\n    if installations.is_empty() {\n        error!(\"Could not find claude binary in any location\");\n        return Err(\"Claude Code not found. Please ensure it's installed in one of these locations: PATH, /usr/local/bin, /opt/homebrew/bin, ~/.nvm/versions/node/*/bin, ~/.claude/local, ~/.local/bin\".to_string());\n    }\n\n    // Log all found installations\n    for installation in &installations {\n        info!(\"Found Claude installation: {:?}\", installation);\n    }\n\n    // Select the best installation (highest version)\n    if let Some(best) = select_best_installation(installations) {\n        info!(\n            \"Selected Claude installation: path={}, version={:?}, source={}\",\n            best.path, best.version, best.source\n        );\n        Ok(best.path)\n    } else {\n        Err(\"No valid Claude installation found\".to_string())\n    }\n}\n\n/// Discovers all available Claude installations and returns them for selection\n/// This allows UI to show a version selector\npub fn discover_claude_installations() -> Vec<ClaudeInstallation> {\n    info!(\"Discovering all Claude installations...\");\n\n    let mut installations = discover_system_installations();\n\n    // Sort by version (highest first), then by source preference\n    installations.sort_by(|a, b| {\n        match (&a.version, &b.version) {\n            (Some(v1), Some(v2)) => {\n                // Compare versions in descending order (newest first)\n                match compare_versions(v2, v1) {\n                    Ordering::Equal => {\n                        // If versions are equal, prefer by source\n                        source_preference(a).cmp(&source_preference(b))\n                    }\n                    other => other,\n                }\n            }\n            (Some(_), None) => Ordering::Less, // Version comes before no version\n            (None, Some(_)) => Ordering::Greater,\n            (None, None) => source_preference(a).cmp(&source_preference(b)),\n        }\n    });\n\n    installations\n}\n\n/// Returns a preference score for installation sources (lower is better)\nfn source_preference(installation: &ClaudeInstallation) -> u8 {\n    match installation.source.as_str() {\n        \"which\" => 1,\n        \"homebrew\" => 2,\n        \"system\" => 3,\n        \"nvm-active\" => 4,\n        source if source.starts_with(\"nvm\") => 5,\n        \"local-bin\" => 6,\n        \"claude-local\" => 7,\n        \"npm-global\" => 8,\n        \"yarn\" | \"yarn-global\" => 9,\n        \"bun\" => 10,\n        \"node-modules\" => 11,\n        \"home-bin\" => 12,\n        \"PATH\" => 13,\n        _ => 14,\n    }\n}\n\n/// Discovers all Claude installations on the system\nfn discover_system_installations() -> Vec<ClaudeInstallation> {\n    let mut installations = Vec::new();\n\n    // 1. Try 'which' command first (now works in production)\n    if let Some(installation) = try_which_command() {\n        installations.push(installation);\n    }\n\n    // 2. Check NVM paths (includes current active NVM)\n    installations.extend(find_nvm_installations());\n\n    // 3. Check standard paths\n    installations.extend(find_standard_installations());\n\n    // Remove duplicates by path\n    let mut unique_paths = std::collections::HashSet::new();\n    installations.retain(|install| unique_paths.insert(install.path.clone()));\n\n    installations\n}\n\n/// Try using the 'which' command to find Claude\n#[cfg(unix)]\nfn try_which_command() -> Option<ClaudeInstallation> {\n    debug!(\"Trying 'which claude' to find binary...\");\n\n    match Command::new(\"which\").arg(\"claude\").output() {\n        Ok(output) if output.status.success() => {\n            let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();\n\n            if output_str.is_empty() {\n                return None;\n            }\n\n            // Parse aliased output: \"claude: aliased to /path/to/claude\"\n            let path = if output_str.starts_with(\"claude:\") && output_str.contains(\"aliased to\") {\n                output_str\n                    .split(\"aliased to\")\n                    .nth(1)\n                    .map(|s| s.trim().to_string())\n            } else {\n                Some(output_str)\n            }?;\n\n            debug!(\"'which' found claude at: {}\", path);\n\n            // Verify the path exists\n            if !PathBuf::from(&path).exists() {\n                warn!(\"Path from 'which' does not exist: {}\", path);\n                return None;\n            }\n\n            // Get version\n            let version = get_claude_version(&path).ok().flatten();\n\n            Some(ClaudeInstallation {\n                path,\n                version,\n                source: \"which\".to_string(),\n                installation_type: InstallationType::System,\n            })\n        }\n        _ => None,\n    }\n}\n\n#[cfg(windows)]\nfn try_which_command() -> Option<ClaudeInstallation> {\n    debug!(\"Trying 'where claude' to find binary...\");\n\n    match Command::new(\"where\").arg(\"claude\").output() {\n        Ok(output) if output.status.success() => {\n            let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();\n\n            if output_str.is_empty() {\n                return None;\n            }\n\n            // On Windows, `where` can return multiple paths, newline-separated. We take the first one.\n            let path = output_str.lines().next().unwrap_or(\"\").trim().to_string();\n\n            if path.is_empty() {\n                return None;\n            }\n\n            debug!(\"'where' found claude at: {}\", path);\n\n            // Verify the path exists\n            if !PathBuf::from(&path).exists() {\n                warn!(\"Path from 'where' does not exist: {}\", path);\n                return None;\n            }\n\n            // Get version\n            let version = get_claude_version(&path).ok().flatten();\n\n            Some(ClaudeInstallation {\n                path,\n                version,\n                source: \"where\".to_string(),\n                installation_type: InstallationType::System,\n            })\n        }\n        _ => None,\n    }\n}\n\n/// Find Claude installations in NVM directories\n#[cfg(unix)]\nfn find_nvm_installations() -> Vec<ClaudeInstallation> {\n    let mut installations = Vec::new();\n\n    // First check NVM_BIN environment variable (current active NVM)\n    if let Ok(nvm_bin) = std::env::var(\"NVM_BIN\") {\n        let claude_path = PathBuf::from(&nvm_bin).join(\"claude\");\n        if claude_path.exists() && claude_path.is_file() {\n            debug!(\"Found Claude via NVM_BIN: {:?}\", claude_path);\n            let version = get_claude_version(&claude_path.to_string_lossy())\n                .ok()\n                .flatten();\n            installations.push(ClaudeInstallation {\n                path: claude_path.to_string_lossy().to_string(),\n                version,\n                source: \"nvm-active\".to_string(),\n                installation_type: InstallationType::System,\n            });\n        }\n    }\n\n    // Then check all NVM directories\n    if let Ok(home) = std::env::var(\"HOME\") {\n        let nvm_dir = PathBuf::from(&home)\n            .join(\".nvm\")\n            .join(\"versions\")\n            .join(\"node\");\n\n        debug!(\"Checking NVM directory: {:?}\", nvm_dir);\n\n        if let Ok(entries) = std::fs::read_dir(&nvm_dir) {\n            for entry in entries.flatten() {\n                if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {\n                    let claude_path = entry.path().join(\"bin\").join(\"claude\");\n\n                    if claude_path.exists() && claude_path.is_file() {\n                        let path_str = claude_path.to_string_lossy().to_string();\n                        let node_version = entry.file_name().to_string_lossy().to_string();\n\n                        debug!(\"Found Claude in NVM node {}: {}\", node_version, path_str);\n\n                        // Get Claude version\n                        let version = get_claude_version(&path_str).ok().flatten();\n\n                        installations.push(ClaudeInstallation {\n                            path: path_str,\n                            version,\n                            source: format!(\"nvm ({})\", node_version),\n                            installation_type: InstallationType::System,\n                        });\n                    }\n                }\n            }\n        }\n    }\n\n    installations\n}\n\n#[cfg(windows)]\nfn find_nvm_installations() -> Vec<ClaudeInstallation> {\n    let mut installations = Vec::new();\n\n    if let Ok(nvm_home) = std::env::var(\"NVM_HOME\") {\n        debug!(\"Checking NVM_HOME directory: {:?}\", nvm_home);\n\n        if let Ok(entries) = std::fs::read_dir(&nvm_home) {\n            for entry in entries.flatten() {\n                if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {\n                    let claude_path = entry.path().join(\"claude.exe\");\n\n                    if claude_path.exists() && claude_path.is_file() {\n                        let path_str = claude_path.to_string_lossy().to_string();\n                        let node_version = entry.file_name().to_string_lossy().to_string();\n\n                        debug!(\"Found Claude in NVM node {}: {}\", node_version, path_str);\n\n                        // Get Claude version\n                        let version = get_claude_version(&path_str).ok().flatten();\n\n                        installations.push(ClaudeInstallation {\n                            path: path_str,\n                            version,\n                            source: format!(\"nvm ({})\", node_version),\n                            installation_type: InstallationType::System,\n                        });\n                    }\n                }\n            }\n        }\n    }\n\n    installations\n}\n\n/// Check standard installation paths\n#[cfg(unix)]\nfn find_standard_installations() -> Vec<ClaudeInstallation> {\n    let mut installations = Vec::new();\n\n    // Common installation paths for claude\n    let mut paths_to_check: Vec<(String, String)> = vec![\n        (\"/usr/local/bin/claude\".to_string(), \"system\".to_string()),\n        (\n            \"/opt/homebrew/bin/claude\".to_string(),\n            \"homebrew\".to_string(),\n        ),\n        (\"/usr/bin/claude\".to_string(), \"system\".to_string()),\n        (\"/bin/claude\".to_string(), \"system\".to_string()),\n    ];\n\n    // Also check user-specific paths\n    if let Ok(home) = std::env::var(\"HOME\") {\n        paths_to_check.extend(vec![\n            (\n                format!(\"{}/.claude/local/claude\", home),\n                \"claude-local\".to_string(),\n            ),\n            (\n                format!(\"{}/.local/bin/claude\", home),\n                \"local-bin\".to_string(),\n            ),\n            (\n                format!(\"{}/.npm-global/bin/claude\", home),\n                \"npm-global\".to_string(),\n            ),\n            (format!(\"{}/.yarn/bin/claude\", home), \"yarn\".to_string()),\n            (format!(\"{}/.bun/bin/claude\", home), \"bun\".to_string()),\n            (format!(\"{}/bin/claude\", home), \"home-bin\".to_string()),\n            // Check common node_modules locations\n            (\n                format!(\"{}/node_modules/.bin/claude\", home),\n                \"node-modules\".to_string(),\n            ),\n            (\n                format!(\"{}/.config/yarn/global/node_modules/.bin/claude\", home),\n                \"yarn-global\".to_string(),\n            ),\n        ]);\n    }\n\n    // Check each path\n    for (path, source) in paths_to_check {\n        let path_buf = PathBuf::from(&path);\n        if path_buf.exists() && path_buf.is_file() {\n            debug!(\"Found claude at standard path: {} ({})\", path, source);\n\n            // Get version\n            let version = get_claude_version(&path).ok().flatten();\n\n            installations.push(ClaudeInstallation {\n                path,\n                version,\n                source,\n                installation_type: InstallationType::System,\n            });\n        }\n    }\n\n    // Also check if claude is available in PATH (without full path)\n    if let Ok(output) = Command::new(\"claude\").arg(\"--version\").output() {\n        if output.status.success() {\n            debug!(\"claude is available in PATH\");\n            let version = extract_version_from_output(&output.stdout);\n\n            installations.push(ClaudeInstallation {\n                path: \"claude\".to_string(),\n                version,\n                source: \"PATH\".to_string(),\n                installation_type: InstallationType::System,\n            });\n        }\n    }\n\n    installations\n}\n\n#[cfg(windows)]\nfn find_standard_installations() -> Vec<ClaudeInstallation> {\n    let mut installations = Vec::new();\n\n    // Common installation paths for claude on Windows\n    let mut paths_to_check: Vec<(String, String)> = vec![];\n\n    // Check user-specific paths\n    if let Ok(user_profile) = std::env::var(\"USERPROFILE\") {\n        paths_to_check.extend(vec![\n            (\n                format!(\"{}\\\\.claude\\\\local\\\\claude.exe\", user_profile),\n                \"claude-local\".to_string(),\n            ),\n            (\n                format!(\"{}\\\\.local\\\\bin\\\\claude.exe\", user_profile),\n                \"local-bin\".to_string(),\n            ),\n            (\n                format!(\"{}\\\\AppData\\\\Roaming\\\\npm\\\\claude.cmd\", user_profile),\n                \"npm-global\".to_string(),\n            ),\n            (\n                format!(\"{}\\\\.yarn\\\\bin\\\\claude.cmd\", user_profile),\n                \"yarn\".to_string(),\n            ),\n            (\n                format!(\"{}\\\\.bun\\\\bin\\\\claude.exe\", user_profile),\n                \"bun\".to_string(),\n            ),\n        ]);\n    }\n\n    // Check each path\n    for (path, source) in paths_to_check {\n        let path_buf = PathBuf::from(&path);\n        if path_buf.exists() && path_buf.is_file() {\n            debug!(\"Found claude at standard path: {} ({})\", path, source);\n\n            // Get version\n            let version = get_claude_version(&path).ok().flatten();\n\n            installations.push(ClaudeInstallation {\n                path,\n                version,\n                source,\n                installation_type: InstallationType::System,\n            });\n        }\n    }\n\n    // Also check if claude is available in PATH (without full path)\n    if let Ok(output) = Command::new(\"claude.exe\").arg(\"--version\").output() {\n        if output.status.success() {\n            debug!(\"claude.exe is available in PATH\");\n            let version = extract_version_from_output(&output.stdout);\n\n            installations.push(ClaudeInstallation {\n                path: \"claude.exe\".to_string(),\n                version,\n                source: \"PATH\".to_string(),\n                installation_type: InstallationType::System,\n            });\n        }\n    }\n\n    installations\n}\n\n/// Get Claude version by running --version command\nfn get_claude_version(path: &str) -> Result<Option<String>, String> {\n    match Command::new(path).arg(\"--version\").output() {\n        Ok(output) => {\n            if output.status.success() {\n                Ok(extract_version_from_output(&output.stdout))\n            } else {\n                Ok(None)\n            }\n        }\n        Err(e) => {\n            warn!(\"Failed to get version for {}: {}\", path, e);\n            Ok(None)\n        }\n    }\n}\n\n/// Extract version string from command output\nfn extract_version_from_output(stdout: &[u8]) -> Option<String> {\n    let output_str = String::from_utf8_lossy(stdout);\n\n    // Debug log the raw output\n    debug!(\"Raw version output: {:?}\", output_str);\n\n    // Use regex to directly extract version pattern (e.g., \"1.0.41\")\n    // This pattern matches:\n    // - One or more digits, followed by\n    // - A dot, followed by\n    // - One or more digits, followed by\n    // - A dot, followed by\n    // - One or more digits\n    // - Optionally followed by pre-release/build metadata\n    let version_regex =\n        regex::Regex::new(r\"(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?(?:\\+[a-zA-Z0-9.-]+)?)\").ok()?;\n\n    if let Some(captures) = version_regex.captures(&output_str) {\n        if let Some(version_match) = captures.get(1) {\n            let version = version_match.as_str().to_string();\n            debug!(\"Extracted version: {:?}\", version);\n            return Some(version);\n        }\n    }\n\n    debug!(\"No version found in output\");\n    None\n}\n\n/// Select the best installation based on version\nfn select_best_installation(installations: Vec<ClaudeInstallation>) -> Option<ClaudeInstallation> {\n    // In production builds, version information may not be retrievable because\n    // spawning external processes can be restricted. We therefore no longer\n    // discard installations that lack a detected version – the mere presence\n    // of a readable binary on disk is enough to consider it valid. We still\n    // prefer binaries with version information when it is available so that\n    // in development builds we keep the previous behaviour of picking the\n    // most recent version.\n    installations.into_iter().max_by(|a, b| {\n        match (&a.version, &b.version) {\n            // If both have versions, compare them semantically.\n            (Some(v1), Some(v2)) => compare_versions(v1, v2),\n            // Prefer the entry that actually has version information.\n            (Some(_), None) => Ordering::Greater,\n            (None, Some(_)) => Ordering::Less,\n            // Neither have version info: prefer the one that is not just\n            // the bare \"claude\" lookup from PATH, because that may fail\n            // at runtime if PATH is modified.\n            (None, None) => {\n                if a.path == \"claude\" && b.path != \"claude\" {\n                    Ordering::Less\n                } else if a.path != \"claude\" && b.path == \"claude\" {\n                    Ordering::Greater\n                } else {\n                    Ordering::Equal\n                }\n            }\n        }\n    })\n}\n\n/// Compare two version strings\nfn compare_versions(a: &str, b: &str) -> Ordering {\n    // Simple semantic version comparison\n    let a_parts: Vec<u32> = a\n        .split('.')\n        .filter_map(|s| {\n            // Handle versions like \"1.0.17-beta\" by taking only numeric part\n            s.chars()\n                .take_while(|c| c.is_numeric())\n                .collect::<String>()\n                .parse()\n                .ok()\n        })\n        .collect();\n\n    let b_parts: Vec<u32> = b\n        .split('.')\n        .filter_map(|s| {\n            s.chars()\n                .take_while(|c| c.is_numeric())\n                .collect::<String>()\n                .parse()\n                .ok()\n        })\n        .collect();\n\n    // Compare each part\n    for i in 0..std::cmp::max(a_parts.len(), b_parts.len()) {\n        let a_val = a_parts.get(i).unwrap_or(&0);\n        let b_val = b_parts.get(i).unwrap_or(&0);\n        match a_val.cmp(b_val) {\n            Ordering::Equal => continue,\n            other => return other,\n        }\n    }\n\n    Ordering::Equal\n}\n\n/// Helper function to create a Command with proper environment variables\n/// This ensures commands like Claude can find Node.js and other dependencies\npub fn create_command_with_env(program: &str) -> Command {\n    let mut cmd = Command::new(program);\n\n    info!(\"Creating command for: {}\", program);\n\n    // Inherit essential environment variables from parent process\n    for (key, value) in std::env::vars() {\n        // Pass through PATH and other essential environment variables\n        if key == \"PATH\"\n            || key == \"HOME\"\n            || key == \"USER\"\n            || key == \"SHELL\"\n            || key == \"LANG\"\n            || key == \"LC_ALL\"\n            || key.starts_with(\"LC_\")\n            || key == \"NODE_PATH\"\n            || key == \"NVM_DIR\"\n            || key == \"NVM_BIN\"\n            || key == \"HOMEBREW_PREFIX\"\n            || key == \"HOMEBREW_CELLAR\"\n            // Add proxy environment variables (only uppercase)\n            || key == \"HTTP_PROXY\"\n            || key == \"HTTPS_PROXY\"\n            || key == \"NO_PROXY\"\n            || key == \"ALL_PROXY\"\n        {\n            debug!(\"Inheriting env var: {}={}\", key, value);\n            cmd.env(&key, &value);\n        }\n    }\n\n    // Log proxy-related environment variables for debugging\n    info!(\"Command will use proxy settings:\");\n    if let Ok(http_proxy) = std::env::var(\"HTTP_PROXY\") {\n        info!(\"  HTTP_PROXY={}\", http_proxy);\n    }\n    if let Ok(https_proxy) = std::env::var(\"HTTPS_PROXY\") {\n        info!(\"  HTTPS_PROXY={}\", https_proxy);\n    }\n\n    // Add NVM support if the program is in an NVM directory\n    if program.contains(\"/.nvm/versions/node/\") {\n        if let Some(node_bin_dir) = std::path::Path::new(program).parent() {\n            // Ensure the Node.js bin directory is in PATH\n            let current_path = std::env::var(\"PATH\").unwrap_or_default();\n            let node_bin_str = node_bin_dir.to_string_lossy();\n            if !current_path.contains(&node_bin_str.as_ref()) {\n                let new_path = format!(\"{}:{}\", node_bin_str, current_path);\n                debug!(\"Adding NVM bin directory to PATH: {}\", node_bin_str);\n                cmd.env(\"PATH\", new_path);\n            }\n        }\n    }\n\n    // Add Homebrew support if the program is in a Homebrew directory\n    if program.contains(\"/homebrew/\") || program.contains(\"/opt/homebrew/\") {\n        if let Some(program_dir) = std::path::Path::new(program).parent() {\n            // Ensure the Homebrew bin directory is in PATH\n            let current_path = std::env::var(\"PATH\").unwrap_or_default();\n            let homebrew_bin_str = program_dir.to_string_lossy();\n            if !current_path.contains(&homebrew_bin_str.as_ref()) {\n                let new_path = format!(\"{}:{}\", homebrew_bin_str, current_path);\n                debug!(\n                    \"Adding Homebrew bin directory to PATH: {}\",\n                    homebrew_bin_str\n                );\n                cmd.env(\"PATH\", new_path);\n            }\n        }\n    }\n\n    cmd\n}\n"
  },
  {
    "path": "src-tauri/src/commands/agents.rs",
    "content": "use anyhow::Result;\nuse chrono;\nuse dirs;\nuse log::{debug, error, info, warn};\nuse reqwest;\nuse rusqlite::{params, Connection, Result as SqliteResult};\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value as JsonValue;\nuse std::io::{BufRead, BufReader};\nuse std::process::Stdio;\nuse std::sync::Mutex;\nuse tauri::{AppHandle, Emitter, Manager, State};\n// Sidecar support removed; using system binary execution only\nuse tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader};\nuse tokio::process::Command;\n\n/// Finds the full path to the claude binary\n/// This is necessary because macOS apps have a limited PATH environment\nfn find_claude_binary(app_handle: &AppHandle) -> Result<String, String> {\n    crate::claude_binary::find_claude_binary(app_handle)\n}\n\n/// Represents a CC Agent stored in the database\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct Agent {\n    pub id: Option<i64>,\n    pub name: String,\n    pub icon: String,\n    pub system_prompt: String,\n    pub default_task: Option<String>,\n    pub model: String,\n    pub enable_file_read: bool,\n    pub enable_file_write: bool,\n    pub enable_network: bool,\n    pub hooks: Option<String>, // JSON string of hooks configuration\n    pub created_at: String,\n    pub updated_at: String,\n}\n\n/// Represents an agent execution run\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct AgentRun {\n    pub id: Option<i64>,\n    pub agent_id: i64,\n    pub agent_name: String,\n    pub agent_icon: String,\n    pub task: String,\n    pub model: String,\n    pub project_path: String,\n    pub session_id: String, // UUID session ID from Claude Code\n    pub status: String,     // 'pending', 'running', 'completed', 'failed', 'cancelled'\n    pub pid: Option<u32>,\n    pub process_started_at: Option<String>,\n    pub created_at: String,\n    pub completed_at: Option<String>,\n}\n\n/// Represents runtime metrics calculated from JSONL\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct AgentRunMetrics {\n    pub duration_ms: Option<i64>,\n    pub total_tokens: Option<i64>,\n    pub cost_usd: Option<f64>,\n    pub message_count: Option<i64>,\n}\n\n/// Combined agent run with real-time metrics\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct AgentRunWithMetrics {\n    #[serde(flatten)]\n    pub run: AgentRun,\n    pub metrics: Option<AgentRunMetrics>,\n    pub output: Option<String>, // Real-time JSONL content\n}\n\n/// Agent export format\n#[derive(Debug, Serialize, Deserialize)]\npub struct AgentExport {\n    pub version: u32,\n    pub exported_at: String,\n    pub agent: AgentData,\n}\n\n/// Agent data within export\n#[derive(Debug, Serialize, Deserialize)]\npub struct AgentData {\n    pub name: String,\n    pub icon: String,\n    pub system_prompt: String,\n    pub default_task: Option<String>,\n    pub model: String,\n    pub hooks: Option<String>,\n}\n\n/// Database connection state\npub struct AgentDb(pub Mutex<Connection>);\n\n/// Real-time JSONL reading and processing functions\nimpl AgentRunMetrics {\n    /// Calculate metrics from JSONL content\n    pub fn from_jsonl(jsonl_content: &str) -> Self {\n        let mut total_tokens = 0i64;\n        let mut cost_usd = 0.0f64;\n        let mut message_count = 0i64;\n        let mut start_time: Option<chrono::DateTime<chrono::Utc>> = None;\n        let mut end_time: Option<chrono::DateTime<chrono::Utc>> = None;\n\n        for line in jsonl_content.lines() {\n            if let Ok(json) = serde_json::from_str::<JsonValue>(line) {\n                message_count += 1;\n\n                // Track timestamps\n                if let Some(timestamp_str) = json.get(\"timestamp\").and_then(|t| t.as_str()) {\n                    if let Ok(timestamp) = chrono::DateTime::parse_from_rfc3339(timestamp_str) {\n                        let utc_time = timestamp.with_timezone(&chrono::Utc);\n                        if start_time.is_none() || utc_time < start_time.unwrap() {\n                            start_time = Some(utc_time);\n                        }\n                        if end_time.is_none() || utc_time > end_time.unwrap() {\n                            end_time = Some(utc_time);\n                        }\n                    }\n                }\n\n                // Extract token usage - check both top-level and nested message.usage\n                let usage = json\n                    .get(\"usage\")\n                    .or_else(|| json.get(\"message\").and_then(|m| m.get(\"usage\")));\n\n                if let Some(usage) = usage {\n                    if let Some(input_tokens) = usage.get(\"input_tokens\").and_then(|t| t.as_i64()) {\n                        total_tokens += input_tokens;\n                    }\n                    if let Some(output_tokens) = usage.get(\"output_tokens\").and_then(|t| t.as_i64())\n                    {\n                        total_tokens += output_tokens;\n                    }\n                }\n\n                // Extract cost information\n                if let Some(cost) = json.get(\"cost\").and_then(|c| c.as_f64()) {\n                    cost_usd += cost;\n                }\n            }\n        }\n\n        let duration_ms = match (start_time, end_time) {\n            (Some(start), Some(end)) => Some((end - start).num_milliseconds()),\n            _ => None,\n        };\n\n        Self {\n            duration_ms,\n            total_tokens: if total_tokens > 0 {\n                Some(total_tokens)\n            } else {\n                None\n            },\n            cost_usd: if cost_usd > 0.0 { Some(cost_usd) } else { None },\n            message_count: if message_count > 0 {\n                Some(message_count)\n            } else {\n                None\n            },\n        }\n    }\n}\n\n/// Read JSONL content from a session file\npub async fn read_session_jsonl(session_id: &str, project_path: &str) -> Result<String, String> {\n    let claude_dir = dirs::home_dir()\n        .ok_or(\"Failed to get home directory\")?\n        .join(\".claude\")\n        .join(\"projects\");\n\n    // Encode project path to match Claude Code's directory naming\n    let encoded_project = project_path.replace('/', \"-\");\n    let project_dir = claude_dir.join(&encoded_project);\n    let session_file = project_dir.join(format!(\"{}.jsonl\", session_id));\n\n    if !session_file.exists() {\n        return Err(format!(\n            \"Session file not found: {}\",\n            session_file.display()\n        ));\n    }\n\n    match tokio::fs::read_to_string(&session_file).await {\n        Ok(content) => Ok(content),\n        Err(e) => Err(format!(\"Failed to read session file: {}\", e)),\n    }\n}\n\n/// Get agent run with real-time metrics\npub async fn get_agent_run_with_metrics(run: AgentRun) -> AgentRunWithMetrics {\n    match read_session_jsonl(&run.session_id, &run.project_path).await {\n        Ok(jsonl_content) => {\n            let metrics = AgentRunMetrics::from_jsonl(&jsonl_content);\n            AgentRunWithMetrics {\n                run,\n                metrics: Some(metrics),\n                output: Some(jsonl_content),\n            }\n        }\n        Err(e) => {\n            log::warn!(\"Failed to read JSONL for session {}: {}\", run.session_id, e);\n            AgentRunWithMetrics {\n                run,\n                metrics: None,\n                output: None,\n            }\n        }\n    }\n}\n\n/// Initialize the agents database\npub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {\n    let app_dir = app\n        .path()\n        .app_data_dir()\n        .expect(\"Failed to get app data dir\");\n    std::fs::create_dir_all(&app_dir).expect(\"Failed to create app data dir\");\n\n    let db_path = app_dir.join(\"agents.db\");\n    let conn = Connection::open(db_path)?;\n\n    // Create agents table\n    conn.execute(\n        \"CREATE TABLE IF NOT EXISTS agents (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            name TEXT NOT NULL,\n            icon TEXT NOT NULL,\n            system_prompt TEXT NOT NULL,\n            default_task TEXT,\n            model TEXT NOT NULL DEFAULT 'sonnet',\n            enable_file_read BOOLEAN NOT NULL DEFAULT 1,\n            enable_file_write BOOLEAN NOT NULL DEFAULT 1,\n            enable_network BOOLEAN NOT NULL DEFAULT 0,\n            hooks TEXT,\n            created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n            updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP\n        )\",\n        [],\n    )?;\n\n    // Add columns to existing table if they don't exist\n    let _ = conn.execute(\"ALTER TABLE agents ADD COLUMN default_task TEXT\", []);\n    let _ = conn.execute(\n        \"ALTER TABLE agents ADD COLUMN model TEXT DEFAULT 'sonnet'\",\n        [],\n    );\n    let _ = conn.execute(\"ALTER TABLE agents ADD COLUMN hooks TEXT\", []);\n    let _ = conn.execute(\n        \"ALTER TABLE agents ADD COLUMN enable_file_read BOOLEAN DEFAULT 1\",\n        [],\n    );\n    let _ = conn.execute(\n        \"ALTER TABLE agents ADD COLUMN enable_file_write BOOLEAN DEFAULT 1\",\n        [],\n    );\n    let _ = conn.execute(\n        \"ALTER TABLE agents ADD COLUMN enable_network BOOLEAN DEFAULT 0\",\n        [],\n    );\n\n    // Create agent_runs table\n    conn.execute(\n        \"CREATE TABLE IF NOT EXISTS agent_runs (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            agent_id INTEGER NOT NULL,\n            agent_name TEXT NOT NULL,\n            agent_icon TEXT NOT NULL,\n            task TEXT NOT NULL,\n            model TEXT NOT NULL,\n            project_path TEXT NOT NULL,\n            session_id TEXT NOT NULL,\n            status TEXT NOT NULL DEFAULT 'pending',\n            pid INTEGER,\n            process_started_at TEXT,\n            created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n            completed_at TEXT,\n            FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE\n        )\",\n        [],\n    )?;\n\n    // Migrate existing agent_runs table if needed\n    let _ = conn.execute(\"ALTER TABLE agent_runs ADD COLUMN session_id TEXT\", []);\n    let _ = conn.execute(\n        \"ALTER TABLE agent_runs ADD COLUMN status TEXT DEFAULT 'pending'\",\n        [],\n    );\n    let _ = conn.execute(\"ALTER TABLE agent_runs ADD COLUMN pid INTEGER\", []);\n    let _ = conn.execute(\n        \"ALTER TABLE agent_runs ADD COLUMN process_started_at TEXT\",\n        [],\n    );\n\n    // Drop old columns that are no longer needed (data is now read from JSONL files)\n    // Note: SQLite doesn't support DROP COLUMN, so we'll ignore errors for existing columns\n    let _ = conn.execute(\n        \"UPDATE agent_runs SET session_id = '' WHERE session_id IS NULL\",\n        [],\n    );\n    let _ = conn.execute(\"UPDATE agent_runs SET status = 'completed' WHERE status IS NULL AND completed_at IS NOT NULL\", []);\n    let _ = conn.execute(\"UPDATE agent_runs SET status = 'failed' WHERE status IS NULL AND completed_at IS NOT NULL AND session_id = ''\", []);\n    let _ = conn.execute(\n        \"UPDATE agent_runs SET status = 'pending' WHERE status IS NULL\",\n        [],\n    );\n\n    // Create trigger to update the updated_at timestamp\n    conn.execute(\n        \"CREATE TRIGGER IF NOT EXISTS update_agent_timestamp \n         AFTER UPDATE ON agents \n         FOR EACH ROW\n         BEGIN\n             UPDATE agents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;\n         END\",\n        [],\n    )?;\n\n    // Create settings table for app-wide settings\n    conn.execute(\n        \"CREATE TABLE IF NOT EXISTS app_settings (\n            key TEXT PRIMARY KEY,\n            value TEXT NOT NULL,\n            created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n            updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP\n        )\",\n        [],\n    )?;\n\n    // Create trigger to update the updated_at timestamp\n    conn.execute(\n        \"CREATE TRIGGER IF NOT EXISTS update_app_settings_timestamp \n         AFTER UPDATE ON app_settings \n         FOR EACH ROW\n         BEGIN\n             UPDATE app_settings SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;\n         END\",\n        [],\n    )?;\n\n    Ok(conn)\n}\n\n/// List all agents\n#[tauri::command]\npub async fn list_agents(db: State<'_, AgentDb>) -> Result<Vec<Agent>, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    let mut stmt = conn\n        .prepare(\"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks, created_at, updated_at FROM agents ORDER BY created_at DESC\")\n        .map_err(|e| e.to_string())?;\n\n    let agents = stmt\n        .query_map([], |row| {\n            Ok(Agent {\n                id: Some(row.get(0)?),\n                name: row.get(1)?,\n                icon: row.get(2)?,\n                system_prompt: row.get(3)?,\n                default_task: row.get(4)?,\n                model: row\n                    .get::<_, String>(5)\n                    .unwrap_or_else(|_| \"sonnet\".to_string()),\n                enable_file_read: row.get::<_, bool>(6).unwrap_or(true),\n                enable_file_write: row.get::<_, bool>(7).unwrap_or(true),\n                enable_network: row.get::<_, bool>(8).unwrap_or(false),\n                hooks: row.get(9)?,\n                created_at: row.get(10)?,\n                updated_at: row.get(11)?,\n            })\n        })\n        .map_err(|e| e.to_string())?\n        .collect::<Result<Vec<_>, _>>()\n        .map_err(|e| e.to_string())?;\n\n    Ok(agents)\n}\n\n/// Create a new agent\n#[tauri::command]\npub async fn create_agent(\n    db: State<'_, AgentDb>,\n    name: String,\n    icon: String,\n    system_prompt: String,\n    default_task: Option<String>,\n    model: Option<String>,\n    enable_file_read: Option<bool>,\n    enable_file_write: Option<bool>,\n    enable_network: Option<bool>,\n    hooks: Option<String>,\n) -> Result<Agent, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n    let model = model.unwrap_or_else(|| \"sonnet\".to_string());\n    let enable_file_read = enable_file_read.unwrap_or(true);\n    let enable_file_write = enable_file_write.unwrap_or(true);\n    let enable_network = enable_network.unwrap_or(false);\n\n    conn.execute(\n        \"INSERT INTO agents (name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)\",\n        params![name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks],\n    )\n    .map_err(|e| e.to_string())?;\n\n    let id = conn.last_insert_rowid();\n\n    // Fetch the created agent\n    let agent = conn\n        .query_row(\n            \"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks, created_at, updated_at FROM agents WHERE id = ?1\",\n            params![id],\n            |row| {\n                Ok(Agent {\n                    id: Some(row.get(0)?),\n                    name: row.get(1)?,\n                    icon: row.get(2)?,\n                    system_prompt: row.get(3)?,\n                    default_task: row.get(4)?,\n                    model: row.get(5)?,\n                    enable_file_read: row.get(6)?,\n                    enable_file_write: row.get(7)?,\n                    enable_network: row.get(8)?,\n                    hooks: row.get(9)?,\n                    created_at: row.get(10)?,\n                    updated_at: row.get(11)?,\n                })\n            },\n        )\n        .map_err(|e| e.to_string())?;\n\n    Ok(agent)\n}\n\n/// Update an existing agent\n#[tauri::command]\npub async fn update_agent(\n    db: State<'_, AgentDb>,\n    id: i64,\n    name: String,\n    icon: String,\n    system_prompt: String,\n    default_task: Option<String>,\n    model: Option<String>,\n    enable_file_read: Option<bool>,\n    enable_file_write: Option<bool>,\n    enable_network: Option<bool>,\n    hooks: Option<String>,\n) -> Result<Agent, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n    let model = model.unwrap_or_else(|| \"sonnet\".to_string());\n\n    // Build dynamic query based on provided parameters\n    let mut query =\n        \"UPDATE agents SET name = ?1, icon = ?2, system_prompt = ?3, default_task = ?4, model = ?5, hooks = ?6\"\n            .to_string();\n    let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = vec![\n        Box::new(name),\n        Box::new(icon),\n        Box::new(system_prompt),\n        Box::new(default_task),\n        Box::new(model),\n        Box::new(hooks),\n    ];\n    let mut param_count = 6;\n\n    if let Some(efr) = enable_file_read {\n        param_count += 1;\n        query.push_str(&format!(\", enable_file_read = ?{}\", param_count));\n        params_vec.push(Box::new(efr));\n    }\n    if let Some(efw) = enable_file_write {\n        param_count += 1;\n        query.push_str(&format!(\", enable_file_write = ?{}\", param_count));\n        params_vec.push(Box::new(efw));\n    }\n    if let Some(en) = enable_network {\n        param_count += 1;\n        query.push_str(&format!(\", enable_network = ?{}\", param_count));\n        params_vec.push(Box::new(en));\n    }\n\n    param_count += 1;\n    query.push_str(&format!(\" WHERE id = ?{}\", param_count));\n    params_vec.push(Box::new(id));\n\n    conn.execute(\n        &query,\n        rusqlite::params_from_iter(params_vec.iter().map(|p| p.as_ref())),\n    )\n    .map_err(|e| e.to_string())?;\n\n    // Fetch the updated agent\n    let agent = conn\n        .query_row(\n            \"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks, created_at, updated_at FROM agents WHERE id = ?1\",\n            params![id],\n            |row| {\n                Ok(Agent {\n                    id: Some(row.get(0)?),\n                    name: row.get(1)?,\n                    icon: row.get(2)?,\n                    system_prompt: row.get(3)?,\n                    default_task: row.get(4)?,\n                    model: row.get(5)?,\n                    enable_file_read: row.get(6)?,\n                    enable_file_write: row.get(7)?,\n                    enable_network: row.get(8)?,\n                    hooks: row.get(9)?,\n                    created_at: row.get(10)?,\n                    updated_at: row.get(11)?,\n                })\n            },\n        )\n        .map_err(|e| e.to_string())?;\n\n    Ok(agent)\n}\n\n/// Delete an agent\n#[tauri::command]\npub async fn delete_agent(db: State<'_, AgentDb>, id: i64) -> Result<(), String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    conn.execute(\"DELETE FROM agents WHERE id = ?1\", params![id])\n        .map_err(|e| e.to_string())?;\n\n    Ok(())\n}\n\n/// Get a single agent by ID\n#[tauri::command]\npub async fn get_agent(db: State<'_, AgentDb>, id: i64) -> Result<Agent, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    let agent = conn\n        .query_row(\n            \"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks, created_at, updated_at FROM agents WHERE id = ?1\",\n            params![id],\n            |row| {\n                Ok(Agent {\n                    id: Some(row.get(0)?),\n                    name: row.get(1)?,\n                    icon: row.get(2)?,\n                    system_prompt: row.get(3)?,\n                    default_task: row.get(4)?,\n                    model: row.get::<_, String>(5).unwrap_or_else(|_| \"sonnet\".to_string()),\n                    enable_file_read: row.get::<_, bool>(6).unwrap_or(true),\n                    enable_file_write: row.get::<_, bool>(7).unwrap_or(true),\n                    enable_network: row.get::<_, bool>(8).unwrap_or(false),\n                    hooks: row.get(9)?,\n                    created_at: row.get(10)?,\n                    updated_at: row.get(11)?,\n                })\n            },\n        )\n        .map_err(|e| e.to_string())?;\n\n    Ok(agent)\n}\n\n/// List agent runs (optionally filtered by agent_id)\n#[tauri::command]\npub async fn list_agent_runs(\n    db: State<'_, AgentDb>,\n    agent_id: Option<i64>,\n) -> Result<Vec<AgentRun>, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    let query = if agent_id.is_some() {\n        \"SELECT id, agent_id, agent_name, agent_icon, task, model, project_path, session_id, status, pid, process_started_at, created_at, completed_at \n         FROM agent_runs WHERE agent_id = ?1 ORDER BY created_at DESC\"\n    } else {\n        \"SELECT id, agent_id, agent_name, agent_icon, task, model, project_path, session_id, status, pid, process_started_at, created_at, completed_at \n         FROM agent_runs ORDER BY created_at DESC\"\n    };\n\n    let mut stmt = conn.prepare(query).map_err(|e| e.to_string())?;\n\n    let run_mapper = |row: &rusqlite::Row| -> rusqlite::Result<AgentRun> {\n        Ok(AgentRun {\n            id: Some(row.get(0)?),\n            agent_id: row.get(1)?,\n            agent_name: row.get(2)?,\n            agent_icon: row.get(3)?,\n            task: row.get(4)?,\n            model: row.get(5)?,\n            project_path: row.get(6)?,\n            session_id: row.get(7)?,\n            status: row\n                .get::<_, String>(8)\n                .unwrap_or_else(|_| \"pending\".to_string()),\n            pid: row\n                .get::<_, Option<i64>>(9)\n                .ok()\n                .flatten()\n                .map(|p| p as u32),\n            process_started_at: row.get(10)?,\n            created_at: row.get(11)?,\n            completed_at: row.get(12)?,\n        })\n    };\n\n    let runs = if let Some(aid) = agent_id {\n        stmt.query_map(params![aid], run_mapper)\n    } else {\n        stmt.query_map(params![], run_mapper)\n    }\n    .map_err(|e| e.to_string())?\n    .collect::<Result<Vec<_>, _>>()\n    .map_err(|e| e.to_string())?;\n\n    Ok(runs)\n}\n\n/// Get a single agent run by ID\n#[tauri::command]\npub async fn get_agent_run(db: State<'_, AgentDb>, id: i64) -> Result<AgentRun, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    let run = conn\n        .query_row(\n            \"SELECT id, agent_id, agent_name, agent_icon, task, model, project_path, session_id, status, pid, process_started_at, created_at, completed_at \n             FROM agent_runs WHERE id = ?1\",\n            params![id],\n            |row| {\n                Ok(AgentRun {\n                    id: Some(row.get(0)?),\n                    agent_id: row.get(1)?,\n                    agent_name: row.get(2)?,\n                    agent_icon: row.get(3)?,\n                    task: row.get(4)?,\n                    model: row.get(5)?,\n                    project_path: row.get(6)?,\n                    session_id: row.get(7)?,\n                    status: row.get::<_, String>(8).unwrap_or_else(|_| \"pending\".to_string()),\n                    pid: row.get::<_, Option<i64>>(9).ok().flatten().map(|p| p as u32),\n                    process_started_at: row.get(10)?,\n                    created_at: row.get(11)?,\n                    completed_at: row.get(12)?,\n                })\n            },\n        )\n        .map_err(|e| e.to_string())?;\n\n    Ok(run)\n}\n\n/// Get agent run with real-time metrics from JSONL\n#[tauri::command]\npub async fn get_agent_run_with_real_time_metrics(\n    db: State<'_, AgentDb>,\n    id: i64,\n) -> Result<AgentRunWithMetrics, String> {\n    let run = get_agent_run(db, id).await?;\n    Ok(get_agent_run_with_metrics(run).await)\n}\n\n/// List agent runs with real-time metrics from JSONL\n#[tauri::command]\npub async fn list_agent_runs_with_metrics(\n    db: State<'_, AgentDb>,\n    agent_id: Option<i64>,\n) -> Result<Vec<AgentRunWithMetrics>, String> {\n    let runs = list_agent_runs(db, agent_id).await?;\n    let mut runs_with_metrics = Vec::new();\n\n    for run in runs {\n        let run_with_metrics = get_agent_run_with_metrics(run).await;\n        runs_with_metrics.push(run_with_metrics);\n    }\n\n    Ok(runs_with_metrics)\n}\n\n/// Execute a CC agent with streaming output\n#[tauri::command]\npub async fn execute_agent(\n    app: AppHandle,\n    agent_id: i64,\n    project_path: String,\n    task: String,\n    model: Option<String>,\n    db: State<'_, AgentDb>,\n    registry: State<'_, crate::process::ProcessRegistryState>,\n) -> Result<i64, String> {\n    info!(\"Executing agent {} with task: {}\", agent_id, task);\n\n    // Get the agent from database\n    let agent = get_agent(db.clone(), agent_id).await?;\n    let execution_model = model.unwrap_or(agent.model.clone());\n\n    // Create .claude/settings.json with agent hooks if it doesn't exist\n    if let Some(hooks_json) = &agent.hooks {\n        let claude_dir = std::path::Path::new(&project_path).join(\".claude\");\n        let settings_path = claude_dir.join(\"settings.json\");\n\n        // Create .claude directory if it doesn't exist\n        if !claude_dir.exists() {\n            std::fs::create_dir_all(&claude_dir)\n                .map_err(|e| format!(\"Failed to create .claude directory: {}\", e))?;\n            info!(\"Created .claude directory at: {:?}\", claude_dir);\n        }\n\n        // Check if settings.json already exists\n        if !settings_path.exists() {\n            // Parse the hooks JSON\n            let hooks: serde_json::Value = serde_json::from_str(hooks_json)\n                .map_err(|e| format!(\"Failed to parse agent hooks: {}\", e))?;\n\n            // Create a settings object with just the hooks\n            let settings = serde_json::json!({\n                \"hooks\": hooks\n            });\n\n            // Write the settings file\n            let settings_content = serde_json::to_string_pretty(&settings)\n                .map_err(|e| format!(\"Failed to serialize settings: {}\", e))?;\n\n            std::fs::write(&settings_path, settings_content)\n                .map_err(|e| format!(\"Failed to write settings.json: {}\", e))?;\n\n            info!(\n                \"Created settings.json with agent hooks at: {:?}\",\n                settings_path\n            );\n        } else {\n            info!(\"settings.json already exists at: {:?}\", settings_path);\n        }\n    }\n\n    // Create a new run record\n    let run_id = {\n        let conn = db.0.lock().map_err(|e| e.to_string())?;\n        conn.execute(\n            \"INSERT INTO agent_runs (agent_id, agent_name, agent_icon, task, model, project_path, session_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)\",\n            params![agent_id, agent.name, agent.icon, task, execution_model, project_path, \"\"],\n        )\n        .map_err(|e| e.to_string())?;\n        conn.last_insert_rowid()\n    };\n\n    // Find Claude binary\n    info!(\"Running agent '{}'\", agent.name);\n    let claude_path = match find_claude_binary(&app) {\n        Ok(path) => path,\n        Err(e) => {\n            error!(\"Failed to find claude binary: {}\", e);\n            return Err(e);\n        }\n    };\n\n    // Build arguments\n    let args = vec![\n        \"-p\".to_string(),\n        task.clone(),\n        \"--system-prompt\".to_string(),\n        agent.system_prompt.clone(),\n        \"--model\".to_string(),\n        execution_model.clone(),\n        \"--output-format\".to_string(),\n        \"stream-json\".to_string(),\n        \"--verbose\".to_string(),\n        \"--dangerously-skip-permissions\".to_string(),\n    ];\n\n    // Always use system binary execution (sidecar removed)\n    spawn_agent_system(\n        app,\n        run_id,\n        agent_id,\n        agent.name.clone(),\n        claude_path,\n        args,\n        project_path,\n        task,\n        execution_model,\n        db,\n        registry,\n    )\n    .await\n}\n\n/// Creates a system binary command for agent execution\nfn create_agent_system_command(\n    claude_path: &str,\n    args: Vec<String>,\n    project_path: &str,\n) -> Command {\n    let mut cmd = create_command_with_env(claude_path);\n\n    // Add all arguments\n    for arg in args {\n        cmd.arg(arg);\n    }\n\n    cmd.current_dir(project_path)\n        .stdin(Stdio::null())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped());\n\n    cmd\n}\n\n/// Spawn agent using system binary command\nasync fn spawn_agent_system(\n    app: AppHandle,\n    run_id: i64,\n    agent_id: i64,\n    agent_name: String,\n    claude_path: String,\n    args: Vec<String>,\n    project_path: String,\n    task: String,\n    execution_model: String,\n    db: State<'_, AgentDb>,\n    registry: State<'_, crate::process::ProcessRegistryState>,\n) -> Result<i64, String> {\n    // Build the command\n    let mut cmd = create_agent_system_command(&claude_path, args, &project_path);\n\n    // Spawn the process\n    info!(\"🚀 Spawning Claude system process...\");\n    let mut child = cmd.spawn().map_err(|e| {\n        error!(\"❌ Failed to spawn Claude process: {}\", e);\n        format!(\"Failed to spawn Claude: {}\", e)\n    })?;\n\n    info!(\"🔌 Using Stdio::null() for stdin - no input expected\");\n\n    // Get the PID and register the process\n    let pid = child.id().unwrap_or(0);\n    let now = chrono::Utc::now().to_rfc3339();\n    info!(\"✅ Claude process spawned successfully with PID: {}\", pid);\n\n    // Update the database with PID and status\n    {\n        let conn = db.0.lock().map_err(|e| e.to_string())?;\n        conn.execute(\n            \"UPDATE agent_runs SET status = 'running', pid = ?1, process_started_at = ?2 WHERE id = ?3\",\n            params![pid as i64, now, run_id],\n        ).map_err(|e| e.to_string())?;\n        info!(\"📝 Updated database with running status and PID\");\n    }\n\n    // Get stdout and stderr\n    let stdout = child.stdout.take().ok_or(\"Failed to get stdout\")?;\n    let stderr = child.stderr.take().ok_or(\"Failed to get stderr\")?;\n    info!(\"📡 Set up stdout/stderr readers\");\n\n    // Create readers\n    let stdout_reader = TokioBufReader::new(stdout);\n    let stderr_reader = TokioBufReader::new(stderr);\n\n    // Create variables we need for the spawned tasks\n    let app_dir = app\n        .path()\n        .app_data_dir()\n        .expect(\"Failed to get app data dir\");\n    let db_path = app_dir.join(\"agents.db\");\n\n    // Shared state for collecting session ID and live output\n    let session_id = std::sync::Arc::new(Mutex::new(String::new()));\n    let live_output = std::sync::Arc::new(Mutex::new(String::new()));\n    let start_time = std::time::Instant::now();\n\n    // Spawn tasks to read stdout and stderr\n    let app_handle = app.clone();\n    let session_id_clone = session_id.clone();\n    let live_output_clone = live_output.clone();\n    let registry_clone = registry.0.clone();\n    let first_output = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\n    let first_output_clone = first_output.clone();\n    let db_path_for_stdout = db_path.clone(); // Clone the db_path for the stdout task\n\n    let stdout_task = tokio::spawn(async move {\n        info!(\"📖 Starting to read Claude stdout...\");\n        let mut lines = stdout_reader.lines();\n        let mut line_count = 0;\n\n        while let Ok(Some(line)) = lines.next_line().await {\n            line_count += 1;\n\n            // Log first output\n            if !first_output_clone.load(std::sync::atomic::Ordering::Relaxed) {\n                info!(\n                    \"🎉 First output received from Claude process! Line: {}\",\n                    line\n                );\n                first_output_clone.store(true, std::sync::atomic::Ordering::Relaxed);\n            }\n\n            if line_count <= 5 {\n                info!(\"stdout[{}]: {}\", line_count, line);\n            } else {\n                debug!(\"stdout[{}]: {}\", line_count, line);\n            }\n\n            // Store live output in both local buffer and registry\n            if let Ok(mut output) = live_output_clone.lock() {\n                output.push_str(&line);\n                output.push('\\n');\n            }\n\n            // Also store in process registry for cross-session access\n            let _ = registry_clone.append_live_output(run_id, &line);\n\n            // Extract session ID from JSONL output\n            if let Ok(json) = serde_json::from_str::<JsonValue>(&line) {\n                // Claude Code uses \"session_id\" (underscore), not \"sessionId\"\n                if json.get(\"type\").and_then(|t| t.as_str()) == Some(\"system\")\n                    && json.get(\"subtype\").and_then(|s| s.as_str()) == Some(\"init\")\n                {\n                    if let Some(sid) = json.get(\"session_id\").and_then(|s| s.as_str()) {\n                        if let Ok(mut current_session_id) = session_id_clone.lock() {\n                            if current_session_id.is_empty() {\n                                *current_session_id = sid.to_string();\n                                info!(\"🔑 Extracted session ID: {}\", sid);\n\n                                // Update database immediately with session ID\n                                if let Ok(conn) = Connection::open(&db_path_for_stdout) {\n                                    match conn.execute(\n                                        \"UPDATE agent_runs SET session_id = ?1 WHERE id = ?2\",\n                                        params![sid, run_id],\n                                    ) {\n                                        Ok(rows) => {\n                                            if rows > 0 {\n                                                info!(\"✅ Updated agent run {} with session ID immediately\", run_id);\n                                            }\n                                        }\n                                        Err(e) => {\n                                            error!(\n                                                \"❌ Failed to update session ID immediately: {}\",\n                                                e\n                                            );\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            // Emit the line to the frontend with run_id for isolation\n            let _ = app_handle.emit(&format!(\"agent-output:{}\", run_id), &line);\n            // Also emit to the generic event for backward compatibility\n            let _ = app_handle.emit(\"agent-output\", &line);\n        }\n\n        info!(\n            \"📖 Finished reading Claude stdout. Total lines: {}\",\n            line_count\n        );\n    });\n\n    let app_handle_stderr = app.clone();\n    let first_error = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\n    let first_error_clone = first_error.clone();\n\n    let stderr_task = tokio::spawn(async move {\n        info!(\"📖 Starting to read Claude stderr...\");\n        let mut lines = stderr_reader.lines();\n        let mut error_count = 0;\n\n        while let Ok(Some(line)) = lines.next_line().await {\n            error_count += 1;\n\n            // Log first error\n            if !first_error_clone.load(std::sync::atomic::Ordering::Relaxed) {\n                warn!(\"⚠️ First error output from Claude process! Line: {}\", line);\n                first_error_clone.store(true, std::sync::atomic::Ordering::Relaxed);\n            }\n\n            error!(\"stderr[{}]: {}\", error_count, line);\n            // Emit error lines to the frontend with run_id for isolation\n            let _ = app_handle_stderr.emit(&format!(\"agent-error:{}\", run_id), &line);\n            // Also emit to the generic event for backward compatibility\n            let _ = app_handle_stderr.emit(\"agent-error\", &line);\n        }\n\n        if error_count > 0 {\n            warn!(\n                \"📖 Finished reading Claude stderr. Total error lines: {}\",\n                error_count\n            );\n        } else {\n            info!(\"📖 Finished reading Claude stderr. No errors.\");\n        }\n    });\n\n    // Register the process in the registry for live output tracking (after stdout/stderr setup)\n    registry\n        .0\n        .register_process(\n            run_id,\n            agent_id,\n            agent_name,\n            pid,\n            project_path.clone(),\n            task.clone(),\n            execution_model.clone(),\n            child,\n        )\n        .map_err(|e| format!(\"Failed to register process: {}\", e))?;\n    info!(\"📋 Registered process in registry\");\n\n    let db_path_for_monitor = db_path.clone(); // Clone for the monitor task\n\n    // Monitor process status and wait for completion\n    tokio::spawn(async move {\n        info!(\"🕐 Starting process monitoring...\");\n\n        // Wait for first output with timeout\n        for i in 0..300 {\n            // 30 seconds (300 * 100ms)\n            if first_output.load(std::sync::atomic::Ordering::Relaxed) {\n                info!(\n                    \"✅ Output detected after {}ms, continuing normal execution\",\n                    i * 100\n                );\n                break;\n            }\n\n            if i == 299 {\n                warn!(\"⏰ TIMEOUT: No output from Claude process after 30 seconds\");\n                warn!(\"💡 This usually means:\");\n                warn!(\"   1. Claude process is waiting for user input\");\n                warn!(\"   3. Claude failed to initialize but didn't report an error\");\n                warn!(\"   4. Network connectivity issues\");\n                warn!(\"   5. Authentication issues (API key not found/invalid)\");\n\n                // Process timed out - kill it via PID\n                warn!(\n                    \"🔍 Process likely stuck waiting for input, attempting to kill PID: {}\",\n                    pid\n                );\n                let kill_result = std::process::Command::new(\"kill\")\n                    .arg(\"-TERM\")\n                    .arg(pid.to_string())\n                    .output();\n\n                match kill_result {\n                    Ok(output) if output.status.success() => {\n                        warn!(\"🔍 Successfully sent TERM signal to process\");\n                    }\n                    Ok(_) => {\n                        warn!(\"🔍 Failed to kill process with TERM, trying KILL\");\n                        let _ = std::process::Command::new(\"kill\")\n                            .arg(\"-KILL\")\n                            .arg(pid.to_string())\n                            .output();\n                    }\n                    Err(e) => {\n                        warn!(\"🔍 Error killing process: {}\", e);\n                    }\n                }\n\n                // Update database\n                if let Ok(conn) = Connection::open(&db_path_for_monitor) {\n                    let _ = conn.execute(\n                        \"UPDATE agent_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP WHERE id = ?1\",\n                        params![run_id],\n                    );\n                }\n\n                let _ = app.emit(\"agent-complete\", false);\n                let _ = app.emit(&format!(\"agent-complete:{}\", run_id), false);\n                return;\n            }\n\n            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n        }\n\n        // Wait for reading tasks to complete\n        info!(\"⏳ Waiting for stdout/stderr reading to complete...\");\n        let _ = stdout_task.await;\n        let _ = stderr_task.await;\n\n        let duration_ms = start_time.elapsed().as_millis() as i64;\n        info!(\"⏱️ Process execution took {} ms\", duration_ms);\n\n        // Get the session ID that was extracted\n        let extracted_session_id = if let Ok(sid) = session_id.lock() {\n            sid.clone()\n        } else {\n            String::new()\n        };\n\n        // Wait for process completion and update status\n        info!(\"✅ Claude process execution monitoring complete\");\n\n        // Update the run record with session ID and mark as completed - open a new connection\n        if let Ok(conn) = Connection::open(&db_path_for_monitor) {\n            info!(\n                \"🔄 Updating database with extracted session ID: {}\",\n                extracted_session_id\n            );\n            match conn.execute(\n                \"UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2\",\n                params![extracted_session_id, run_id],\n            ) {\n                Ok(rows_affected) => {\n                    if rows_affected > 0 {\n                        info!(\"✅ Successfully updated agent run {} with session ID: {}\", run_id, extracted_session_id);\n                    } else {\n                        warn!(\"⚠️ No rows affected when updating agent run {} with session ID\", run_id);\n                    }\n                }\n                Err(e) => {\n                    error!(\"❌ Failed to update agent run {} with session ID: {}\", run_id, e);\n                }\n            }\n        } else {\n            error!(\n                \"❌ Failed to open database to update session ID for run {}\",\n                run_id\n            );\n        }\n\n        // Cleanup will be handled by the cleanup_finished_processes function\n\n        let _ = app.emit(\"agent-complete\", true);\n        let _ = app.emit(&format!(\"agent-complete:{}\", run_id), true);\n    });\n\n    Ok(run_id)\n}\n\n/// List all currently running agent sessions\n#[tauri::command]\npub async fn list_running_sessions(\n    db: State<'_, AgentDb>,\n    registry: State<'_, crate::process::ProcessRegistryState>,\n) -> Result<Vec<AgentRun>, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // First get all running sessions from the database\n    let mut stmt = conn.prepare(\n        \"SELECT id, agent_id, agent_name, agent_icon, task, model, project_path, session_id, status, pid, process_started_at, created_at, completed_at \n         FROM agent_runs WHERE status = 'running' ORDER BY process_started_at DESC\"\n    ).map_err(|e| e.to_string())?;\n\n    let mut runs = stmt\n        .query_map([], |row| {\n            Ok(AgentRun {\n                id: Some(row.get(0)?),\n                agent_id: row.get(1)?,\n                agent_name: row.get(2)?,\n                agent_icon: row.get(3)?,\n                task: row.get(4)?,\n                model: row.get(5)?,\n                project_path: row.get(6)?,\n                session_id: row.get(7)?,\n                status: row\n                    .get::<_, String>(8)\n                    .unwrap_or_else(|_| \"pending\".to_string()),\n                pid: row\n                    .get::<_, Option<i64>>(9)\n                    .ok()\n                    .flatten()\n                    .map(|p| p as u32),\n                process_started_at: row.get(10)?,\n                created_at: row.get(11)?,\n                completed_at: row.get(12)?,\n            })\n        })\n        .map_err(|e| e.to_string())?\n        .collect::<Result<Vec<_>, _>>()\n        .map_err(|e| e.to_string())?;\n\n    drop(stmt);\n    drop(conn);\n\n    // Cross-check with the process registry to ensure accuracy\n    // Get actually running processes from the registry\n    let registry_processes = registry.0.get_running_agent_processes()?;\n    let registry_run_ids: std::collections::HashSet<i64> =\n        registry_processes.iter().map(|p| p.run_id).collect();\n\n    // Filter out any database entries that aren't actually running in the registry\n    // This handles cases where processes crashed without updating the database\n    runs.retain(|run| {\n        if let Some(run_id) = run.id {\n            registry_run_ids.contains(&run_id)\n        } else {\n            false\n        }\n    });\n\n    Ok(runs)\n}\n\n/// Kill a running agent session\n#[tauri::command]\npub async fn kill_agent_session(\n    app: AppHandle,\n    db: State<'_, AgentDb>,\n    registry: State<'_, crate::process::ProcessRegistryState>,\n    run_id: i64,\n) -> Result<bool, String> {\n    info!(\"Attempting to kill agent session {}\", run_id);\n\n    // First try to kill using the process registry\n    let killed_via_registry = match registry.0.kill_process(run_id).await {\n        Ok(success) => {\n            if success {\n                info!(\"Successfully killed process {} via registry\", run_id);\n                true\n            } else {\n                warn!(\"Process {} not found in registry\", run_id);\n                false\n            }\n        }\n        Err(e) => {\n            warn!(\"Failed to kill process {} via registry: {}\", run_id, e);\n            false\n        }\n    };\n\n    // If registry kill didn't work, try fallback with PID from database\n    if !killed_via_registry {\n        let pid_result = {\n            let conn = db.0.lock().map_err(|e| e.to_string())?;\n            conn.query_row(\n                \"SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'\",\n                params![run_id],\n                |row| row.get::<_, Option<i64>>(0),\n            )\n            .map_err(|e| e.to_string())?\n        };\n\n        if let Some(pid) = pid_result {\n            info!(\"Attempting fallback kill for PID {} from database\", pid);\n            let _ = registry.0.kill_process_by_pid(run_id, pid as u32)?;\n        }\n    }\n\n    // Update the database to mark as cancelled\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n    let updated = conn.execute(\n        \"UPDATE agent_runs SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP WHERE id = ?1 AND status = 'running'\",\n        params![run_id],\n    ).map_err(|e| e.to_string())?;\n\n    // Emit cancellation event with run_id for proper isolation\n    let _ = app.emit(&format!(\"agent-cancelled:{}\", run_id), true);\n\n    Ok(updated > 0 || killed_via_registry)\n}\n\n/// Get the status of a specific agent session\n#[tauri::command]\npub async fn get_session_status(\n    db: State<'_, AgentDb>,\n    run_id: i64,\n) -> Result<Option<String>, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    match conn.query_row(\n        \"SELECT status FROM agent_runs WHERE id = ?1\",\n        params![run_id],\n        |row| row.get::<_, String>(0),\n    ) {\n        Ok(status) => Ok(Some(status)),\n        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),\n        Err(e) => Err(e.to_string()),\n    }\n}\n\n/// Cleanup finished processes and update their status\n#[tauri::command]\npub async fn cleanup_finished_processes(db: State<'_, AgentDb>) -> Result<Vec<i64>, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Get all running processes\n    let mut stmt = conn\n        .prepare(\"SELECT id, pid FROM agent_runs WHERE status = 'running' AND pid IS NOT NULL\")\n        .map_err(|e| e.to_string())?;\n\n    let running_processes = stmt\n        .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))\n        .map_err(|e| e.to_string())?\n        .collect::<Result<Vec<_>, _>>()\n        .map_err(|e| e.to_string())?;\n\n    drop(stmt);\n\n    let mut cleaned_up = Vec::new();\n\n    for (run_id, pid) in running_processes {\n        // Check if the process is still running\n        let is_running = if cfg!(target_os = \"windows\") {\n            // On Windows, use tasklist to check if process exists\n            match std::process::Command::new(\"tasklist\")\n                .args([\"/FI\", &format!(\"PID eq {}\", pid)])\n                .args([\"/FO\", \"CSV\"])\n                .output()\n            {\n                Ok(output) => {\n                    let output_str = String::from_utf8_lossy(&output.stdout);\n                    output_str.lines().count() > 1 // Header + process line if exists\n                }\n                Err(_) => false,\n            }\n        } else {\n            // On Unix-like systems, use kill -0 to check if process exists\n            match std::process::Command::new(\"kill\")\n                .args([\"-0\", &pid.to_string()])\n                .output()\n            {\n                Ok(output) => output.status.success(),\n                Err(_) => false,\n            }\n        };\n\n        if !is_running {\n            // Process has finished, update status\n            let updated = conn.execute(\n                \"UPDATE agent_runs SET status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?1\",\n                params![run_id],\n            ).map_err(|e| e.to_string())?;\n\n            if updated > 0 {\n                cleaned_up.push(run_id);\n                info!(\n                    \"Marked agent run {} as completed (PID {} no longer running)\",\n                    run_id, pid\n                );\n            }\n        }\n    }\n\n    Ok(cleaned_up)\n}\n\n/// Get live output from a running process\n#[tauri::command]\npub async fn get_live_session_output(\n    registry: State<'_, crate::process::ProcessRegistryState>,\n    run_id: i64,\n) -> Result<String, String> {\n    registry.0.get_live_output(run_id)\n}\n\n/// Get real-time output for a running session by reading its JSONL file with live output fallback\n#[tauri::command]\npub async fn get_session_output(\n    db: State<'_, AgentDb>,\n    registry: State<'_, crate::process::ProcessRegistryState>,\n    run_id: i64,\n) -> Result<String, String> {\n    // Get the session information\n    let run = get_agent_run(db, run_id).await?;\n\n    // If no session ID yet, try to get live output from registry\n    if run.session_id.is_empty() {\n        let live_output = registry.0.get_live_output(run_id)?;\n        if !live_output.is_empty() {\n            return Ok(live_output);\n        }\n        return Ok(String::new());\n    }\n\n    // Get the Claude directory\n    let claude_dir = dirs::home_dir()\n        .ok_or(\"Failed to get home directory\")?\n        .join(\".claude\");\n\n    // Find the correct project directory by searching for the session file\n    let projects_dir = claude_dir.join(\"projects\");\n\n    // Check if projects directory exists\n    if !projects_dir.exists() {\n        log::error!(\"Projects directory not found at: {:?}\", projects_dir);\n        return Err(\"Projects directory not found\".to_string());\n    }\n\n    // Search for the session file in all project directories\n    let mut session_file_path = None;\n    log::info!(\n        \"Searching for session file {} in all project directories\",\n        run.session_id\n    );\n\n    if let Ok(entries) = std::fs::read_dir(&projects_dir) {\n        for entry in entries.filter_map(Result::ok) {\n            let path = entry.path();\n            if path.is_dir() {\n                let dir_name = path.file_name().unwrap_or_default().to_string_lossy();\n                log::debug!(\"Checking project directory: {}\", dir_name);\n\n                let potential_session_file = path.join(format!(\"{}.jsonl\", run.session_id));\n                if potential_session_file.exists() {\n                    log::info!(\"Found session file at: {:?}\", potential_session_file);\n                    session_file_path = Some(potential_session_file);\n                    break;\n                } else {\n                    log::debug!(\"Session file not found in: {}\", dir_name);\n                }\n            }\n        }\n    } else {\n        log::error!(\"Failed to read projects directory\");\n    }\n\n    // If we found the session file, read it\n    if let Some(session_path) = session_file_path {\n        match tokio::fs::read_to_string(&session_path).await {\n            Ok(content) => Ok(content),\n            Err(e) => {\n                log::error!(\n                    \"Failed to read session file {}: {}\",\n                    session_path.display(),\n                    e\n                );\n                // Fallback to live output if file read fails\n                let live_output = registry.0.get_live_output(run_id)?;\n                Ok(live_output)\n            }\n        }\n    } else {\n        // If session file not found, try the old method as fallback\n        log::warn!(\n            \"Session file not found for {}, trying legacy method\",\n            run.session_id\n        );\n        match read_session_jsonl(&run.session_id, &run.project_path).await {\n            Ok(content) => Ok(content),\n            Err(_) => {\n                // Final fallback to live output\n                let live_output = registry.0.get_live_output(run_id)?;\n                Ok(live_output)\n            }\n        }\n    }\n}\n\n/// Stream real-time session output by watching the JSONL file\n#[tauri::command]\npub async fn stream_session_output(\n    app: AppHandle,\n    db: State<'_, AgentDb>,\n    run_id: i64,\n) -> Result<(), String> {\n    // Get the session information\n    let run = get_agent_run(db, run_id).await?;\n\n    // If no session ID yet, can't stream\n    if run.session_id.is_empty() {\n        return Err(\"Session not started yet\".to_string());\n    }\n\n    let session_id = run.session_id.clone();\n    let project_path = run.project_path.clone();\n\n    // Spawn a task to monitor the file\n    tokio::spawn(async move {\n        let claude_dir = match dirs::home_dir() {\n            Some(home) => home.join(\".claude\").join(\"projects\"),\n            None => return,\n        };\n\n        let encoded_project = project_path.replace('/', \"-\");\n        let project_dir = claude_dir.join(&encoded_project);\n        let session_file = project_dir.join(format!(\"{}.jsonl\", session_id));\n\n        let mut last_size = 0u64;\n\n        // Monitor file changes continuously while session is running\n        loop {\n            if session_file.exists() {\n                if let Ok(metadata) = tokio::fs::metadata(&session_file).await {\n                    let current_size = metadata.len();\n\n                    if current_size > last_size {\n                        // File has grown, read new content\n                        if let Ok(content) = tokio::fs::read_to_string(&session_file).await {\n                            let _ = app\n                                .emit(\"session-output-update\", &format!(\"{}:{}\", run_id, content));\n                        }\n                        last_size = current_size;\n                    }\n                }\n            } else {\n                // If session file doesn't exist yet, keep waiting\n                tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;\n                continue;\n            }\n\n            // Check if the session is still running by querying the database\n            // If the session is no longer running, stop streaming\n            if let Ok(conn) = rusqlite::Connection::open(\n                app.path()\n                    .app_data_dir()\n                    .expect(\"Failed to get app data dir\")\n                    .join(\"agents.db\"),\n            ) {\n                if let Ok(status) = conn.query_row(\n                    \"SELECT status FROM agent_runs WHERE id = ?1\",\n                    rusqlite::params![run_id],\n                    |row| row.get::<_, String>(0),\n                ) {\n                    if status != \"running\" {\n                        debug!(\"Session {} is no longer running, stopping stream\", run_id);\n                        break;\n                    }\n                } else {\n                    // If we can't query the status, assume it's still running\n                    debug!(\n                        \"Could not query session status for {}, continuing stream\",\n                        run_id\n                    );\n                }\n            }\n\n            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n        }\n\n        debug!(\"Stopped streaming for session {}\", run_id);\n    });\n\n    Ok(())\n}\n\n/// Export a single agent to JSON format\n#[tauri::command]\npub async fn export_agent(db: State<'_, AgentDb>, id: i64) -> Result<String, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Fetch the agent\n    let agent = conn\n        .query_row(\n            \"SELECT name, icon, system_prompt, default_task, model, hooks FROM agents WHERE id = ?1\",\n            params![id],\n            |row| {\n                Ok(serde_json::json!({\n                    \"name\": row.get::<_, String>(0)?,\n                    \"icon\": row.get::<_, String>(1)?,\n                    \"system_prompt\": row.get::<_, String>(2)?,\n                    \"default_task\": row.get::<_, Option<String>>(3)?,\n                    \"model\": row.get::<_, String>(4)?,\n                    \"hooks\": row.get::<_, Option<String>>(5)?\n                }))\n            },\n        )\n        .map_err(|e| format!(\"Failed to fetch agent: {}\", e))?;\n\n    // Create the export wrapper\n    let export_data = serde_json::json!({\n        \"version\": 1,\n        \"exported_at\": chrono::Utc::now().to_rfc3339(),\n        \"agent\": agent\n    });\n\n    // Convert to pretty JSON string\n    serde_json::to_string_pretty(&export_data)\n        .map_err(|e| format!(\"Failed to serialize agent: {}\", e))\n}\n\n/// Export agent to file with native dialog\n#[tauri::command]\npub async fn export_agent_to_file(\n    db: State<'_, AgentDb>,\n    id: i64,\n    file_path: String,\n) -> Result<(), String> {\n    // Get the JSON data\n    let json_data = export_agent(db, id).await?;\n\n    // Write to file\n    std::fs::write(&file_path, json_data).map_err(|e| format!(\"Failed to write file: {}\", e))?;\n\n    Ok(())\n}\n\n/// Get the stored Claude binary path from settings\n#[tauri::command]\npub async fn get_claude_binary_path(db: State<'_, AgentDb>) -> Result<Option<String>, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    match conn.query_row(\n        \"SELECT value FROM app_settings WHERE key = 'claude_binary_path'\",\n        [],\n        |row| row.get::<_, String>(0),\n    ) {\n        Ok(path) => Ok(Some(path)),\n        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),\n        Err(e) => Err(format!(\"Failed to get Claude binary path: {}\", e)),\n    }\n}\n\n/// Set the Claude binary path in settings\n#[tauri::command]\npub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Result<(), String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Validate that the path exists and is executable\n    let path_buf = std::path::PathBuf::from(&path);\n    if !path_buf.exists() {\n        return Err(format!(\"File does not exist: {}\", path));\n    }\n\n    // Check if it's executable (on Unix systems)\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        let metadata = std::fs::metadata(&path_buf)\n            .map_err(|e| format!(\"Failed to read file metadata: {}\", e))?;\n        let permissions = metadata.permissions();\n        if permissions.mode() & 0o111 == 0 {\n            return Err(format!(\"File is not executable: {}\", path));\n        }\n    }\n\n    // Insert or update the setting\n    conn.execute(\n        \"INSERT INTO app_settings (key, value) VALUES ('claude_binary_path', ?1)\n         ON CONFLICT(key) DO UPDATE SET value = ?1\",\n        params![path],\n    )\n    .map_err(|e| format!(\"Failed to save Claude binary path: {}\", e))?;\n\n    Ok(())\n}\n\n/// List all available Claude installations on the system\n#[tauri::command]\npub async fn list_claude_installations(\n    _app: AppHandle,\n) -> Result<Vec<crate::claude_binary::ClaudeInstallation>, String> {\n    let installations = crate::claude_binary::discover_claude_installations();\n\n    if installations.is_empty() {\n        return Err(\"No Claude Code installations found on the system\".to_string());\n    }\n\n    Ok(installations)\n}\n\n/// Helper function to create a tokio Command with proper environment variables\n/// This ensures commands like Claude can find Node.js and other dependencies\nfn create_command_with_env(program: &str) -> Command {\n    // Convert std::process::Command to tokio::process::Command\n    let _std_cmd = crate::claude_binary::create_command_with_env(program);\n\n    // Create a new tokio Command from the program path\n    let mut tokio_cmd = Command::new(program);\n\n    // Copy over all environment variables from the std::process::Command\n    // This is a workaround since we can't directly convert between the two types\n    for (key, value) in std::env::vars() {\n        if key == \"PATH\"\n            || key == \"HOME\"\n            || key == \"USER\"\n            || key == \"SHELL\"\n            || key == \"LANG\"\n            || key == \"LC_ALL\"\n            || key.starts_with(\"LC_\")\n            || key == \"NODE_PATH\"\n            || key == \"NVM_DIR\"\n            || key == \"NVM_BIN\"\n            || key == \"HOMEBREW_PREFIX\"\n            || key == \"HOMEBREW_CELLAR\"\n        {\n            tokio_cmd.env(&key, &value);\n        }\n    }\n\n    // Add NVM support if the program is in an NVM directory\n    if program.contains(\"/.nvm/versions/node/\") {\n        if let Some(node_bin_dir) = std::path::Path::new(program).parent() {\n            let current_path = std::env::var(\"PATH\").unwrap_or_default();\n            let node_bin_str = node_bin_dir.to_string_lossy();\n            if !current_path.contains(&node_bin_str.as_ref()) {\n                let new_path = format!(\"{}:{}\", node_bin_str, current_path);\n                tokio_cmd.env(\"PATH\", new_path);\n            }\n        }\n    }\n\n    // Ensure PATH contains common Homebrew locations\n    if let Ok(existing_path) = std::env::var(\"PATH\") {\n        let mut paths: Vec<&str> = existing_path.split(':').collect();\n        for p in [\"/opt/homebrew/bin\", \"/usr/local/bin\", \"/usr/bin\", \"/bin\"].iter() {\n            if !paths.contains(p) {\n                paths.push(p);\n            }\n        }\n        let joined = paths.join(\":\");\n        tokio_cmd.env(\"PATH\", joined);\n    } else {\n        tokio_cmd.env(\"PATH\", \"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin\");\n    }\n\n    tokio_cmd\n}\n\n/// Import an agent from JSON data\n#[tauri::command]\npub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<Agent, String> {\n    // Parse the JSON data\n    let export_data: AgentExport =\n        serde_json::from_str(&json_data).map_err(|e| format!(\"Invalid JSON format: {}\", e))?;\n\n    // Validate version\n    if export_data.version != 1 {\n        return Err(format!(\n            \"Unsupported export version: {}. This version of the app only supports version 1.\",\n            export_data.version\n        ));\n    }\n\n    let agent_data = export_data.agent;\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Check if an agent with the same name already exists\n    let existing_count: i64 = conn\n        .query_row(\n            \"SELECT COUNT(*) FROM agents WHERE name = ?1\",\n            params![agent_data.name],\n            |row| row.get(0),\n        )\n        .map_err(|e| e.to_string())?;\n\n    // If agent with same name exists, append a suffix\n    let final_name = if existing_count > 0 {\n        format!(\"{} (Imported)\", agent_data.name)\n    } else {\n        agent_data.name\n    };\n\n    // Create the agent\n    conn.execute(\n        \"INSERT INTO agents (name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks) VALUES (?1, ?2, ?3, ?4, ?5, 1, 1, 0, ?6)\",\n        params![\n            final_name,\n            agent_data.icon,\n            agent_data.system_prompt,\n            agent_data.default_task,\n            agent_data.model,\n            agent_data.hooks\n        ],\n    )\n    .map_err(|e| format!(\"Failed to create agent: {}\", e))?;\n\n    let id = conn.last_insert_rowid();\n\n    // Fetch the created agent\n    let agent = conn\n        .query_row(\n            \"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks, created_at, updated_at FROM agents WHERE id = ?1\",\n            params![id],\n            |row| {\n                Ok(Agent {\n                    id: Some(row.get(0)?),\n                    name: row.get(1)?,\n                    icon: row.get(2)?,\n                    system_prompt: row.get(3)?,\n                    default_task: row.get(4)?,\n                    model: row.get(5)?,\n                    enable_file_read: row.get(6)?,\n                    enable_file_write: row.get(7)?,\n                    enable_network: row.get(8)?,\n                    hooks: row.get(9)?,\n                    created_at: row.get(10)?,\n                    updated_at: row.get(11)?,\n                })\n            },\n        )\n        .map_err(|e| format!(\"Failed to fetch created agent: {}\", e))?;\n\n    Ok(agent)\n}\n\n/// Import agent from file\n#[tauri::command]\npub async fn import_agent_from_file(\n    db: State<'_, AgentDb>,\n    file_path: String,\n) -> Result<Agent, String> {\n    // Read the file\n    let mut json_data =\n        std::fs::read_to_string(&file_path).map_err(|e| format!(\"Failed to read file: {}\", e))?;\n\n    // Normalize potential BOM and whitespace issues\n    if json_data.starts_with('\\u{feff}') {\n        json_data = json_data.trim_start_matches('\\u{feff}').to_string();\n    }\n    // Also trim leading/trailing whitespace to avoid parse surprises\n    json_data = json_data.trim().to_string();\n\n    // Import the agent\n    import_agent(db, json_data).await\n}\n\n// GitHub Agent Import functionality\n\n/// Represents a GitHub agent file from the API\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct GitHubAgentFile {\n    pub name: String,\n    pub path: String,\n    pub download_url: String,\n    pub size: i64,\n    pub sha: String,\n}\n\n/// Represents the GitHub API response for directory contents\n#[derive(Debug, Deserialize)]\nstruct GitHubApiResponse {\n    name: String,\n    path: String,\n    sha: String,\n    size: i64,\n    download_url: Option<String>,\n    #[serde(rename = \"type\")]\n    file_type: String,\n}\n\n/// Fetch list of agents from GitHub repository\n#[tauri::command]\npub async fn fetch_github_agents() -> Result<Vec<GitHubAgentFile>, String> {\n    info!(\"Fetching agents from GitHub repository...\");\n\n    let client = reqwest::Client::new();\n    let url = \"https://api.github.com/repos/getAsterisk/opcode/contents/cc_agents\";\n\n    let response = client\n        .get(url)\n        .header(\"Accept\", \"application/vnd.github+json\")\n        .header(\"User-Agent\", \"opcode-App\")\n        .send()\n        .await\n        .map_err(|e| format!(\"Failed to fetch from GitHub: {}\", e))?;\n\n    if !response.status().is_success() {\n        let status = response.status();\n        let error_text = response.text().await.unwrap_or_default();\n        return Err(format!(\"GitHub API error ({}): {}\", status, error_text));\n    }\n\n    let api_files: Vec<GitHubApiResponse> = response\n        .json()\n        .await\n        .map_err(|e| format!(\"Failed to parse GitHub response: {}\", e))?;\n\n    // Filter only .opcode.json agent files\n    let agent_files: Vec<GitHubAgentFile> = api_files\n        .into_iter()\n        .filter(|f| f.name.ends_with(\".opcode.json\") && f.file_type == \"file\")\n        .filter_map(|f| {\n            f.download_url.map(|download_url| GitHubAgentFile {\n                name: f.name,\n                path: f.path,\n                download_url,\n                size: f.size,\n                sha: f.sha,\n            })\n        })\n        .collect();\n\n    info!(\"Found {} agents on GitHub\", agent_files.len());\n    Ok(agent_files)\n}\n\n/// Fetch and preview a specific agent from GitHub\n#[tauri::command]\npub async fn fetch_github_agent_content(download_url: String) -> Result<AgentExport, String> {\n    info!(\"Fetching agent content from: {}\", download_url);\n\n    let client = reqwest::Client::new();\n    let response = client\n        .get(&download_url)\n        .header(\"Accept\", \"application/json\")\n        .header(\"User-Agent\", \"opcode-App\")\n        .send()\n        .await\n        .map_err(|e| format!(\"Failed to download agent: {}\", e))?;\n\n    if !response.status().is_success() {\n        return Err(format!(\n            \"Failed to download agent: HTTP {}\",\n            response.status()\n        ));\n    }\n\n    let json_text = response\n        .text()\n        .await\n        .map_err(|e| format!(\"Failed to read response: {}\", e))?;\n\n    // Parse and validate the agent data\n    let export_data: AgentExport = serde_json::from_str(&json_text)\n        .map_err(|e| format!(\"Invalid agent JSON format: {}\", e))?;\n\n    // Validate version\n    if export_data.version != 1 {\n        return Err(format!(\n            \"Unsupported agent version: {}\",\n            export_data.version\n        ));\n    }\n\n    Ok(export_data)\n}\n\n/// Import an agent directly from GitHub\n#[tauri::command]\npub async fn import_agent_from_github(\n    db: State<'_, AgentDb>,\n    download_url: String,\n) -> Result<Agent, String> {\n    info!(\"Importing agent from GitHub: {}\", download_url);\n\n    // First, fetch the agent content\n    let export_data = fetch_github_agent_content(download_url).await?;\n\n    // Convert to JSON string and use existing import logic\n    let json_data = serde_json::to_string(&export_data)\n        .map_err(|e| format!(\"Failed to serialize agent data: {}\", e))?;\n\n    // Import using existing function\n    import_agent(db, json_data).await\n}\n\n/// Load agent session history from JSONL file\n/// Similar to Claude Code's load_session_history, but searches across all project directories\n#[tauri::command]\npub async fn load_agent_session_history(\n    session_id: String,\n) -> Result<Vec<serde_json::Value>, String> {\n    log::info!(\"Loading agent session history for session: {}\", session_id);\n\n    let claude_dir = dirs::home_dir()\n        .ok_or(\"Failed to get home directory\")?\n        .join(\".claude\");\n\n    let projects_dir = claude_dir.join(\"projects\");\n\n    if !projects_dir.exists() {\n        log::error!(\"Projects directory not found at: {:?}\", projects_dir);\n        return Err(\"Projects directory not found\".to_string());\n    }\n\n    // Search for the session file in all project directories\n    let mut session_file_path = None;\n    log::info!(\n        \"Searching for session file {} in all project directories\",\n        session_id\n    );\n\n    if let Ok(entries) = std::fs::read_dir(&projects_dir) {\n        for entry in entries.filter_map(Result::ok) {\n            let path = entry.path();\n            if path.is_dir() {\n                let dir_name = path.file_name().unwrap_or_default().to_string_lossy();\n                log::debug!(\"Checking project directory: {}\", dir_name);\n\n                let potential_session_file = path.join(format!(\"{}.jsonl\", session_id));\n                if potential_session_file.exists() {\n                    log::info!(\"Found session file at: {:?}\", potential_session_file);\n                    session_file_path = Some(potential_session_file);\n                    break;\n                } else {\n                    log::debug!(\"Session file not found in: {}\", dir_name);\n                }\n            }\n        }\n    } else {\n        log::error!(\"Failed to read projects directory\");\n    }\n\n    if let Some(session_path) = session_file_path {\n        let file = std::fs::File::open(&session_path)\n            .map_err(|e| format!(\"Failed to open session file: {}\", e))?;\n\n        let reader = BufReader::new(file);\n        let mut messages = Vec::new();\n\n        for line in reader.lines() {\n            if let Ok(line) = line {\n                if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) {\n                    messages.push(json);\n                }\n            }\n        }\n\n        Ok(messages)\n    } else {\n        Err(format!(\"Session file not found: {}\", session_id))\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/commands/claude.rs",
    "content": "use anyhow::{Context, Result};\nuse serde::{Deserialize, Serialize};\nuse std::fs;\nuse std::io::{BufRead, BufReader};\nuse std::path::PathBuf;\nuse std::process::Stdio;\nuse std::sync::Arc;\nuse std::time::{SystemTime, UNIX_EPOCH};\nuse tauri::{AppHandle, Emitter, Manager};\nuse tokio::process::{Child, Command};\nuse tokio::sync::Mutex;\n\n/// Global state to track current Claude process\npub struct ClaudeProcessState {\n    pub current_process: Arc<Mutex<Option<Child>>>,\n}\n\nimpl Default for ClaudeProcessState {\n    fn default() -> Self {\n        Self {\n            current_process: Arc::new(Mutex::new(None)),\n        }\n    }\n}\n\n/// Represents a project in the ~/.claude/projects directory\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Project {\n    /// The project ID (derived from the directory name)\n    pub id: String,\n    /// The original project path (decoded from the directory name)\n    pub path: String,\n    /// List of session IDs (JSONL file names without extension)\n    pub sessions: Vec<String>,\n    /// Unix timestamp when the project directory was created\n    pub created_at: u64,\n    /// Unix timestamp of the most recent session (if any)\n    pub most_recent_session: Option<u64>,\n}\n\n/// Represents a session with its metadata\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Session {\n    /// The session ID (UUID)\n    pub id: String,\n    /// The project ID this session belongs to\n    pub project_id: String,\n    /// The project path\n    pub project_path: String,\n    /// Optional todo data associated with this session\n    pub todo_data: Option<serde_json::Value>,\n    /// Unix timestamp when the session file was created\n    pub created_at: u64,\n    /// First user message content (if available)\n    pub first_message: Option<String>,\n    /// Timestamp of the first user message (if available)\n    pub message_timestamp: Option<String>,\n}\n\n/// Represents a message entry in the JSONL file\n#[derive(Debug, Deserialize)]\nstruct JsonlEntry {\n    #[serde(rename = \"type\")]\n    #[allow(dead_code)]\n    entry_type: Option<String>,\n    message: Option<MessageContent>,\n    timestamp: Option<String>,\n}\n\n/// Represents the message content\n#[derive(Debug, Deserialize)]\nstruct MessageContent {\n    role: Option<String>,\n    content: Option<String>,\n}\n\n/// Represents the settings from ~/.claude/settings.json\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ClaudeSettings {\n    #[serde(flatten)]\n    pub data: serde_json::Value,\n}\n\nimpl Default for ClaudeSettings {\n    fn default() -> Self {\n        Self {\n            data: serde_json::json!({}),\n        }\n    }\n}\n\n/// Represents the Claude Code version status\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ClaudeVersionStatus {\n    /// Whether Claude Code is installed and working\n    pub is_installed: bool,\n    /// The version string if available\n    pub version: Option<String>,\n    /// The full output from the command\n    pub output: String,\n}\n\n/// Represents a CLAUDE.md file found in the project\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ClaudeMdFile {\n    /// Relative path from the project root\n    pub relative_path: String,\n    /// Absolute path to the file\n    pub absolute_path: String,\n    /// File size in bytes\n    pub size: u64,\n    /// Last modified timestamp\n    pub modified: u64,\n}\n\n/// Represents a file or directory entry\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct FileEntry {\n    /// The name of the file or directory\n    pub name: String,\n    /// The full path\n    pub path: String,\n    /// Whether this is a directory\n    pub is_directory: bool,\n    /// File size in bytes (0 for directories)\n    pub size: u64,\n    /// File extension (if applicable)\n    pub extension: Option<String>,\n}\n\n/// Finds the full path to the claude binary\n/// This is necessary because macOS apps have a limited PATH environment\nfn find_claude_binary(app_handle: &AppHandle) -> Result<String, String> {\n    crate::claude_binary::find_claude_binary(app_handle)\n}\n\n/// Gets the path to the ~/.claude directory\nfn get_claude_dir() -> Result<PathBuf> {\n    dirs::home_dir()\n        .context(\"Could not find home directory\")?\n        .join(\".claude\")\n        .canonicalize()\n        .context(\"Could not find ~/.claude directory\")\n}\n\n/// Gets the actual project path by reading the cwd from the JSONL entries\nfn get_project_path_from_sessions(project_dir: &PathBuf) -> Result<String, String> {\n    // Try to read any JSONL file in the directory\n    let entries = fs::read_dir(project_dir)\n        .map_err(|e| format!(\"Failed to read project directory: {}\", e))?;\n\n    for entry in entries {\n        if let Ok(entry) = entry {\n            let path = entry.path();\n            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some(\"jsonl\") {\n                // Read the JSONL file and find the first line with a valid cwd\n                if let Ok(file) = fs::File::open(&path) {\n                    let reader = BufReader::new(file);\n                    // Check first few lines instead of just the first line\n                    // Some session files may have null cwd in the first line\n                    for line in reader.lines().take(10) {\n                        if let Ok(line_content) = line {\n                            // Parse the JSON and extract cwd\n                            if let Ok(json) =\n                                serde_json::from_str::<serde_json::Value>(&line_content)\n                            {\n                                if let Some(cwd) = json.get(\"cwd\").and_then(|v| v.as_str()) {\n                                    if !cwd.is_empty() {\n                                        return Ok(cwd.to_string());\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Err(\"Could not determine project path from session files\".to_string())\n}\n\n/// Decodes a project directory name back to its original path\n/// The directory names in ~/.claude/projects are encoded paths\n/// DEPRECATED: Use get_project_path_from_sessions instead when possible\nfn decode_project_path(encoded: &str) -> String {\n    // This is a fallback - the encoding isn't reversible when paths contain hyphens\n    // For example: -Users-mufeedvh-dev-jsonl-viewer could be /Users/mufeedvh/dev/jsonl-viewer\n    // or /Users/mufeedvh/dev/jsonl/viewer\n    encoded.replace('-', \"/\")\n}\n\n/// Extracts the first valid user message from a JSONL file\nfn extract_first_user_message(jsonl_path: &PathBuf) -> (Option<String>, Option<String>) {\n    let file = match fs::File::open(jsonl_path) {\n        Ok(file) => file,\n        Err(_) => return (None, None),\n    };\n\n    let reader = BufReader::new(file);\n\n    for line in reader.lines() {\n        if let Ok(line) = line {\n            if let Ok(entry) = serde_json::from_str::<JsonlEntry>(&line) {\n                if let Some(message) = entry.message {\n                    if message.role.as_deref() == Some(\"user\") {\n                        if let Some(content) = message.content {\n                            // Skip if it contains the caveat message\n                            if content.contains(\"Caveat: The messages below were generated by the user while running local commands\") {\n                                continue;\n                            }\n\n                            // Skip if it starts with command tags\n                            if content.starts_with(\"<command-name>\")\n                                || content.starts_with(\"<local-command-stdout>\")\n                            {\n                                continue;\n                            }\n\n                            // Found a valid user message\n                            return (Some(content), entry.timestamp);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    (None, None)\n}\n\n/// Helper function to create a tokio Command with proper environment variables\n/// This ensures commands like Claude can find Node.js and other dependencies\nfn create_command_with_env(program: &str) -> Command {\n    // Convert std::process::Command to tokio::process::Command\n    let _std_cmd = crate::claude_binary::create_command_with_env(program);\n\n    // Create a new tokio Command from the program path\n    let mut tokio_cmd = Command::new(program);\n\n    // Copy over all environment variables\n    for (key, value) in std::env::vars() {\n        if key == \"PATH\"\n            || key == \"HOME\"\n            || key == \"USER\"\n            || key == \"SHELL\"\n            || key == \"LANG\"\n            || key == \"LC_ALL\"\n            || key.starts_with(\"LC_\")\n            || key == \"NODE_PATH\"\n            || key == \"NVM_DIR\"\n            || key == \"NVM_BIN\"\n            || key == \"HOMEBREW_PREFIX\"\n            || key == \"HOMEBREW_CELLAR\"\n        {\n            log::debug!(\"Inheriting env var: {}={}\", key, value);\n            tokio_cmd.env(&key, &value);\n        }\n    }\n\n    // Add NVM support if the program is in an NVM directory\n    if program.contains(\"/.nvm/versions/node/\") {\n        if let Some(node_bin_dir) = std::path::Path::new(program).parent() {\n            let current_path = std::env::var(\"PATH\").unwrap_or_default();\n            let node_bin_str = node_bin_dir.to_string_lossy();\n            if !current_path.contains(&node_bin_str.as_ref()) {\n                let new_path = format!(\"{}:{}\", node_bin_str, current_path);\n                tokio_cmd.env(\"PATH\", new_path);\n            }\n        }\n    }\n\n    // Add Homebrew support if the program is in a Homebrew directory\n    if program.contains(\"/homebrew/\") || program.contains(\"/opt/homebrew/\") {\n        if let Some(program_dir) = std::path::Path::new(program).parent() {\n            let current_path = std::env::var(\"PATH\").unwrap_or_default();\n            let homebrew_bin_str = program_dir.to_string_lossy();\n            if !current_path.contains(&homebrew_bin_str.as_ref()) {\n                let new_path = format!(\"{}:{}\", homebrew_bin_str, current_path);\n                log::debug!(\n                    \"Adding Homebrew bin directory to PATH: {}\",\n                    homebrew_bin_str\n                );\n                tokio_cmd.env(\"PATH\", new_path);\n            }\n        }\n    }\n\n    tokio_cmd\n}\n\n/// Creates a system binary command with the given arguments\nfn create_system_command(claude_path: &str, args: Vec<String>, project_path: &str) -> Command {\n    let mut cmd = create_command_with_env(claude_path);\n\n    // Add all arguments\n    for arg in args {\n        cmd.arg(arg);\n    }\n\n    cmd.current_dir(project_path)\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped());\n\n    cmd\n}\n\n/// Gets the user's home directory path\n#[tauri::command]\npub async fn get_home_directory() -> Result<String, String> {\n    dirs::home_dir()\n        .and_then(|path| path.to_str().map(|s| s.to_string()))\n        .ok_or_else(|| \"Could not determine home directory\".to_string())\n}\n\n/// Lists all projects in the ~/.claude/projects directory\n#[tauri::command]\npub async fn list_projects() -> Result<Vec<Project>, String> {\n    log::info!(\"Listing projects from ~/.claude/projects\");\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let projects_dir = claude_dir.join(\"projects\");\n\n    if !projects_dir.exists() {\n        log::warn!(\"Projects directory does not exist: {:?}\", projects_dir);\n        return Ok(Vec::new());\n    }\n\n    let mut projects = Vec::new();\n\n    // Read all directories in the projects folder\n    let entries = fs::read_dir(&projects_dir)\n        .map_err(|e| format!(\"Failed to read projects directory: {}\", e))?;\n\n    for entry in entries {\n        let entry = entry.map_err(|e| format!(\"Failed to read directory entry: {}\", e))?;\n        let path = entry.path();\n\n        if path.is_dir() {\n            let dir_name = path\n                .file_name()\n                .and_then(|n| n.to_str())\n                .ok_or_else(|| \"Invalid directory name\".to_string())?;\n\n            // Get directory creation time\n            let metadata = fs::metadata(&path)\n                .map_err(|e| format!(\"Failed to read directory metadata: {}\", e))?;\n\n            let created_at = metadata\n                .created()\n                .or_else(|_| metadata.modified())\n                .unwrap_or(SystemTime::UNIX_EPOCH)\n                .duration_since(SystemTime::UNIX_EPOCH)\n                .unwrap_or_default()\n                .as_secs();\n\n            // Get the actual project path from JSONL files\n            let project_path = match get_project_path_from_sessions(&path) {\n                Ok(path) => path,\n                Err(e) => {\n                    log::warn!(\"Failed to get project path from sessions for {}: {}, falling back to decode\", dir_name, e);\n                    decode_project_path(dir_name)\n                }\n            };\n\n            // List all JSONL files (sessions) in this project directory\n            let mut sessions = Vec::new();\n            let mut most_recent_session: Option<u64> = None;\n\n            if let Ok(session_entries) = fs::read_dir(&path) {\n                for session_entry in session_entries.flatten() {\n                    let session_path = session_entry.path();\n                    if session_path.is_file()\n                        && session_path.extension().and_then(|s| s.to_str()) == Some(\"jsonl\")\n                    {\n                        if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str())\n                        {\n                            sessions.push(session_id.to_string());\n\n                            // Track the most recent session timestamp\n                            if let Ok(metadata) = fs::metadata(&session_path) {\n                                let modified = metadata\n                                    .modified()\n                                    .unwrap_or(SystemTime::UNIX_EPOCH)\n                                    .duration_since(UNIX_EPOCH)\n                                    .unwrap_or_default()\n                                    .as_secs();\n\n                                most_recent_session = Some(match most_recent_session {\n                                    Some(current) => current.max(modified),\n                                    None => modified,\n                                });\n                            }\n                        }\n                    }\n                }\n            }\n\n            projects.push(Project {\n                id: dir_name.to_string(),\n                path: project_path,\n                sessions,\n                created_at,\n                most_recent_session,\n            });\n        }\n    }\n\n    // Sort projects by most recent session activity, then by creation time\n    projects.sort_by(|a, b| {\n        // First compare by most recent session\n        match (a.most_recent_session, b.most_recent_session) {\n            (Some(a_time), Some(b_time)) => b_time.cmp(&a_time),\n            (Some(_), None) => std::cmp::Ordering::Less,\n            (None, Some(_)) => std::cmp::Ordering::Greater,\n            (None, None) => b.created_at.cmp(&a.created_at),\n        }\n    });\n\n    log::info!(\"Found {} projects\", projects.len());\n    Ok(projects)\n}\n\n/// Creates a new project for the given directory path\n#[tauri::command]\npub async fn create_project(path: String) -> Result<Project, String> {\n    log::info!(\"Creating project for path: {}\", path);\n\n    // Encode the path to create a project ID\n    let project_id = path.replace('/', \"-\");\n\n    // Get claude directory\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let projects_dir = claude_dir.join(\"projects\");\n\n    // Create projects directory if it doesn't exist\n    if !projects_dir.exists() {\n        fs::create_dir_all(&projects_dir)\n            .map_err(|e| format!(\"Failed to create projects directory: {}\", e))?;\n    }\n\n    // Create project directory if it doesn't exist\n    let project_dir = projects_dir.join(&project_id);\n    if !project_dir.exists() {\n        fs::create_dir_all(&project_dir)\n            .map_err(|e| format!(\"Failed to create project directory: {}\", e))?;\n    }\n\n    // Get creation time\n    let metadata = fs::metadata(&project_dir)\n        .map_err(|e| format!(\"Failed to read directory metadata: {}\", e))?;\n\n    let created_at = metadata\n        .created()\n        .or_else(|_| metadata.modified())\n        .unwrap_or(SystemTime::UNIX_EPOCH)\n        .duration_since(UNIX_EPOCH)\n        .unwrap_or_default()\n        .as_secs();\n\n    // Return the created project\n    Ok(Project {\n        id: project_id,\n        path,\n        sessions: Vec::new(),\n        created_at,\n        most_recent_session: None,\n    })\n}\n\n/// Gets sessions for a specific project\n#[tauri::command]\npub async fn get_project_sessions(project_id: String) -> Result<Vec<Session>, String> {\n    log::info!(\"Getting sessions for project: {}\", project_id);\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let project_dir = claude_dir.join(\"projects\").join(&project_id);\n    let todos_dir = claude_dir.join(\"todos\");\n\n    if !project_dir.exists() {\n        return Err(format!(\"Project directory not found: {}\", project_id));\n    }\n\n    // Get the actual project path from JSONL files\n    let project_path = match get_project_path_from_sessions(&project_dir) {\n        Ok(path) => path,\n        Err(e) => {\n            log::warn!(\n                \"Failed to get project path from sessions for {}: {}, falling back to decode\",\n                project_id,\n                e\n            );\n            decode_project_path(&project_id)\n        }\n    };\n\n    let mut sessions = Vec::new();\n\n    // Read all JSONL files in the project directory\n    let entries = fs::read_dir(&project_dir)\n        .map_err(|e| format!(\"Failed to read project directory: {}\", e))?;\n\n    for entry in entries {\n        let entry = entry.map_err(|e| format!(\"Failed to read directory entry: {}\", e))?;\n        let path = entry.path();\n\n        if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some(\"jsonl\") {\n            if let Some(session_id) = path.file_stem().and_then(|s| s.to_str()) {\n                // Get file creation time\n                let metadata = fs::metadata(&path)\n                    .map_err(|e| format!(\"Failed to read file metadata: {}\", e))?;\n\n                let created_at = metadata\n                    .created()\n                    .or_else(|_| metadata.modified())\n                    .unwrap_or(SystemTime::UNIX_EPOCH)\n                    .duration_since(SystemTime::UNIX_EPOCH)\n                    .unwrap_or_default()\n                    .as_secs();\n\n                // Extract first user message and timestamp\n                let (first_message, message_timestamp) = extract_first_user_message(&path);\n\n                // Try to load associated todo data\n                let todo_path = todos_dir.join(format!(\"{}.json\", session_id));\n                let todo_data = if todo_path.exists() {\n                    fs::read_to_string(&todo_path)\n                        .ok()\n                        .and_then(|content| serde_json::from_str(&content).ok())\n                } else {\n                    None\n                };\n\n                sessions.push(Session {\n                    id: session_id.to_string(),\n                    project_id: project_id.clone(),\n                    project_path: project_path.clone(),\n                    todo_data,\n                    created_at,\n                    first_message,\n                    message_timestamp,\n                });\n            }\n        }\n    }\n\n    // Sort sessions by creation time (newest first)\n    sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at));\n\n    log::info!(\n        \"Found {} sessions for project {}\",\n        sessions.len(),\n        project_id\n    );\n    Ok(sessions)\n}\n\n/// Reads the Claude settings file\n#[tauri::command]\npub async fn get_claude_settings() -> Result<ClaudeSettings, String> {\n    log::info!(\"Reading Claude settings\");\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let settings_path = claude_dir.join(\"settings.json\");\n\n    if !settings_path.exists() {\n        log::warn!(\"Settings file not found, returning empty settings\");\n        return Ok(ClaudeSettings {\n            data: serde_json::json!({}),\n        });\n    }\n\n    let content = fs::read_to_string(&settings_path)\n        .map_err(|e| format!(\"Failed to read settings file: {}\", e))?;\n\n    let data: serde_json::Value = serde_json::from_str(&content)\n        .map_err(|e| format!(\"Failed to parse settings JSON: {}\", e))?;\n\n    Ok(ClaudeSettings { data })\n}\n\n/// Opens a new Claude Code session by executing the claude command\n#[tauri::command]\npub async fn open_new_session(app: AppHandle, path: Option<String>) -> Result<String, String> {\n    log::info!(\"Opening new Claude Code session at path: {:?}\", path);\n\n    #[cfg(not(debug_assertions))]\n    let _claude_path = find_claude_binary(&app)?;\n\n    #[cfg(debug_assertions)]\n    let claude_path = find_claude_binary(&app)?;\n\n    // In production, we can't use std::process::Command directly\n    // The user should launch Claude Code through other means or use the execute_claude_code command\n    #[cfg(not(debug_assertions))]\n    {\n        log::error!(\"Cannot spawn processes directly in production builds\");\n        return Err(\"Direct process spawning is not available in production builds. Please use Claude Code directly or use the integrated execution commands.\".to_string());\n    }\n\n    #[cfg(debug_assertions)]\n    {\n        let mut cmd = std::process::Command::new(claude_path);\n\n        // If a path is provided, use it; otherwise use current directory\n        if let Some(project_path) = path {\n            cmd.current_dir(&project_path);\n        }\n\n        // Execute the command\n        match cmd.spawn() {\n            Ok(_) => {\n                log::info!(\"Successfully launched Claude Code\");\n                Ok(\"Claude Code session started\".to_string())\n            }\n            Err(e) => {\n                log::error!(\"Failed to launch Claude Code: {}\", e);\n                Err(format!(\"Failed to launch Claude Code: {}\", e))\n            }\n        }\n    }\n}\n\n/// Reads the CLAUDE.md system prompt file\n#[tauri::command]\npub async fn get_system_prompt() -> Result<String, String> {\n    log::info!(\"Reading CLAUDE.md system prompt\");\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let claude_md_path = claude_dir.join(\"CLAUDE.md\");\n\n    if !claude_md_path.exists() {\n        log::warn!(\"CLAUDE.md not found\");\n        return Ok(String::new());\n    }\n\n    fs::read_to_string(&claude_md_path).map_err(|e| format!(\"Failed to read CLAUDE.md: {}\", e))\n}\n\n/// Checks if Claude Code is installed and gets its version\n#[tauri::command]\npub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus, String> {\n    log::info!(\"Checking Claude Code version\");\n\n    let claude_path = match find_claude_binary(&app) {\n        Ok(path) => path,\n        Err(e) => {\n            return Ok(ClaudeVersionStatus {\n                is_installed: false,\n                version: None,\n                output: e,\n            });\n        }\n    };\n\n    use log::debug;\n    debug!(\"Claude path: {}\", claude_path);\n\n    // In production builds, we can't check the version directly\n    #[cfg(not(debug_assertions))]\n    {\n        log::warn!(\"Cannot check claude version in production build\");\n        // If we found a path (either stored or in common locations), assume it's installed\n        if claude_path != \"claude\" && PathBuf::from(&claude_path).exists() {\n            return Ok(ClaudeVersionStatus {\n                is_installed: true,\n                version: None,\n                output: \"Claude binary found at: \".to_string() + &claude_path,\n            });\n        } else {\n            return Ok(ClaudeVersionStatus {\n                is_installed: false,\n                version: None,\n                output: \"Cannot verify Claude installation in production build. Please ensure Claude Code is installed.\".to_string(),\n            });\n        }\n    }\n\n    #[cfg(debug_assertions)]\n    {\n        let output = std::process::Command::new(claude_path)\n            .arg(\"--version\")\n            .output();\n\n        match output {\n            Ok(output) => {\n                let stdout = String::from_utf8_lossy(&output.stdout).to_string();\n                let stderr = String::from_utf8_lossy(&output.stderr).to_string();\n\n                // Use regex to directly extract version pattern (e.g., \"1.0.41\")\n                let version_regex =\n                    regex::Regex::new(r\"(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?(?:\\+[a-zA-Z0-9.-]+)?)\")\n                        .ok();\n\n                let version = if let Some(regex) = version_regex {\n                    regex\n                        .captures(&stdout)\n                        .and_then(|captures| captures.get(1))\n                        .map(|m| m.as_str().to_string())\n                } else {\n                    None\n                };\n\n                let full_output = if stderr.is_empty() {\n                    stdout.clone()\n                } else {\n                    format!(\"{}\\n{}\", stdout, stderr)\n                };\n\n                // Check if the output matches the expected format\n                // Expected format: \"1.0.17 (Claude Code)\" or similar\n                let is_valid = stdout.contains(\"(Claude Code)\") || stdout.contains(\"Claude Code\");\n\n                Ok(ClaudeVersionStatus {\n                    is_installed: is_valid && output.status.success(),\n                    version,\n                    output: full_output.trim().to_string(),\n                })\n            }\n            Err(e) => {\n                log::error!(\"Failed to run claude command: {}\", e);\n                Ok(ClaudeVersionStatus {\n                    is_installed: false,\n                    version: None,\n                    output: format!(\"Command not found: {}\", e),\n                })\n            }\n        }\n    }\n}\n\n/// Saves the CLAUDE.md system prompt file\n#[tauri::command]\npub async fn save_system_prompt(content: String) -> Result<String, String> {\n    log::info!(\"Saving CLAUDE.md system prompt\");\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let claude_md_path = claude_dir.join(\"CLAUDE.md\");\n\n    fs::write(&claude_md_path, content).map_err(|e| format!(\"Failed to write CLAUDE.md: {}\", e))?;\n\n    Ok(\"System prompt saved successfully\".to_string())\n}\n\n/// Saves the Claude settings file\n#[tauri::command]\npub async fn save_claude_settings(settings: serde_json::Value) -> Result<String, String> {\n    log::info!(\"Saving Claude settings\");\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let settings_path = claude_dir.join(\"settings.json\");\n\n    // Pretty print the JSON with 2-space indentation\n    let json_string = serde_json::to_string_pretty(&settings)\n        .map_err(|e| format!(\"Failed to serialize settings: {}\", e))?;\n\n    fs::write(&settings_path, json_string)\n        .map_err(|e| format!(\"Failed to write settings file: {}\", e))?;\n\n    Ok(\"Settings saved successfully\".to_string())\n}\n\n/// Recursively finds all CLAUDE.md files in a project directory\n#[tauri::command]\npub async fn find_claude_md_files(project_path: String) -> Result<Vec<ClaudeMdFile>, String> {\n    log::info!(\"Finding CLAUDE.md files in project: {}\", project_path);\n\n    let path = PathBuf::from(&project_path);\n    if !path.exists() {\n        return Err(format!(\"Project path does not exist: {}\", project_path));\n    }\n\n    let mut claude_files = Vec::new();\n    find_claude_md_recursive(&path, &path, &mut claude_files)?;\n\n    // Sort by relative path\n    claude_files.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));\n\n    log::info!(\"Found {} CLAUDE.md files\", claude_files.len());\n    Ok(claude_files)\n}\n\n/// Helper function to recursively find CLAUDE.md files\nfn find_claude_md_recursive(\n    current_path: &PathBuf,\n    project_root: &PathBuf,\n    claude_files: &mut Vec<ClaudeMdFile>,\n) -> Result<(), String> {\n    let entries = fs::read_dir(current_path)\n        .map_err(|e| format!(\"Failed to read directory {:?}: {}\", current_path, e))?;\n\n    for entry in entries {\n        let entry = entry.map_err(|e| format!(\"Failed to read directory entry: {}\", e))?;\n        let path = entry.path();\n\n        // Skip hidden files/directories\n        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {\n            if name.starts_with('.') {\n                continue;\n            }\n        }\n\n        if path.is_dir() {\n            // Skip common directories that shouldn't be searched\n            if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {\n                if matches!(\n                    dir_name,\n                    \"node_modules\" | \"target\" | \".git\" | \"dist\" | \"build\" | \".next\" | \"__pycache__\"\n                ) {\n                    continue;\n                }\n            }\n\n            find_claude_md_recursive(&path, project_root, claude_files)?;\n        } else if path.is_file() {\n            // Check if it's a CLAUDE.md file (case insensitive)\n            if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {\n                if file_name.eq_ignore_ascii_case(\"CLAUDE.md\") {\n                    let metadata = fs::metadata(&path)\n                        .map_err(|e| format!(\"Failed to read file metadata: {}\", e))?;\n\n                    let relative_path = path\n                        .strip_prefix(project_root)\n                        .map_err(|e| format!(\"Failed to get relative path: {}\", e))?\n                        .to_string_lossy()\n                        .to_string();\n\n                    let modified = metadata\n                        .modified()\n                        .unwrap_or(SystemTime::UNIX_EPOCH)\n                        .duration_since(SystemTime::UNIX_EPOCH)\n                        .unwrap_or_default()\n                        .as_secs();\n\n                    claude_files.push(ClaudeMdFile {\n                        relative_path,\n                        absolute_path: path.to_string_lossy().to_string(),\n                        size: metadata.len(),\n                        modified,\n                    });\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Reads a specific CLAUDE.md file by its absolute path\n#[tauri::command]\npub async fn read_claude_md_file(file_path: String) -> Result<String, String> {\n    log::info!(\"Reading CLAUDE.md file: {}\", file_path);\n\n    let path = PathBuf::from(&file_path);\n    if !path.exists() {\n        return Err(format!(\"File does not exist: {}\", file_path));\n    }\n\n    fs::read_to_string(&path).map_err(|e| format!(\"Failed to read file: {}\", e))\n}\n\n/// Saves a specific CLAUDE.md file by its absolute path\n#[tauri::command]\npub async fn save_claude_md_file(file_path: String, content: String) -> Result<String, String> {\n    log::info!(\"Saving CLAUDE.md file: {}\", file_path);\n\n    let path = PathBuf::from(&file_path);\n\n    // Ensure the parent directory exists\n    if let Some(parent) = path.parent() {\n        fs::create_dir_all(parent)\n            .map_err(|e| format!(\"Failed to create parent directory: {}\", e))?;\n    }\n\n    fs::write(&path, content).map_err(|e| format!(\"Failed to write file: {}\", e))?;\n\n    Ok(\"File saved successfully\".to_string())\n}\n\n/// Loads the JSONL history for a specific session\n#[tauri::command]\npub async fn load_session_history(\n    session_id: String,\n    project_id: String,\n) -> Result<Vec<serde_json::Value>, String> {\n    log::info!(\n        \"Loading session history for session: {} in project: {}\",\n        session_id,\n        project_id\n    );\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let session_path = claude_dir\n        .join(\"projects\")\n        .join(&project_id)\n        .join(format!(\"{}.jsonl\", session_id));\n\n    if !session_path.exists() {\n        return Err(format!(\"Session file not found: {}\", session_id));\n    }\n\n    let file =\n        fs::File::open(&session_path).map_err(|e| format!(\"Failed to open session file: {}\", e))?;\n\n    let reader = BufReader::new(file);\n    let mut messages = Vec::new();\n\n    for line in reader.lines() {\n        if let Ok(line) = line {\n            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) {\n                messages.push(json);\n            }\n        }\n    }\n\n    Ok(messages)\n}\n\n/// Execute a new interactive Claude Code session with streaming output\n#[tauri::command]\npub async fn execute_claude_code(\n    app: AppHandle,\n    project_path: String,\n    prompt: String,\n    model: String,\n) -> Result<(), String> {\n    log::info!(\n        \"Starting new Claude Code session in: {} with model: {}\",\n        project_path,\n        model\n    );\n\n    let claude_path = find_claude_binary(&app)?;\n\n    let args = vec![\n        \"-p\".to_string(),\n        prompt.clone(),\n        \"--model\".to_string(),\n        model.clone(),\n        \"--output-format\".to_string(),\n        \"stream-json\".to_string(),\n        \"--verbose\".to_string(),\n        \"--dangerously-skip-permissions\".to_string(),\n    ];\n\n    let cmd = create_system_command(&claude_path, args, &project_path);\n    spawn_claude_process(app, cmd, prompt, model, project_path).await\n}\n\n/// Continue an existing Claude Code conversation with streaming output\n#[tauri::command]\npub async fn continue_claude_code(\n    app: AppHandle,\n    project_path: String,\n    prompt: String,\n    model: String,\n) -> Result<(), String> {\n    log::info!(\n        \"Continuing Claude Code conversation in: {} with model: {}\",\n        project_path,\n        model\n    );\n\n    let claude_path = find_claude_binary(&app)?;\n\n    let args = vec![\n        \"-c\".to_string(), // Continue flag\n        \"-p\".to_string(),\n        prompt.clone(),\n        \"--model\".to_string(),\n        model.clone(),\n        \"--output-format\".to_string(),\n        \"stream-json\".to_string(),\n        \"--verbose\".to_string(),\n        \"--dangerously-skip-permissions\".to_string(),\n    ];\n\n    let cmd = create_system_command(&claude_path, args, &project_path);\n    spawn_claude_process(app, cmd, prompt, model, project_path).await\n}\n\n/// Resume an existing Claude Code session by ID with streaming output\n#[tauri::command]\npub async fn resume_claude_code(\n    app: AppHandle,\n    project_path: String,\n    session_id: String,\n    prompt: String,\n    model: String,\n) -> Result<(), String> {\n    log::info!(\n        \"Resuming Claude Code session: {} in: {} with model: {}\",\n        session_id,\n        project_path,\n        model\n    );\n\n    let claude_path = find_claude_binary(&app)?;\n\n    let args = vec![\n        \"--resume\".to_string(),\n        session_id.clone(),\n        \"-p\".to_string(),\n        prompt.clone(),\n        \"--model\".to_string(),\n        model.clone(),\n        \"--output-format\".to_string(),\n        \"stream-json\".to_string(),\n        \"--verbose\".to_string(),\n        \"--dangerously-skip-permissions\".to_string(),\n    ];\n\n    let cmd = create_system_command(&claude_path, args, &project_path);\n    spawn_claude_process(app, cmd, prompt, model, project_path).await\n}\n\n/// Cancel the currently running Claude Code execution\n#[tauri::command]\npub async fn cancel_claude_execution(\n    app: AppHandle,\n    session_id: Option<String>,\n) -> Result<(), String> {\n    log::info!(\n        \"Cancelling Claude Code execution for session: {:?}\",\n        session_id\n    );\n\n    let mut killed = false;\n    let mut attempted_methods = Vec::new();\n\n    // Method 1: Try to find and kill via ProcessRegistry using session ID\n    if let Some(sid) = &session_id {\n        let registry = app.state::<crate::process::ProcessRegistryState>();\n        match registry.0.get_claude_session_by_id(sid) {\n            Ok(Some(process_info)) => {\n                log::info!(\n                    \"Found process in registry for session {}: run_id={}, PID={}\",\n                    sid,\n                    process_info.run_id,\n                    process_info.pid\n                );\n                match registry.0.kill_process(process_info.run_id).await {\n                    Ok(success) => {\n                        if success {\n                            log::info!(\"Successfully killed process via registry\");\n                            killed = true;\n                        } else {\n                            log::warn!(\"Registry kill returned false\");\n                        }\n                    }\n                    Err(e) => {\n                        log::warn!(\"Failed to kill via registry: {}\", e);\n                    }\n                }\n                attempted_methods.push(\"registry\");\n            }\n            Ok(None) => {\n                log::warn!(\"Session {} not found in ProcessRegistry\", sid);\n            }\n            Err(e) => {\n                log::error!(\"Error querying ProcessRegistry: {}\", e);\n            }\n        }\n    }\n\n    // Method 2: Try the legacy approach via ClaudeProcessState\n    if !killed {\n        let claude_state = app.state::<ClaudeProcessState>();\n        let mut current_process = claude_state.current_process.lock().await;\n\n        if let Some(mut child) = current_process.take() {\n            // Try to get the PID before killing\n            let pid = child.id();\n            log::info!(\n                \"Attempting to kill Claude process via ClaudeProcessState with PID: {:?}\",\n                pid\n            );\n\n            // Kill the process\n            match child.kill().await {\n                Ok(_) => {\n                    log::info!(\"Successfully killed Claude process via ClaudeProcessState\");\n                    killed = true;\n                }\n                Err(e) => {\n                    log::error!(\n                        \"Failed to kill Claude process via ClaudeProcessState: {}\",\n                        e\n                    );\n\n                    // Method 3: If we have a PID, try system kill as last resort\n                    if let Some(pid) = pid {\n                        log::info!(\"Attempting system kill as last resort for PID: {}\", pid);\n                        let kill_result = if cfg!(target_os = \"windows\") {\n                            std::process::Command::new(\"taskkill\")\n                                .args([\"/F\", \"/PID\", &pid.to_string()])\n                                .output()\n                        } else {\n                            std::process::Command::new(\"kill\")\n                                .args([\"-KILL\", &pid.to_string()])\n                                .output()\n                        };\n\n                        match kill_result {\n                            Ok(output) if output.status.success() => {\n                                log::info!(\"Successfully killed process via system command\");\n                                killed = true;\n                            }\n                            Ok(output) => {\n                                let stderr = String::from_utf8_lossy(&output.stderr);\n                                log::error!(\"System kill failed: {}\", stderr);\n                            }\n                            Err(e) => {\n                                log::error!(\"Failed to execute system kill command: {}\", e);\n                            }\n                        }\n                    }\n                }\n            }\n            attempted_methods.push(\"claude_state\");\n        } else {\n            log::warn!(\"No active Claude process in ClaudeProcessState\");\n        }\n    }\n\n    if !killed && attempted_methods.is_empty() {\n        log::warn!(\"No active Claude process found to cancel\");\n    }\n\n    // Always emit cancellation events for UI consistency\n    if let Some(sid) = session_id {\n        let _ = app.emit(&format!(\"claude-cancelled:{}\", sid), true);\n        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n        let _ = app.emit(&format!(\"claude-complete:{}\", sid), false);\n    }\n\n    // Also emit generic events for backward compatibility\n    let _ = app.emit(\"claude-cancelled\", true);\n    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n    let _ = app.emit(\"claude-complete\", false);\n\n    if killed {\n        log::info!(\"Claude process cancellation completed successfully\");\n    } else if !attempted_methods.is_empty() {\n        log::warn!(\"Claude process cancellation attempted but process may have already exited. Attempted methods: {:?}\", attempted_methods);\n    }\n\n    Ok(())\n}\n\n/// Get all running Claude sessions\n#[tauri::command]\npub async fn list_running_claude_sessions(\n    registry: tauri::State<'_, crate::process::ProcessRegistryState>,\n) -> Result<Vec<crate::process::ProcessInfo>, String> {\n    registry.0.get_running_claude_sessions()\n}\n\n/// Get live output from a Claude session\n#[tauri::command]\npub async fn get_claude_session_output(\n    registry: tauri::State<'_, crate::process::ProcessRegistryState>,\n    session_id: String,\n) -> Result<String, String> {\n    // Find the process by session ID\n    if let Some(process_info) = registry.0.get_claude_session_by_id(&session_id)? {\n        registry.0.get_live_output(process_info.run_id)\n    } else {\n        Ok(String::new())\n    }\n}\n\n/// Helper function to spawn Claude process and handle streaming\nasync fn spawn_claude_process(\n    app: AppHandle,\n    mut cmd: Command,\n    prompt: String,\n    model: String,\n    project_path: String,\n) -> Result<(), String> {\n    use std::sync::Mutex;\n    use tokio::io::{AsyncBufReadExt, BufReader};\n\n    // Spawn the process\n    let mut child = cmd\n        .spawn()\n        .map_err(|e| format!(\"Failed to spawn Claude: {}\", e))?;\n\n    // Get stdout and stderr\n    let stdout = child.stdout.take().ok_or(\"Failed to get stdout\")?;\n    let stderr = child.stderr.take().ok_or(\"Failed to get stderr\")?;\n\n    // Get the child PID for logging\n    let pid = child.id().unwrap_or(0);\n    log::info!(\"Spawned Claude process with PID: {:?}\", pid);\n\n    // Create readers first (before moving child)\n    let stdout_reader = BufReader::new(stdout);\n    let stderr_reader = BufReader::new(stderr);\n\n    // We'll extract the session ID from Claude's init message\n    let session_id_holder: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));\n    let run_id_holder: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));\n\n    // Store the child process in the global state (for backward compatibility)\n    let claude_state = app.state::<ClaudeProcessState>();\n    {\n        let mut current_process = claude_state.current_process.lock().await;\n        // If there's already a process running, kill it first\n        if let Some(mut existing_child) = current_process.take() {\n            log::warn!(\"Killing existing Claude process before starting new one\");\n            let _ = existing_child.kill().await;\n        }\n        *current_process = Some(child);\n    }\n\n    // Spawn tasks to read stdout and stderr\n    let app_handle = app.clone();\n    let session_id_holder_clone = session_id_holder.clone();\n    let run_id_holder_clone = run_id_holder.clone();\n    let registry = app.state::<crate::process::ProcessRegistryState>();\n    let registry_clone = registry.0.clone();\n    let project_path_clone = project_path.clone();\n    let prompt_clone = prompt.clone();\n    let model_clone = model.clone();\n    let stdout_task = tokio::spawn(async move {\n        let mut lines = stdout_reader.lines();\n        while let Ok(Some(line)) = lines.next_line().await {\n            log::debug!(\"Claude stdout: {}\", line);\n\n            // Parse the line to check for init message with session ID\n            if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&line) {\n                if msg[\"type\"] == \"system\" && msg[\"subtype\"] == \"init\" {\n                    if let Some(claude_session_id) = msg[\"session_id\"].as_str() {\n                        let mut session_id_guard = session_id_holder_clone.lock().unwrap();\n                        if session_id_guard.is_none() {\n                            *session_id_guard = Some(claude_session_id.to_string());\n                            log::info!(\"Extracted Claude session ID: {}\", claude_session_id);\n\n                            // Now register with ProcessRegistry using Claude's session ID\n                            match registry_clone.register_claude_session(\n                                claude_session_id.to_string(),\n                                pid,\n                                project_path_clone.clone(),\n                                prompt_clone.clone(),\n                                model_clone.clone(),\n                            ) {\n                                Ok(run_id) => {\n                                    log::info!(\"Registered Claude session with run_id: {}\", run_id);\n                                    let mut run_id_guard = run_id_holder_clone.lock().unwrap();\n                                    *run_id_guard = Some(run_id);\n                                }\n                                Err(e) => {\n                                    log::error!(\"Failed to register Claude session: {}\", e);\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            // Store live output in registry if we have a run_id\n            if let Some(run_id) = *run_id_holder_clone.lock().unwrap() {\n                let _ = registry_clone.append_live_output(run_id, &line);\n            }\n\n            // Emit the line to the frontend with session isolation if we have session ID\n            if let Some(ref session_id) = *session_id_holder_clone.lock().unwrap() {\n                let _ = app_handle.emit(&format!(\"claude-output:{}\", session_id), &line);\n            }\n            // Also emit to the generic event for backward compatibility\n            let _ = app_handle.emit(\"claude-output\", &line);\n        }\n    });\n\n    let app_handle_stderr = app.clone();\n    let session_id_holder_clone2 = session_id_holder.clone();\n    let stderr_task = tokio::spawn(async move {\n        let mut lines = stderr_reader.lines();\n        while let Ok(Some(line)) = lines.next_line().await {\n            log::error!(\"Claude stderr: {}\", line);\n            // Emit error lines to the frontend with session isolation if we have session ID\n            if let Some(ref session_id) = *session_id_holder_clone2.lock().unwrap() {\n                let _ = app_handle_stderr.emit(&format!(\"claude-error:{}\", session_id), &line);\n            }\n            // Also emit to the generic event for backward compatibility\n            let _ = app_handle_stderr.emit(\"claude-error\", &line);\n        }\n    });\n\n    // Wait for the process to complete\n    let app_handle_wait = app.clone();\n    let claude_state_wait = claude_state.current_process.clone();\n    let session_id_holder_clone3 = session_id_holder.clone();\n    let run_id_holder_clone2 = run_id_holder.clone();\n    let registry_clone2 = registry.0.clone();\n    tokio::spawn(async move {\n        let _ = stdout_task.await;\n        let _ = stderr_task.await;\n\n        // Get the child from the state to wait on it\n        let mut current_process = claude_state_wait.lock().await;\n        if let Some(mut child) = current_process.take() {\n            match child.wait().await {\n                Ok(status) => {\n                    log::info!(\"Claude process exited with status: {}\", status);\n                    // Add a small delay to ensure all messages are processed\n                    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n                    if let Some(ref session_id) = *session_id_holder_clone3.lock().unwrap() {\n                        let _ = app_handle_wait\n                            .emit(&format!(\"claude-complete:{}\", session_id), status.success());\n                    }\n                    // Also emit to the generic event for backward compatibility\n                    let _ = app_handle_wait.emit(\"claude-complete\", status.success());\n                }\n                Err(e) => {\n                    log::error!(\"Failed to wait for Claude process: {}\", e);\n                    // Add a small delay to ensure all messages are processed\n                    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n                    if let Some(ref session_id) = *session_id_holder_clone3.lock().unwrap() {\n                        let _ =\n                            app_handle_wait.emit(&format!(\"claude-complete:{}\", session_id), false);\n                    }\n                    // Also emit to the generic event for backward compatibility\n                    let _ = app_handle_wait.emit(\"claude-complete\", false);\n                }\n            }\n        }\n\n        // Unregister from ProcessRegistry if we have a run_id\n        if let Some(run_id) = *run_id_holder_clone2.lock().unwrap() {\n            let _ = registry_clone2.unregister_process(run_id);\n        }\n\n        // Clear the process from state\n        *current_process = None;\n    });\n\n    Ok(())\n}\n\n/// Lists files and directories in a given path\n#[tauri::command]\npub async fn list_directory_contents(directory_path: String) -> Result<Vec<FileEntry>, String> {\n    log::info!(\"Listing directory contents: '{}'\", directory_path);\n\n    // Check if path is empty\n    if directory_path.trim().is_empty() {\n        log::error!(\"Directory path is empty or whitespace\");\n        return Err(\"Directory path cannot be empty\".to_string());\n    }\n\n    let path = PathBuf::from(&directory_path);\n    log::debug!(\"Resolved path: {:?}\", path);\n\n    if !path.exists() {\n        log::error!(\"Path does not exist: {:?}\", path);\n        return Err(format!(\"Path does not exist: {}\", directory_path));\n    }\n\n    if !path.is_dir() {\n        log::error!(\"Path is not a directory: {:?}\", path);\n        return Err(format!(\"Path is not a directory: {}\", directory_path));\n    }\n\n    let mut entries = Vec::new();\n\n    let dir_entries =\n        fs::read_dir(&path).map_err(|e| format!(\"Failed to read directory: {}\", e))?;\n\n    for entry in dir_entries {\n        let entry = entry.map_err(|e| format!(\"Failed to read entry: {}\", e))?;\n        let entry_path = entry.path();\n        let metadata = entry\n            .metadata()\n            .map_err(|e| format!(\"Failed to read metadata: {}\", e))?;\n\n        // Skip hidden files/directories unless they are .claude directories\n        if let Some(name) = entry_path.file_name().and_then(|n| n.to_str()) {\n            if name.starts_with('.') && name != \".claude\" {\n                continue;\n            }\n        }\n\n        let name = entry_path\n            .file_name()\n            .and_then(|n| n.to_str())\n            .unwrap_or(\"\")\n            .to_string();\n\n        let extension = if metadata.is_file() {\n            entry_path\n                .extension()\n                .and_then(|e| e.to_str())\n                .map(|e| e.to_string())\n        } else {\n            None\n        };\n\n        entries.push(FileEntry {\n            name,\n            path: entry_path.to_string_lossy().to_string(),\n            is_directory: metadata.is_dir(),\n            size: metadata.len(),\n            extension,\n        });\n    }\n\n    // Sort: directories first, then files, alphabetically within each group\n    entries.sort_by(|a, b| match (a.is_directory, b.is_directory) {\n        (true, false) => std::cmp::Ordering::Less,\n        (false, true) => std::cmp::Ordering::Greater,\n        _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),\n    });\n\n    Ok(entries)\n}\n\n/// Search for files and directories matching a pattern\n#[tauri::command]\npub async fn search_files(base_path: String, query: String) -> Result<Vec<FileEntry>, String> {\n    log::info!(\"Searching files in '{}' for: '{}'\", base_path, query);\n\n    // Check if path is empty\n    if base_path.trim().is_empty() {\n        log::error!(\"Base path is empty or whitespace\");\n        return Err(\"Base path cannot be empty\".to_string());\n    }\n\n    // Check if query is empty\n    if query.trim().is_empty() {\n        log::warn!(\"Search query is empty, returning empty results\");\n        return Ok(Vec::new());\n    }\n\n    let path = PathBuf::from(&base_path);\n    log::debug!(\"Resolved search base path: {:?}\", path);\n\n    if !path.exists() {\n        log::error!(\"Base path does not exist: {:?}\", path);\n        return Err(format!(\"Path does not exist: {}\", base_path));\n    }\n\n    let query_lower = query.to_lowercase();\n    let mut results = Vec::new();\n\n    search_files_recursive(&path, &path, &query_lower, &mut results, 0)?;\n\n    // Sort by relevance: exact matches first, then by name\n    results.sort_by(|a, b| {\n        let a_exact = a.name.to_lowercase() == query_lower;\n        let b_exact = b.name.to_lowercase() == query_lower;\n\n        match (a_exact, b_exact) {\n            (true, false) => std::cmp::Ordering::Less,\n            (false, true) => std::cmp::Ordering::Greater,\n            _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),\n        }\n    });\n\n    // Limit results to prevent overwhelming the UI\n    results.truncate(50);\n\n    Ok(results)\n}\n\nfn search_files_recursive(\n    current_path: &PathBuf,\n    base_path: &PathBuf,\n    query: &str,\n    results: &mut Vec<FileEntry>,\n    depth: usize,\n) -> Result<(), String> {\n    // Limit recursion depth to prevent excessive searching\n    if depth > 5 || results.len() >= 50 {\n        return Ok(());\n    }\n\n    let entries = fs::read_dir(current_path)\n        .map_err(|e| format!(\"Failed to read directory {:?}: {}\", current_path, e))?;\n\n    for entry in entries {\n        let entry = entry.map_err(|e| format!(\"Failed to read entry: {}\", e))?;\n        let entry_path = entry.path();\n\n        // Skip hidden files/directories\n        if let Some(name) = entry_path.file_name().and_then(|n| n.to_str()) {\n            if name.starts_with('.') {\n                continue;\n            }\n\n            // Check if name matches query\n            if name.to_lowercase().contains(query) {\n                let metadata = entry\n                    .metadata()\n                    .map_err(|e| format!(\"Failed to read metadata: {}\", e))?;\n\n                let extension = if metadata.is_file() {\n                    entry_path\n                        .extension()\n                        .and_then(|e| e.to_str())\n                        .map(|e| e.to_string())\n                } else {\n                    None\n                };\n\n                results.push(FileEntry {\n                    name: name.to_string(),\n                    path: entry_path.to_string_lossy().to_string(),\n                    is_directory: metadata.is_dir(),\n                    size: metadata.len(),\n                    extension,\n                });\n            }\n        }\n\n        // Recurse into directories\n        if entry_path.is_dir() {\n            // Skip common directories that shouldn't be searched\n            if let Some(dir_name) = entry_path.file_name().and_then(|n| n.to_str()) {\n                if matches!(\n                    dir_name,\n                    \"node_modules\" | \"target\" | \".git\" | \"dist\" | \"build\" | \".next\" | \"__pycache__\"\n                ) {\n                    continue;\n                }\n            }\n\n            search_files_recursive(&entry_path, base_path, query, results, depth + 1)?;\n        }\n    }\n\n    Ok(())\n}\n\n/// Creates a checkpoint for the current session state\n#[tauri::command]\npub async fn create_checkpoint(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    message_index: Option<usize>,\n    description: Option<String>,\n) -> Result<crate::checkpoint::CheckpointResult, String> {\n    log::info!(\n        \"Creating checkpoint for session: {} in project: {}\",\n        session_id,\n        project_id\n    );\n\n    let manager = app\n        .get_or_create_manager(\n            session_id.clone(),\n            project_id.clone(),\n            PathBuf::from(&project_path),\n        )\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    // Always load current session messages from the JSONL file\n    let session_path = get_claude_dir()\n        .map_err(|e| e.to_string())?\n        .join(\"projects\")\n        .join(&project_id)\n        .join(format!(\"{}.jsonl\", session_id));\n\n    if session_path.exists() {\n        let file = fs::File::open(&session_path)\n            .map_err(|e| format!(\"Failed to open session file: {}\", e))?;\n        let reader = BufReader::new(file);\n\n        let mut line_count = 0;\n        for line in reader.lines() {\n            if let Some(index) = message_index {\n                if line_count > index {\n                    break;\n                }\n            }\n            if let Ok(line) = line {\n                manager\n                    .track_message(line)\n                    .await\n                    .map_err(|e| format!(\"Failed to track message: {}\", e))?;\n            }\n            line_count += 1;\n        }\n    }\n\n    manager\n        .create_checkpoint(description, None)\n        .await\n        .map_err(|e| format!(\"Failed to create checkpoint: {}\", e))\n}\n\n/// Restores a session to a specific checkpoint\n#[tauri::command]\npub async fn restore_checkpoint(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    checkpoint_id: String,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n) -> Result<crate::checkpoint::CheckpointResult, String> {\n    log::info!(\n        \"Restoring checkpoint: {} for session: {}\",\n        checkpoint_id,\n        session_id\n    );\n\n    let manager = app\n        .get_or_create_manager(\n            session_id.clone(),\n            project_id.clone(),\n            PathBuf::from(&project_path),\n        )\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    let result = manager\n        .restore_checkpoint(&checkpoint_id)\n        .await\n        .map_err(|e| format!(\"Failed to restore checkpoint: {}\", e))?;\n\n    // Update the session JSONL file with restored messages\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let session_path = claude_dir\n        .join(\"projects\")\n        .join(&result.checkpoint.project_id)\n        .join(format!(\"{}.jsonl\", session_id));\n\n    // The manager has already restored the messages internally,\n    // but we need to update the actual session file\n    let (_, _, messages) = manager\n        .storage\n        .load_checkpoint(&result.checkpoint.project_id, &session_id, &checkpoint_id)\n        .map_err(|e| format!(\"Failed to load checkpoint data: {}\", e))?;\n\n    fs::write(&session_path, messages)\n        .map_err(|e| format!(\"Failed to update session file: {}\", e))?;\n\n    Ok(result)\n}\n\n/// Lists all checkpoints for a session\n#[tauri::command]\npub async fn list_checkpoints(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n) -> Result<Vec<crate::checkpoint::Checkpoint>, String> {\n    log::info!(\n        \"Listing checkpoints for session: {} in project: {}\",\n        session_id,\n        project_id\n    );\n\n    let manager = app\n        .get_or_create_manager(session_id, project_id, PathBuf::from(&project_path))\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    Ok(manager.list_checkpoints().await)\n}\n\n/// Forks a new timeline branch from a checkpoint\n#[tauri::command]\npub async fn fork_from_checkpoint(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    checkpoint_id: String,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    new_session_id: String,\n    description: Option<String>,\n) -> Result<crate::checkpoint::CheckpointResult, String> {\n    log::info!(\n        \"Forking from checkpoint: {} to new session: {}\",\n        checkpoint_id,\n        new_session_id\n    );\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n\n    // First, copy the session file to the new session\n    let source_session_path = claude_dir\n        .join(\"projects\")\n        .join(&project_id)\n        .join(format!(\"{}.jsonl\", session_id));\n    let new_session_path = claude_dir\n        .join(\"projects\")\n        .join(&project_id)\n        .join(format!(\"{}.jsonl\", new_session_id));\n\n    if source_session_path.exists() {\n        fs::copy(&source_session_path, &new_session_path)\n            .map_err(|e| format!(\"Failed to copy session file: {}\", e))?;\n    }\n\n    // Create manager for the new session\n    let manager = app\n        .get_or_create_manager(\n            new_session_id.clone(),\n            project_id,\n            PathBuf::from(&project_path),\n        )\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    manager\n        .fork_from_checkpoint(&checkpoint_id, description)\n        .await\n        .map_err(|e| format!(\"Failed to fork checkpoint: {}\", e))\n}\n\n/// Gets the timeline for a session\n#[tauri::command]\npub async fn get_session_timeline(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n) -> Result<crate::checkpoint::SessionTimeline, String> {\n    log::info!(\n        \"Getting timeline for session: {} in project: {}\",\n        session_id,\n        project_id\n    );\n\n    let manager = app\n        .get_or_create_manager(session_id, project_id, PathBuf::from(&project_path))\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    Ok(manager.get_timeline().await)\n}\n\n/// Updates checkpoint settings for a session\n#[tauri::command]\npub async fn update_checkpoint_settings(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    auto_checkpoint_enabled: bool,\n    checkpoint_strategy: String,\n) -> Result<(), String> {\n    use crate::checkpoint::CheckpointStrategy;\n\n    log::info!(\"Updating checkpoint settings for session: {}\", session_id);\n\n    let strategy = match checkpoint_strategy.as_str() {\n        \"manual\" => CheckpointStrategy::Manual,\n        \"per_prompt\" => CheckpointStrategy::PerPrompt,\n        \"per_tool_use\" => CheckpointStrategy::PerToolUse,\n        \"smart\" => CheckpointStrategy::Smart,\n        _ => {\n            return Err(format!(\n                \"Invalid checkpoint strategy: {}\",\n                checkpoint_strategy\n            ))\n        }\n    };\n\n    let manager = app\n        .get_or_create_manager(session_id, project_id, PathBuf::from(&project_path))\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    manager\n        .update_settings(auto_checkpoint_enabled, strategy)\n        .await\n        .map_err(|e| format!(\"Failed to update settings: {}\", e))\n}\n\n/// Gets diff between two checkpoints\n#[tauri::command]\npub async fn get_checkpoint_diff(\n    from_checkpoint_id: String,\n    to_checkpoint_id: String,\n    session_id: String,\n    project_id: String,\n) -> Result<crate::checkpoint::CheckpointDiff, String> {\n    use crate::checkpoint::storage::CheckpointStorage;\n\n    log::info!(\n        \"Getting diff between checkpoints: {} -> {}\",\n        from_checkpoint_id,\n        to_checkpoint_id\n    );\n\n    let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;\n    let storage = CheckpointStorage::new(claude_dir);\n\n    // Load both checkpoints\n    let (from_checkpoint, from_files, _) = storage\n        .load_checkpoint(&project_id, &session_id, &from_checkpoint_id)\n        .map_err(|e| format!(\"Failed to load source checkpoint: {}\", e))?;\n    let (to_checkpoint, to_files, _) = storage\n        .load_checkpoint(&project_id, &session_id, &to_checkpoint_id)\n        .map_err(|e| format!(\"Failed to load target checkpoint: {}\", e))?;\n\n    // Build file maps\n    let mut from_map: std::collections::HashMap<PathBuf, &crate::checkpoint::FileSnapshot> =\n        std::collections::HashMap::new();\n    for file in &from_files {\n        from_map.insert(file.file_path.clone(), file);\n    }\n\n    let mut to_map: std::collections::HashMap<PathBuf, &crate::checkpoint::FileSnapshot> =\n        std::collections::HashMap::new();\n    for file in &to_files {\n        to_map.insert(file.file_path.clone(), file);\n    }\n\n    // Calculate differences\n    let mut modified_files = Vec::new();\n    let mut added_files = Vec::new();\n    let mut deleted_files = Vec::new();\n\n    // Check for modified and deleted files\n    for (path, from_file) in &from_map {\n        if let Some(to_file) = to_map.get(path) {\n            if from_file.hash != to_file.hash {\n                // File was modified\n                let additions = to_file.content.lines().count();\n                let deletions = from_file.content.lines().count();\n\n                modified_files.push(crate::checkpoint::FileDiff {\n                    path: path.clone(),\n                    additions,\n                    deletions,\n                    diff_content: None, // TODO: Generate actual diff\n                });\n            }\n        } else {\n            // File was deleted\n            deleted_files.push(path.clone());\n        }\n    }\n\n    // Check for added files\n    for (path, _) in &to_map {\n        if !from_map.contains_key(path) {\n            added_files.push(path.clone());\n        }\n    }\n\n    // Calculate token delta\n    let token_delta = (to_checkpoint.metadata.total_tokens as i64)\n        - (from_checkpoint.metadata.total_tokens as i64);\n\n    Ok(crate::checkpoint::CheckpointDiff {\n        from_checkpoint_id,\n        to_checkpoint_id,\n        modified_files,\n        added_files,\n        deleted_files,\n        token_delta,\n    })\n}\n\n/// Tracks a message for checkpointing\n#[tauri::command]\npub async fn track_checkpoint_message(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    message: String,\n) -> Result<(), String> {\n    log::info!(\"Tracking message for session: {}\", session_id);\n\n    let manager = app\n        .get_or_create_manager(session_id, project_id, PathBuf::from(project_path))\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    manager\n        .track_message(message)\n        .await\n        .map_err(|e| format!(\"Failed to track message: {}\", e))\n}\n\n/// Checks if auto-checkpoint should be triggered\n#[tauri::command]\npub async fn check_auto_checkpoint(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    message: String,\n) -> Result<bool, String> {\n    log::info!(\"Checking auto-checkpoint for session: {}\", session_id);\n\n    let manager = app\n        .get_or_create_manager(session_id.clone(), project_id, PathBuf::from(project_path))\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    Ok(manager.should_auto_checkpoint(&message).await)\n}\n\n/// Triggers cleanup of old checkpoints\n#[tauri::command]\npub async fn cleanup_old_checkpoints(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    keep_count: usize,\n) -> Result<usize, String> {\n    log::info!(\n        \"Cleaning up old checkpoints for session: {}, keeping {}\",\n        session_id,\n        keep_count\n    );\n\n    let manager = app\n        .get_or_create_manager(\n            session_id.clone(),\n            project_id.clone(),\n            PathBuf::from(project_path),\n        )\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    manager\n        .storage\n        .cleanup_old_checkpoints(&project_id, &session_id, keep_count)\n        .map_err(|e| format!(\"Failed to cleanup checkpoints: {}\", e))\n}\n\n/// Gets checkpoint settings for a session\n#[tauri::command]\npub async fn get_checkpoint_settings(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n) -> Result<serde_json::Value, String> {\n    log::info!(\"Getting checkpoint settings for session: {}\", session_id);\n\n    let manager = app\n        .get_or_create_manager(session_id, project_id, PathBuf::from(project_path))\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    let timeline = manager.get_timeline().await;\n\n    Ok(serde_json::json!({\n        \"auto_checkpoint_enabled\": timeline.auto_checkpoint_enabled,\n        \"checkpoint_strategy\": timeline.checkpoint_strategy,\n        \"total_checkpoints\": timeline.total_checkpoints,\n        \"current_checkpoint_id\": timeline.current_checkpoint_id,\n    }))\n}\n\n/// Clears checkpoint manager for a session (cleanup on session end)\n#[tauri::command]\npub async fn clear_checkpoint_manager(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n) -> Result<(), String> {\n    log::info!(\"Clearing checkpoint manager for session: {}\", session_id);\n\n    app.remove_manager(&session_id).await;\n    Ok(())\n}\n\n/// Gets checkpoint state statistics (for debugging/monitoring)\n#[tauri::command]\npub async fn get_checkpoint_state_stats(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n) -> Result<serde_json::Value, String> {\n    let active_count = app.active_count().await;\n    let active_sessions = app.list_active_sessions().await;\n\n    Ok(serde_json::json!({\n        \"active_managers\": active_count,\n        \"active_sessions\": active_sessions,\n    }))\n}\n\n/// Gets files modified in the last N minutes for a session\n#[tauri::command]\npub async fn get_recently_modified_files(\n    app: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    minutes: i64,\n) -> Result<Vec<String>, String> {\n    use chrono::{Duration, Utc};\n\n    log::info!(\n        \"Getting files modified in the last {} minutes for session: {}\",\n        minutes,\n        session_id\n    );\n\n    let manager = app\n        .get_or_create_manager(session_id, project_id, PathBuf::from(project_path))\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    let since = Utc::now() - Duration::minutes(minutes);\n    let modified_files = manager.get_files_modified_since(since).await;\n\n    // Also log the last modification time\n    if let Some(last_mod) = manager.get_last_modification_time().await {\n        log::info!(\"Last file modification was at: {}\", last_mod);\n    }\n\n    Ok(modified_files\n        .into_iter()\n        .map(|p| p.to_string_lossy().to_string())\n        .collect())\n}\n\n/// Track session messages from the frontend for checkpointing\n#[tauri::command]\npub async fn track_session_messages(\n    state: tauri::State<'_, crate::checkpoint::state::CheckpointState>,\n    session_id: String,\n    project_id: String,\n    project_path: String,\n    messages: Vec<String>,\n) -> Result<(), String> {\n    log::info!(\n        \"Tracking {} messages for session {}\",\n        messages.len(),\n        session_id\n    );\n\n    let manager = state\n        .get_or_create_manager(\n            session_id.clone(),\n            project_id.clone(),\n            PathBuf::from(&project_path),\n        )\n        .await\n        .map_err(|e| format!(\"Failed to get checkpoint manager: {}\", e))?;\n\n    for message in messages {\n        manager\n            .track_message(message)\n            .await\n            .map_err(|e| format!(\"Failed to track message: {}\", e))?;\n    }\n\n    Ok(())\n}\n\n/// Gets hooks configuration from settings at specified scope\n#[tauri::command]\npub async fn get_hooks_config(\n    scope: String,\n    project_path: Option<String>,\n) -> Result<serde_json::Value, String> {\n    log::info!(\n        \"Getting hooks config for scope: {}, project: {:?}\",\n        scope,\n        project_path\n    );\n\n    let settings_path = match scope.as_str() {\n        \"user\" => get_claude_dir()\n            .map_err(|e| e.to_string())?\n            .join(\"settings.json\"),\n        \"project\" => {\n            let path = project_path.ok_or(\"Project path required for project scope\")?;\n            PathBuf::from(path).join(\".claude\").join(\"settings.json\")\n        }\n        \"local\" => {\n            let path = project_path.ok_or(\"Project path required for local scope\")?;\n            PathBuf::from(path)\n                .join(\".claude\")\n                .join(\"settings.local.json\")\n        }\n        _ => return Err(\"Invalid scope\".to_string()),\n    };\n\n    if !settings_path.exists() {\n        log::info!(\n            \"Settings file does not exist at {:?}, returning empty hooks\",\n            settings_path\n        );\n        return Ok(serde_json::json!({}));\n    }\n\n    let content = fs::read_to_string(&settings_path)\n        .map_err(|e| format!(\"Failed to read settings: {}\", e))?;\n\n    let settings: serde_json::Value =\n        serde_json::from_str(&content).map_err(|e| format!(\"Failed to parse settings: {}\", e))?;\n\n    Ok(settings\n        .get(\"hooks\")\n        .cloned()\n        .unwrap_or(serde_json::json!({})))\n}\n\n/// Updates hooks configuration in settings at specified scope\n#[tauri::command]\npub async fn update_hooks_config(\n    scope: String,\n    hooks: serde_json::Value,\n    project_path: Option<String>,\n) -> Result<String, String> {\n    log::info!(\n        \"Updating hooks config for scope: {}, project: {:?}\",\n        scope,\n        project_path\n    );\n\n    let settings_path = match scope.as_str() {\n        \"user\" => get_claude_dir()\n            .map_err(|e| e.to_string())?\n            .join(\"settings.json\"),\n        \"project\" => {\n            let path = project_path.ok_or(\"Project path required for project scope\")?;\n            let claude_dir = PathBuf::from(path).join(\".claude\");\n            fs::create_dir_all(&claude_dir)\n                .map_err(|e| format!(\"Failed to create .claude directory: {}\", e))?;\n            claude_dir.join(\"settings.json\")\n        }\n        \"local\" => {\n            let path = project_path.ok_or(\"Project path required for local scope\")?;\n            let claude_dir = PathBuf::from(path).join(\".claude\");\n            fs::create_dir_all(&claude_dir)\n                .map_err(|e| format!(\"Failed to create .claude directory: {}\", e))?;\n            claude_dir.join(\"settings.local.json\")\n        }\n        _ => return Err(\"Invalid scope\".to_string()),\n    };\n\n    // Read existing settings or create new\n    let mut settings = if settings_path.exists() {\n        let content = fs::read_to_string(&settings_path)\n            .map_err(|e| format!(\"Failed to read settings: {}\", e))?;\n        serde_json::from_str(&content).map_err(|e| format!(\"Failed to parse settings: {}\", e))?\n    } else {\n        serde_json::json!({})\n    };\n\n    // Update hooks section\n    settings[\"hooks\"] = hooks;\n\n    // Write back with pretty formatting\n    let json_string = serde_json::to_string_pretty(&settings)\n        .map_err(|e| format!(\"Failed to serialize settings: {}\", e))?;\n\n    fs::write(&settings_path, json_string)\n        .map_err(|e| format!(\"Failed to write settings: {}\", e))?;\n\n    Ok(\"Hooks configuration updated successfully\".to_string())\n}\n\n/// Validates a hook command by dry-running it\n#[tauri::command]\npub async fn validate_hook_command(command: String) -> Result<serde_json::Value, String> {\n    log::info!(\"Validating hook command syntax\");\n\n    // Validate syntax without executing\n    let mut cmd = std::process::Command::new(\"bash\");\n    cmd.arg(\"-n\") // Syntax check only\n        .arg(\"-c\")\n        .arg(&command);\n\n    match cmd.output() {\n        Ok(output) => {\n            if output.status.success() {\n                Ok(serde_json::json!({\n                    \"valid\": true,\n                    \"message\": \"Command syntax is valid\"\n                }))\n            } else {\n                let stderr = String::from_utf8_lossy(&output.stderr);\n                Ok(serde_json::json!({\n                    \"valid\": false,\n                    \"message\": format!(\"Syntax error: {}\", stderr)\n                }))\n            }\n        }\n        Err(e) => Err(format!(\"Failed to validate command: {}\", e)),\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::io::Write;\n    use tempfile::TempDir;\n\n    /// Helper function to create a test session file\n    fn create_test_session_file(\n        dir: &PathBuf,\n        filename: &str,\n        content: &str,\n    ) -> Result<(), std::io::Error> {\n        let file_path = dir.join(filename);\n        let mut file = fs::File::create(file_path)?;\n        file.write_all(content.as_bytes())?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_normal_case() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        // Create a session file with cwd on the first line\n        let content = r#\"{\"type\":\"system\",\"cwd\":\"/Users/test/my-project\"}\"#;\n        create_test_session_file(&project_dir, \"session1.jsonl\", content).unwrap();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"/Users/test/my-project\");\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_with_hyphen() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        // This is the bug scenario - project path contains hyphens\n        let content = r#\"{\"type\":\"system\",\"cwd\":\"/Users/test/data-discovery\"}\"#;\n        create_test_session_file(&project_dir, \"session1.jsonl\", content).unwrap();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"/Users/test/data-discovery\");\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_null_cwd_first_line() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        // First line has null cwd, second line has valid path\n        let content = format!(\n            \"{}\\n{}\",\n            r#\"{\"type\":\"system\",\"cwd\":null}\"#,\n            r#\"{\"type\":\"system\",\"cwd\":\"/Users/test/valid-path\"}\"#\n        );\n        create_test_session_file(&project_dir, \"session1.jsonl\", &content).unwrap();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"/Users/test/valid-path\");\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_multiple_lines() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        // Multiple lines with cwd appearing on line 5\n        let content = format!(\n            \"{}\\n{}\\n{}\\n{}\\n{}\",\n            r#\"{\"type\":\"other\"}\"#,\n            r#\"{\"type\":\"system\",\"cwd\":null}\"#,\n            r#\"{\"type\":\"message\"}\"#,\n            r#\"{\"type\":\"system\"}\"#,\n            r#\"{\"type\":\"system\",\"cwd\":\"/Users/test/project\"}\"#\n        );\n        create_test_session_file(&project_dir, \"session1.jsonl\", &content).unwrap();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"/Users/test/project\");\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_empty_dir() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_err());\n        assert_eq!(\n            result.unwrap_err(),\n            \"Could not determine project path from session files\"\n        );\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_no_jsonl_files() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        // Create a non-JSONL file\n        create_test_session_file(&project_dir, \"readme.txt\", \"Some text\").unwrap();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_no_cwd() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        // JSONL file without any cwd field\n        let content = format!(\n            \"{}\\n{}\\n{}\",\n            r#\"{\"type\":\"system\"}\"#, r#\"{\"type\":\"message\"}\"#, r#\"{\"type\":\"other\"}\"#\n        );\n        create_test_session_file(&project_dir, \"session1.jsonl\", &content).unwrap();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_get_project_path_from_sessions_multiple_sessions() {\n        let temp_dir = TempDir::new().unwrap();\n        let project_dir = temp_dir.path().to_path_buf();\n\n        // Create multiple session files - should return from first valid one\n        create_test_session_file(\n            &project_dir,\n            \"session1.jsonl\",\n            r#\"{\"type\":\"system\",\"cwd\":\"/path1\"}\"#,\n        )\n        .unwrap();\n        create_test_session_file(\n            &project_dir,\n            \"session2.jsonl\",\n            r#\"{\"type\":\"system\",\"cwd\":\"/path2\"}\"#,\n        )\n        .unwrap();\n\n        let result = get_project_path_from_sessions(&project_dir);\n        assert!(result.is_ok());\n        // Should get one of the paths (implementation checks first file it finds)\n        let path = result.unwrap();\n        assert!(path == \"/path1\" || path == \"/path2\");\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/commands/mcp.rs",
    "content": "use anyhow::{Context, Result};\nuse dirs;\nuse log::{error, info};\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse std::fs;\nuse std::path::PathBuf;\nuse std::process::Command;\nuse tauri::AppHandle;\n\n/// Helper function to create a std::process::Command with proper environment variables\n/// This ensures commands like Claude can find Node.js and other dependencies\nfn create_command_with_env(program: &str) -> Command {\n    crate::claude_binary::create_command_with_env(program)\n}\n\n/// Finds the full path to the claude binary\n/// This is necessary because macOS apps have a limited PATH environment\nfn find_claude_binary(app_handle: &AppHandle) -> Result<String> {\n    crate::claude_binary::find_claude_binary(app_handle).map_err(|e| anyhow::anyhow!(e))\n}\n\n/// Represents an MCP server configuration\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct MCPServer {\n    /// Server name/identifier\n    pub name: String,\n    /// Transport type: \"stdio\" or \"sse\"\n    pub transport: String,\n    /// Command to execute (for stdio)\n    pub command: Option<String>,\n    /// Command arguments (for stdio)\n    pub args: Vec<String>,\n    /// Environment variables\n    pub env: HashMap<String, String>,\n    /// URL endpoint (for SSE)\n    pub url: Option<String>,\n    /// Configuration scope: \"local\", \"project\", or \"user\"\n    pub scope: String,\n    /// Whether the server is currently active\n    pub is_active: bool,\n    /// Server status\n    pub status: ServerStatus,\n}\n\n/// Server status information\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ServerStatus {\n    /// Whether the server is running\n    pub running: bool,\n    /// Last error message if any\n    pub error: Option<String>,\n    /// Last checked timestamp\n    pub last_checked: Option<u64>,\n}\n\n/// MCP configuration for project scope (.mcp.json)\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct MCPProjectConfig {\n    #[serde(rename = \"mcpServers\")]\n    pub mcp_servers: HashMap<String, MCPServerConfig>,\n}\n\n/// Individual server configuration in .mcp.json\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct MCPServerConfig {\n    pub command: String,\n    #[serde(default)]\n    pub args: Vec<String>,\n    #[serde(default)]\n    pub env: HashMap<String, String>,\n}\n\n/// Result of adding a server\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct AddServerResult {\n    pub success: bool,\n    pub message: String,\n    pub server_name: Option<String>,\n}\n\n/// Import result for multiple servers\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ImportResult {\n    pub imported_count: u32,\n    pub failed_count: u32,\n    pub servers: Vec<ImportServerResult>,\n}\n\n/// Result for individual server import\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ImportServerResult {\n    pub name: String,\n    pub success: bool,\n    pub error: Option<String>,\n}\n\n/// Executes a claude mcp command\nfn execute_claude_mcp_command(app_handle: &AppHandle, args: Vec<&str>) -> Result<String> {\n    info!(\"Executing claude mcp command with args: {:?}\", args);\n\n    let claude_path = find_claude_binary(app_handle)?;\n    let mut cmd = create_command_with_env(&claude_path);\n    cmd.arg(\"mcp\");\n    for arg in args {\n        cmd.arg(arg);\n    }\n\n    let output = cmd.output().context(\"Failed to execute claude command\")?;\n\n    if output.status.success() {\n        Ok(String::from_utf8_lossy(&output.stdout).to_string())\n    } else {\n        let stderr = String::from_utf8_lossy(&output.stderr).to_string();\n        Err(anyhow::anyhow!(\"Command failed: {}\", stderr))\n    }\n}\n\n/// Adds a new MCP server\n#[tauri::command]\npub async fn mcp_add(\n    app: AppHandle,\n    name: String,\n    transport: String,\n    command: Option<String>,\n    args: Vec<String>,\n    env: HashMap<String, String>,\n    url: Option<String>,\n    scope: String,\n) -> Result<AddServerResult, String> {\n    info!(\"Adding MCP server: {} with transport: {}\", name, transport);\n\n    // Prepare owned strings for environment variables\n    let env_args: Vec<String> = env\n        .iter()\n        .map(|(key, value)| format!(\"{}={}\", key, value))\n        .collect();\n\n    let mut cmd_args = vec![\"add\"];\n\n    // Add scope flag\n    cmd_args.push(\"-s\");\n    cmd_args.push(&scope);\n\n    // Add transport flag for SSE\n    if transport == \"sse\" {\n        cmd_args.push(\"--transport\");\n        cmd_args.push(\"sse\");\n    }\n\n    // Add environment variables\n    for (i, _) in env.iter().enumerate() {\n        cmd_args.push(\"-e\");\n        cmd_args.push(&env_args[i]);\n    }\n\n    // Add name\n    cmd_args.push(&name);\n\n    // Add command/URL based on transport\n    if transport == \"stdio\" {\n        if let Some(cmd) = &command {\n            // Add \"--\" separator before command to prevent argument parsing issues\n            if !args.is_empty() || cmd.contains('-') {\n                cmd_args.push(\"--\");\n            }\n            cmd_args.push(cmd);\n            // Add arguments\n            for arg in &args {\n                cmd_args.push(arg);\n            }\n        } else {\n            return Ok(AddServerResult {\n                success: false,\n                message: \"Command is required for stdio transport\".to_string(),\n                server_name: None,\n            });\n        }\n    } else if transport == \"sse\" {\n        if let Some(url_str) = &url {\n            cmd_args.push(url_str);\n        } else {\n            return Ok(AddServerResult {\n                success: false,\n                message: \"URL is required for SSE transport\".to_string(),\n                server_name: None,\n            });\n        }\n    }\n\n    match execute_claude_mcp_command(&app, cmd_args) {\n        Ok(output) => {\n            info!(\"Successfully added MCP server: {}\", name);\n            Ok(AddServerResult {\n                success: true,\n                message: output.trim().to_string(),\n                server_name: Some(name),\n            })\n        }\n        Err(e) => {\n            error!(\"Failed to add MCP server: {}\", e);\n            Ok(AddServerResult {\n                success: false,\n                message: e.to_string(),\n                server_name: None,\n            })\n        }\n    }\n}\n\n/// Lists all configured MCP servers\n#[tauri::command]\npub async fn mcp_list(app: AppHandle) -> Result<Vec<MCPServer>, String> {\n    info!(\"Listing MCP servers\");\n\n    match execute_claude_mcp_command(&app, vec![\"list\"]) {\n        Ok(output) => {\n            info!(\"Raw output from 'claude mcp list': {:?}\", output);\n            let trimmed = output.trim();\n            info!(\"Trimmed output: {:?}\", trimmed);\n\n            // Check if no servers are configured\n            if trimmed.contains(\"No MCP servers configured\") || trimmed.is_empty() {\n                info!(\"No servers found - empty or 'No MCP servers' message\");\n                return Ok(vec![]);\n            }\n\n            // Parse the text output, handling multi-line commands\n            let mut servers = Vec::new();\n            let lines: Vec<&str> = trimmed.lines().collect();\n            info!(\"Total lines in output: {}\", lines.len());\n            for (idx, line) in lines.iter().enumerate() {\n                info!(\"Line {}: {:?}\", idx, line);\n            }\n\n            let mut i = 0;\n\n            while i < lines.len() {\n                let line = lines[i];\n                info!(\"Processing line {}: {:?}\", i, line);\n\n                // Check if this line starts a new server entry\n                if let Some(colon_pos) = line.find(':') {\n                    info!(\"Found colon at position {} in line: {:?}\", colon_pos, line);\n                    // Make sure this is a server name line (not part of a path)\n                    // Server names typically don't contain '/' or '\\'\n                    let potential_name = line[..colon_pos].trim();\n                    info!(\"Potential server name: {:?}\", potential_name);\n\n                    if !potential_name.contains('/') && !potential_name.contains('\\\\') {\n                        info!(\"Valid server name detected: {:?}\", potential_name);\n                        let name = potential_name.to_string();\n                        let mut command_parts = vec![line[colon_pos + 1..].trim().to_string()];\n                        info!(\"Initial command part: {:?}\", command_parts[0]);\n\n                        // Check if command continues on next lines\n                        i += 1;\n                        while i < lines.len() {\n                            let next_line = lines[i];\n                            info!(\"Checking next line {} for continuation: {:?}\", i, next_line);\n\n                            // If the next line starts with a server name pattern, break\n                            if next_line.contains(':') {\n                                let potential_next_name =\n                                    next_line.split(':').next().unwrap_or(\"\").trim();\n                                info!(\n                                    \"Found colon in next line, potential name: {:?}\",\n                                    potential_next_name\n                                );\n                                if !potential_next_name.is_empty()\n                                    && !potential_next_name.contains('/')\n                                    && !potential_next_name.contains('\\\\')\n                                {\n                                    info!(\"Next line is a new server, breaking\");\n                                    break;\n                                }\n                            }\n                            // Otherwise, this line is a continuation of the command\n                            info!(\"Line {} is a continuation\", i);\n                            command_parts.push(next_line.trim().to_string());\n                            i += 1;\n                        }\n\n                        // Join all command parts\n                        let full_command = command_parts.join(\" \");\n                        info!(\"Full command for server '{}': {:?}\", name, full_command);\n\n                        // For now, we'll create a basic server entry\n                        servers.push(MCPServer {\n                            name: name.clone(),\n                            transport: \"stdio\".to_string(), // Default assumption\n                            command: Some(full_command),\n                            args: vec![],\n                            env: HashMap::new(),\n                            url: None,\n                            scope: \"local\".to_string(), // Default assumption\n                            is_active: false,\n                            status: ServerStatus {\n                                running: false,\n                                error: None,\n                                last_checked: None,\n                            },\n                        });\n                        info!(\"Added server: {:?}\", name);\n\n                        continue;\n                    } else {\n                        info!(\"Skipping line - name contains path separators\");\n                    }\n                } else {\n                    info!(\"No colon found in line {}\", i);\n                }\n\n                i += 1;\n            }\n\n            info!(\"Found {} MCP servers total\", servers.len());\n            for (idx, server) in servers.iter().enumerate() {\n                info!(\n                    \"Server {}: name='{}', command={:?}\",\n                    idx, server.name, server.command\n                );\n            }\n            Ok(servers)\n        }\n        Err(e) => {\n            error!(\"Failed to list MCP servers: {}\", e);\n            Err(e.to_string())\n        }\n    }\n}\n\n/// Gets details for a specific MCP server\n#[tauri::command]\npub async fn mcp_get(app: AppHandle, name: String) -> Result<MCPServer, String> {\n    info!(\"Getting MCP server details for: {}\", name);\n\n    match execute_claude_mcp_command(&app, vec![\"get\", &name]) {\n        Ok(output) => {\n            // Parse the structured text output\n            let mut scope = \"local\".to_string();\n            let mut transport = \"stdio\".to_string();\n            let mut command = None;\n            let mut args = vec![];\n            let env = HashMap::new();\n            let mut url = None;\n\n            for line in output.lines() {\n                let line = line.trim();\n\n                if line.starts_with(\"Scope:\") {\n                    let scope_part = line.replace(\"Scope:\", \"\").trim().to_string();\n                    if scope_part.to_lowercase().contains(\"local\") {\n                        scope = \"local\".to_string();\n                    } else if scope_part.to_lowercase().contains(\"project\") {\n                        scope = \"project\".to_string();\n                    } else if scope_part.to_lowercase().contains(\"user\")\n                        || scope_part.to_lowercase().contains(\"global\")\n                    {\n                        scope = \"user\".to_string();\n                    }\n                } else if line.starts_with(\"Type:\") {\n                    transport = line.replace(\"Type:\", \"\").trim().to_string();\n                } else if line.starts_with(\"Command:\") {\n                    command = Some(line.replace(\"Command:\", \"\").trim().to_string());\n                } else if line.starts_with(\"Args:\") {\n                    let args_str = line.replace(\"Args:\", \"\").trim().to_string();\n                    if !args_str.is_empty() {\n                        args = args_str.split_whitespace().map(|s| s.to_string()).collect();\n                    }\n                } else if line.starts_with(\"URL:\") {\n                    url = Some(line.replace(\"URL:\", \"\").trim().to_string());\n                } else if line.starts_with(\"Environment:\") {\n                    // TODO: Parse environment variables if they're listed\n                    // For now, we'll leave it empty\n                }\n            }\n\n            Ok(MCPServer {\n                name,\n                transport,\n                command,\n                args,\n                env,\n                url,\n                scope,\n                is_active: false,\n                status: ServerStatus {\n                    running: false,\n                    error: None,\n                    last_checked: None,\n                },\n            })\n        }\n        Err(e) => {\n            error!(\"Failed to get MCP server: {}\", e);\n            Err(e.to_string())\n        }\n    }\n}\n\n/// Removes an MCP server\n#[tauri::command]\npub async fn mcp_remove(app: AppHandle, name: String) -> Result<String, String> {\n    info!(\"Removing MCP server: {}\", name);\n\n    match execute_claude_mcp_command(&app, vec![\"remove\", &name]) {\n        Ok(output) => {\n            info!(\"Successfully removed MCP server: {}\", name);\n            Ok(output.trim().to_string())\n        }\n        Err(e) => {\n            error!(\"Failed to remove MCP server: {}\", e);\n            Err(e.to_string())\n        }\n    }\n}\n\n/// Adds an MCP server from JSON configuration\n#[tauri::command]\npub async fn mcp_add_json(\n    app: AppHandle,\n    name: String,\n    json_config: String,\n    scope: String,\n) -> Result<AddServerResult, String> {\n    info!(\n        \"Adding MCP server from JSON: {} with scope: {}\",\n        name, scope\n    );\n\n    // Build command args\n    let mut cmd_args = vec![\"add-json\", &name, &json_config];\n\n    // Add scope flag\n    let scope_flag = \"-s\";\n    cmd_args.push(scope_flag);\n    cmd_args.push(&scope);\n\n    match execute_claude_mcp_command(&app, cmd_args) {\n        Ok(output) => {\n            info!(\"Successfully added MCP server from JSON: {}\", name);\n            Ok(AddServerResult {\n                success: true,\n                message: output.trim().to_string(),\n                server_name: Some(name),\n            })\n        }\n        Err(e) => {\n            error!(\"Failed to add MCP server from JSON: {}\", e);\n            Ok(AddServerResult {\n                success: false,\n                message: e.to_string(),\n                server_name: None,\n            })\n        }\n    }\n}\n\n/// Imports MCP servers from Claude Desktop\n#[tauri::command]\npub async fn mcp_add_from_claude_desktop(\n    app: AppHandle,\n    scope: String,\n) -> Result<ImportResult, String> {\n    info!(\n        \"Importing MCP servers from Claude Desktop with scope: {}\",\n        scope\n    );\n\n    // Get Claude Desktop config path based on platform\n    let config_path = if cfg!(target_os = \"macos\") {\n        dirs::home_dir()\n            .ok_or_else(|| \"Could not find home directory\".to_string())?\n            .join(\"Library\")\n            .join(\"Application Support\")\n            .join(\"Claude\")\n            .join(\"claude_desktop_config.json\")\n    } else if cfg!(target_os = \"linux\") {\n        // For WSL/Linux, check common locations\n        dirs::config_dir()\n            .ok_or_else(|| \"Could not find config directory\".to_string())?\n            .join(\"Claude\")\n            .join(\"claude_desktop_config.json\")\n    } else {\n        return Err(\n            \"Import from Claude Desktop is only supported on macOS and Linux/WSL\".to_string(),\n        );\n    };\n\n    // Check if config file exists\n    if !config_path.exists() {\n        return Err(\n            \"Claude Desktop configuration not found. Make sure Claude Desktop is installed.\"\n                .to_string(),\n        );\n    }\n\n    // Read and parse the config file\n    let config_content = fs::read_to_string(&config_path)\n        .map_err(|e| format!(\"Failed to read Claude Desktop config: {}\", e))?;\n\n    let config: serde_json::Value = serde_json::from_str(&config_content)\n        .map_err(|e| format!(\"Failed to parse Claude Desktop config: {}\", e))?;\n\n    // Extract MCP servers\n    let mcp_servers = config\n        .get(\"mcpServers\")\n        .and_then(|v| v.as_object())\n        .ok_or_else(|| \"No MCP servers found in Claude Desktop config\".to_string())?;\n\n    let mut imported_count = 0;\n    let mut failed_count = 0;\n    let mut server_results = Vec::new();\n\n    // Import each server using add-json\n    for (name, server_config) in mcp_servers {\n        info!(\"Importing server: {}\", name);\n\n        // Convert Claude Desktop format to add-json format\n        let mut json_config = serde_json::Map::new();\n\n        // All Claude Desktop servers are stdio type\n        json_config.insert(\n            \"type\".to_string(),\n            serde_json::Value::String(\"stdio\".to_string()),\n        );\n\n        // Add command\n        if let Some(command) = server_config.get(\"command\").and_then(|v| v.as_str()) {\n            json_config.insert(\n                \"command\".to_string(),\n                serde_json::Value::String(command.to_string()),\n            );\n        } else {\n            failed_count += 1;\n            server_results.push(ImportServerResult {\n                name: name.clone(),\n                success: false,\n                error: Some(\"Missing command field\".to_string()),\n            });\n            continue;\n        }\n\n        // Add args if present\n        if let Some(args) = server_config.get(\"args\").and_then(|v| v.as_array()) {\n            json_config.insert(\"args\".to_string(), args.clone().into());\n        } else {\n            json_config.insert(\"args\".to_string(), serde_json::Value::Array(vec![]));\n        }\n\n        // Add env if present\n        if let Some(env) = server_config.get(\"env\").and_then(|v| v.as_object()) {\n            json_config.insert(\"env\".to_string(), env.clone().into());\n        } else {\n            json_config.insert(\n                \"env\".to_string(),\n                serde_json::Value::Object(serde_json::Map::new()),\n            );\n        }\n\n        // Convert to JSON string\n        let json_str = serde_json::to_string(&json_config)\n            .map_err(|e| format!(\"Failed to serialize config for {}: {}\", name, e))?;\n\n        // Call add-json command\n        match mcp_add_json(app.clone(), name.clone(), json_str, scope.clone()).await {\n            Ok(result) => {\n                if result.success {\n                    imported_count += 1;\n                    server_results.push(ImportServerResult {\n                        name: name.clone(),\n                        success: true,\n                        error: None,\n                    });\n                    info!(\"Successfully imported server: {}\", name);\n                } else {\n                    failed_count += 1;\n                    let error_msg = result.message.clone();\n                    server_results.push(ImportServerResult {\n                        name: name.clone(),\n                        success: false,\n                        error: Some(result.message),\n                    });\n                    error!(\"Failed to import server {}: {}\", name, error_msg);\n                }\n            }\n            Err(e) => {\n                failed_count += 1;\n                let error_msg = e.clone();\n                server_results.push(ImportServerResult {\n                    name: name.clone(),\n                    success: false,\n                    error: Some(e),\n                });\n                error!(\"Error importing server {}: {}\", name, error_msg);\n            }\n        }\n    }\n\n    info!(\n        \"Import complete: {} imported, {} failed\",\n        imported_count, failed_count\n    );\n\n    Ok(ImportResult {\n        imported_count,\n        failed_count,\n        servers: server_results,\n    })\n}\n\n/// Starts Claude Code as an MCP server\n#[tauri::command]\npub async fn mcp_serve(app: AppHandle) -> Result<String, String> {\n    info!(\"Starting Claude Code as MCP server\");\n\n    // Start the server in a separate process\n    let claude_path = match find_claude_binary(&app) {\n        Ok(path) => path,\n        Err(e) => {\n            error!(\"Failed to find claude binary: {}\", e);\n            return Err(e.to_string());\n        }\n    };\n\n    let mut cmd = create_command_with_env(&claude_path);\n    cmd.arg(\"mcp\").arg(\"serve\");\n\n    match cmd.spawn() {\n        Ok(_) => {\n            info!(\"Successfully started Claude Code MCP server\");\n            Ok(\"Claude Code MCP server started\".to_string())\n        }\n        Err(e) => {\n            error!(\"Failed to start MCP server: {}\", e);\n            Err(e.to_string())\n        }\n    }\n}\n\n/// Tests connection to an MCP server\n#[tauri::command]\npub async fn mcp_test_connection(app: AppHandle, name: String) -> Result<String, String> {\n    info!(\"Testing connection to MCP server: {}\", name);\n\n    // For now, we'll use the get command to test if the server exists\n    match execute_claude_mcp_command(&app, vec![\"get\", &name]) {\n        Ok(_) => Ok(format!(\"Connection to {} successful\", name)),\n        Err(e) => Err(e.to_string()),\n    }\n}\n\n/// Resets project-scoped server approval choices\n#[tauri::command]\npub async fn mcp_reset_project_choices(app: AppHandle) -> Result<String, String> {\n    info!(\"Resetting MCP project choices\");\n\n    match execute_claude_mcp_command(&app, vec![\"reset-project-choices\"]) {\n        Ok(output) => {\n            info!(\"Successfully reset MCP project choices\");\n            Ok(output.trim().to_string())\n        }\n        Err(e) => {\n            error!(\"Failed to reset project choices: {}\", e);\n            Err(e.to_string())\n        }\n    }\n}\n\n/// Gets the status of MCP servers\n#[tauri::command]\npub async fn mcp_get_server_status() -> Result<HashMap<String, ServerStatus>, String> {\n    info!(\"Getting MCP server status\");\n\n    // TODO: Implement actual status checking\n    // For now, return empty status\n    Ok(HashMap::new())\n}\n\n/// Reads .mcp.json from the current project\n#[tauri::command]\npub async fn mcp_read_project_config(project_path: String) -> Result<MCPProjectConfig, String> {\n    info!(\"Reading .mcp.json from project: {}\", project_path);\n\n    let mcp_json_path = PathBuf::from(&project_path).join(\".mcp.json\");\n\n    if !mcp_json_path.exists() {\n        return Ok(MCPProjectConfig {\n            mcp_servers: HashMap::new(),\n        });\n    }\n\n    match fs::read_to_string(&mcp_json_path) {\n        Ok(content) => match serde_json::from_str::<MCPProjectConfig>(&content) {\n            Ok(config) => Ok(config),\n            Err(e) => {\n                error!(\"Failed to parse .mcp.json: {}\", e);\n                Err(format!(\"Failed to parse .mcp.json: {}\", e))\n            }\n        },\n        Err(e) => {\n            error!(\"Failed to read .mcp.json: {}\", e);\n            Err(format!(\"Failed to read .mcp.json: {}\", e))\n        }\n    }\n}\n\n/// Saves .mcp.json to the current project\n#[tauri::command]\npub async fn mcp_save_project_config(\n    project_path: String,\n    config: MCPProjectConfig,\n) -> Result<String, String> {\n    info!(\"Saving .mcp.json to project: {}\", project_path);\n\n    let mcp_json_path = PathBuf::from(&project_path).join(\".mcp.json\");\n\n    let json_content = serde_json::to_string_pretty(&config)\n        .map_err(|e| format!(\"Failed to serialize config: {}\", e))?;\n\n    fs::write(&mcp_json_path, json_content)\n        .map_err(|e| format!(\"Failed to write .mcp.json: {}\", e))?;\n\n    Ok(\"Project MCP configuration saved\".to_string())\n}\n"
  },
  {
    "path": "src-tauri/src/commands/mod.rs",
    "content": "pub mod agents;\npub mod claude;\npub mod mcp;\npub mod proxy;\npub mod slash_commands;\npub mod storage;\npub mod usage;\n"
  },
  {
    "path": "src-tauri/src/commands/proxy.rs",
    "content": "use rusqlite::params;\nuse serde::{Deserialize, Serialize};\nuse tauri::State;\n\nuse crate::commands::agents::AgentDb;\n\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct ProxySettings {\n    pub http_proxy: Option<String>,\n    pub https_proxy: Option<String>,\n    pub no_proxy: Option<String>,\n    pub all_proxy: Option<String>,\n    pub enabled: bool,\n}\n\nimpl Default for ProxySettings {\n    fn default() -> Self {\n        Self {\n            http_proxy: None,\n            https_proxy: None,\n            no_proxy: None,\n            all_proxy: None,\n            enabled: false,\n        }\n    }\n}\n\n/// Get proxy settings from the database\n#[tauri::command]\npub async fn get_proxy_settings(db: State<'_, AgentDb>) -> Result<ProxySettings, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    let mut settings = ProxySettings::default();\n\n    // Query each proxy setting\n    let keys = vec![\n        (\"proxy_enabled\", \"enabled\"),\n        (\"proxy_http\", \"http_proxy\"),\n        (\"proxy_https\", \"https_proxy\"),\n        (\"proxy_no\", \"no_proxy\"),\n        (\"proxy_all\", \"all_proxy\"),\n    ];\n\n    for (db_key, field) in keys {\n        if let Ok(value) = conn.query_row(\n            \"SELECT value FROM app_settings WHERE key = ?1\",\n            params![db_key],\n            |row| row.get::<_, String>(0),\n        ) {\n            match field {\n                \"enabled\" => settings.enabled = value == \"true\",\n                \"http_proxy\" => settings.http_proxy = Some(value).filter(|s| !s.is_empty()),\n                \"https_proxy\" => settings.https_proxy = Some(value).filter(|s| !s.is_empty()),\n                \"no_proxy\" => settings.no_proxy = Some(value).filter(|s| !s.is_empty()),\n                \"all_proxy\" => settings.all_proxy = Some(value).filter(|s| !s.is_empty()),\n                _ => {}\n            }\n        }\n    }\n\n    Ok(settings)\n}\n\n/// Save proxy settings to the database\n#[tauri::command]\npub async fn save_proxy_settings(\n    db: State<'_, AgentDb>,\n    settings: ProxySettings,\n) -> Result<(), String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Save each setting\n    let values = vec![\n        (\"proxy_enabled\", settings.enabled.to_string()),\n        (\n            \"proxy_http\",\n            settings.http_proxy.clone().unwrap_or_default(),\n        ),\n        (\n            \"proxy_https\",\n            settings.https_proxy.clone().unwrap_or_default(),\n        ),\n        (\"proxy_no\", settings.no_proxy.clone().unwrap_or_default()),\n        (\"proxy_all\", settings.all_proxy.clone().unwrap_or_default()),\n    ];\n\n    for (key, value) in values {\n        conn.execute(\n            \"INSERT OR REPLACE INTO app_settings (key, value) VALUES (?1, ?2)\",\n            params![key, value],\n        )\n        .map_err(|e| format!(\"Failed to save {}: {}\", key, e))?;\n    }\n\n    // Apply the proxy settings immediately to the current process\n    apply_proxy_settings(&settings);\n\n    Ok(())\n}\n\n/// Apply proxy settings as environment variables\npub fn apply_proxy_settings(settings: &ProxySettings) {\n    log::info!(\"Applying proxy settings: enabled={}\", settings.enabled);\n\n    if !settings.enabled {\n        // Clear proxy environment variables if disabled\n        log::info!(\"Clearing proxy environment variables\");\n        std::env::remove_var(\"HTTP_PROXY\");\n        std::env::remove_var(\"HTTPS_PROXY\");\n        std::env::remove_var(\"NO_PROXY\");\n        std::env::remove_var(\"ALL_PROXY\");\n        // Also clear lowercase versions\n        std::env::remove_var(\"http_proxy\");\n        std::env::remove_var(\"https_proxy\");\n        std::env::remove_var(\"no_proxy\");\n        std::env::remove_var(\"all_proxy\");\n        return;\n    }\n\n    // Ensure NO_PROXY includes localhost by default\n    let mut no_proxy_list = vec![\"localhost\", \"127.0.0.1\", \"::1\", \"0.0.0.0\"];\n    if let Some(user_no_proxy) = &settings.no_proxy {\n        if !user_no_proxy.is_empty() {\n            no_proxy_list.push(user_no_proxy.as_str());\n        }\n    }\n    let no_proxy_value = no_proxy_list.join(\",\");\n\n    // Set proxy environment variables (uppercase is standard)\n    if let Some(http_proxy) = &settings.http_proxy {\n        if !http_proxy.is_empty() {\n            log::info!(\"Setting HTTP_PROXY={}\", http_proxy);\n            std::env::set_var(\"HTTP_PROXY\", http_proxy);\n        }\n    }\n\n    if let Some(https_proxy) = &settings.https_proxy {\n        if !https_proxy.is_empty() {\n            log::info!(\"Setting HTTPS_PROXY={}\", https_proxy);\n            std::env::set_var(\"HTTPS_PROXY\", https_proxy);\n        }\n    }\n\n    // Always set NO_PROXY to include localhost\n    log::info!(\"Setting NO_PROXY={}\", no_proxy_value);\n    std::env::set_var(\"NO_PROXY\", &no_proxy_value);\n\n    if let Some(all_proxy) = &settings.all_proxy {\n        if !all_proxy.is_empty() {\n            log::info!(\"Setting ALL_PROXY={}\", all_proxy);\n            std::env::set_var(\"ALL_PROXY\", all_proxy);\n        }\n    }\n\n    // Log current proxy environment variables for debugging\n    log::info!(\"Current proxy environment variables:\");\n    for (key, value) in std::env::vars() {\n        if key.contains(\"PROXY\") || key.contains(\"proxy\") {\n            log::info!(\"  {}={}\", key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/commands/slash_commands.rs",
    "content": "use anyhow::{Context, Result};\nuse dirs;\nuse log::{debug, error, info};\nuse serde::{Deserialize, Serialize};\nuse std::fs;\nuse std::path::{Path, PathBuf};\n\n/// Represents a custom slash command\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct SlashCommand {\n    /// Unique identifier for the command (derived from file path)\n    pub id: String,\n    /// Command name (without prefix)\n    pub name: String,\n    /// Full command with prefix (e.g., \"/project:optimize\")\n    pub full_command: String,\n    /// Command scope: \"project\" or \"user\"\n    pub scope: String,\n    /// Optional namespace (e.g., \"frontend\" in \"/project:frontend:component\")\n    pub namespace: Option<String>,\n    /// Path to the markdown file\n    pub file_path: String,\n    /// Command content (markdown body)\n    pub content: String,\n    /// Optional description from frontmatter\n    pub description: Option<String>,\n    /// Allowed tools from frontmatter\n    pub allowed_tools: Vec<String>,\n    /// Whether the command has bash commands (!)\n    pub has_bash_commands: bool,\n    /// Whether the command has file references (@)\n    pub has_file_references: bool,\n    /// Whether the command uses $ARGUMENTS placeholder\n    pub accepts_arguments: bool,\n}\n\n/// YAML frontmatter structure\n#[derive(Debug, Deserialize)]\nstruct CommandFrontmatter {\n    #[serde(rename = \"allowed-tools\")]\n    allowed_tools: Option<Vec<String>>,\n    description: Option<String>,\n}\n\n/// Parse a markdown file with optional YAML frontmatter\nfn parse_markdown_with_frontmatter(content: &str) -> Result<(Option<CommandFrontmatter>, String)> {\n    let lines: Vec<&str> = content.lines().collect();\n\n    // Check if the file starts with YAML frontmatter\n    if lines.is_empty() || lines[0] != \"---\" {\n        // No frontmatter\n        return Ok((None, content.to_string()));\n    }\n\n    // Find the end of frontmatter\n    let mut frontmatter_end = None;\n    for (i, line) in lines.iter().enumerate().skip(1) {\n        if *line == \"---\" {\n            frontmatter_end = Some(i);\n            break;\n        }\n    }\n\n    if let Some(end) = frontmatter_end {\n        // Extract frontmatter\n        let frontmatter_content = lines[1..end].join(\"\\n\");\n        let body_content = lines[(end + 1)..].join(\"\\n\");\n\n        // Parse YAML\n        match serde_yaml::from_str::<CommandFrontmatter>(&frontmatter_content) {\n            Ok(frontmatter) => Ok((Some(frontmatter), body_content)),\n            Err(e) => {\n                debug!(\"Failed to parse frontmatter: {}\", e);\n                // Return full content if frontmatter parsing fails\n                Ok((None, content.to_string()))\n            }\n        }\n    } else {\n        // Malformed frontmatter, treat as regular content\n        Ok((None, content.to_string()))\n    }\n}\n\n/// Extract command name and namespace from file path\nfn extract_command_info(file_path: &Path, base_path: &Path) -> Result<(String, Option<String>)> {\n    let relative_path = file_path\n        .strip_prefix(base_path)\n        .context(\"Failed to get relative path\")?;\n\n    // Remove .md extension\n    let path_without_ext = relative_path\n        .with_extension(\"\")\n        .to_string_lossy()\n        .to_string();\n\n    // Split into components\n    let components: Vec<&str> = path_without_ext.split('/').collect();\n\n    if components.is_empty() {\n        return Err(anyhow::anyhow!(\"Invalid command path\"));\n    }\n\n    if components.len() == 1 {\n        // No namespace\n        Ok((components[0].to_string(), None))\n    } else {\n        // Last component is the command name, rest is namespace\n        let command_name = components.last().unwrap().to_string();\n        let namespace = components[..components.len() - 1].join(\":\");\n        Ok((command_name, Some(namespace)))\n    }\n}\n\n/// Load a single command from a markdown file\nfn load_command_from_file(file_path: &Path, base_path: &Path, scope: &str) -> Result<SlashCommand> {\n    debug!(\"Loading command from: {:?}\", file_path);\n\n    // Read file content\n    let content = fs::read_to_string(file_path).context(\"Failed to read command file\")?;\n\n    // Parse frontmatter\n    let (frontmatter, body) = parse_markdown_with_frontmatter(&content)?;\n\n    // Extract command info\n    let (name, namespace) = extract_command_info(file_path, base_path)?;\n\n    // Build full command (no scope prefix, just /command or /namespace:command)\n    let full_command = match &namespace {\n        Some(ns) => format!(\"/{ns}:{name}\"),\n        None => format!(\"/{name}\"),\n    };\n\n    // Generate unique ID\n    let id = format!(\n        \"{}-{}\",\n        scope,\n        file_path.to_string_lossy().replace('/', \"-\")\n    );\n\n    // Check for special content\n    let has_bash_commands = body.contains(\"!`\");\n    let has_file_references = body.contains('@');\n    let accepts_arguments = body.contains(\"$ARGUMENTS\");\n\n    // Extract metadata from frontmatter\n    let (description, allowed_tools) = if let Some(fm) = frontmatter {\n        (fm.description, fm.allowed_tools.unwrap_or_default())\n    } else {\n        (None, Vec::new())\n    };\n\n    Ok(SlashCommand {\n        id,\n        name,\n        full_command,\n        scope: scope.to_string(),\n        namespace,\n        file_path: file_path.to_string_lossy().to_string(),\n        content: body,\n        description,\n        allowed_tools,\n        has_bash_commands,\n        has_file_references,\n        accepts_arguments,\n    })\n}\n\n/// Recursively find all markdown files in a directory\nfn find_markdown_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {\n    if !dir.exists() {\n        return Ok(());\n    }\n\n    for entry in fs::read_dir(dir)? {\n        let entry = entry?;\n        let path = entry.path();\n\n        // Skip hidden files/directories\n        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {\n            if name.starts_with('.') {\n                continue;\n            }\n        }\n\n        if path.is_dir() {\n            find_markdown_files(&path, files)?;\n        } else if path.is_file() {\n            if let Some(ext) = path.extension() {\n                if ext == \"md\" {\n                    files.push(path);\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Create default/built-in slash commands\nfn create_default_commands() -> Vec<SlashCommand> {\n    vec![\n        SlashCommand {\n            id: \"default-add-dir\".to_string(),\n            name: \"add-dir\".to_string(),\n            full_command: \"/add-dir\".to_string(),\n            scope: \"default\".to_string(),\n            namespace: None,\n            file_path: \"\".to_string(),\n            content: \"Add additional working directories\".to_string(),\n            description: Some(\"Add additional working directories\".to_string()),\n            allowed_tools: vec![],\n            has_bash_commands: false,\n            has_file_references: false,\n            accepts_arguments: false,\n        },\n        SlashCommand {\n            id: \"default-init\".to_string(),\n            name: \"init\".to_string(),\n            full_command: \"/init\".to_string(),\n            scope: \"default\".to_string(),\n            namespace: None,\n            file_path: \"\".to_string(),\n            content: \"Initialize project with CLAUDE.md guide\".to_string(),\n            description: Some(\"Initialize project with CLAUDE.md guide\".to_string()),\n            allowed_tools: vec![],\n            has_bash_commands: false,\n            has_file_references: false,\n            accepts_arguments: false,\n        },\n        SlashCommand {\n            id: \"default-review\".to_string(),\n            name: \"review\".to_string(),\n            full_command: \"/review\".to_string(),\n            scope: \"default\".to_string(),\n            namespace: None,\n            file_path: \"\".to_string(),\n            content: \"Request code review\".to_string(),\n            description: Some(\"Request code review\".to_string()),\n            allowed_tools: vec![],\n            has_bash_commands: false,\n            has_file_references: false,\n            accepts_arguments: false,\n        },\n    ]\n}\n\n/// Discover all custom slash commands\n#[tauri::command]\npub async fn slash_commands_list(\n    project_path: Option<String>,\n) -> Result<Vec<SlashCommand>, String> {\n    info!(\"Discovering slash commands\");\n    let mut commands = Vec::new();\n\n    // Add default commands\n    commands.extend(create_default_commands());\n\n    // Load project commands if project path is provided\n    if let Some(proj_path) = project_path {\n        let project_commands_dir = PathBuf::from(&proj_path).join(\".claude\").join(\"commands\");\n        if project_commands_dir.exists() {\n            debug!(\"Scanning project commands at: {:?}\", project_commands_dir);\n\n            let mut md_files = Vec::new();\n            if let Err(e) = find_markdown_files(&project_commands_dir, &mut md_files) {\n                error!(\"Failed to find project command files: {}\", e);\n            } else {\n                for file_path in md_files {\n                    match load_command_from_file(&file_path, &project_commands_dir, \"project\") {\n                        Ok(cmd) => {\n                            debug!(\"Loaded project command: {}\", cmd.full_command);\n                            commands.push(cmd);\n                        }\n                        Err(e) => {\n                            error!(\"Failed to load command from {:?}: {}\", file_path, e);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // Load user commands\n    if let Some(home_dir) = dirs::home_dir() {\n        let user_commands_dir = home_dir.join(\".claude\").join(\"commands\");\n        if user_commands_dir.exists() {\n            debug!(\"Scanning user commands at: {:?}\", user_commands_dir);\n\n            let mut md_files = Vec::new();\n            if let Err(e) = find_markdown_files(&user_commands_dir, &mut md_files) {\n                error!(\"Failed to find user command files: {}\", e);\n            } else {\n                for file_path in md_files {\n                    match load_command_from_file(&file_path, &user_commands_dir, \"user\") {\n                        Ok(cmd) => {\n                            debug!(\"Loaded user command: {}\", cmd.full_command);\n                            commands.push(cmd);\n                        }\n                        Err(e) => {\n                            error!(\"Failed to load command from {:?}: {}\", file_path, e);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    info!(\"Found {} slash commands\", commands.len());\n    Ok(commands)\n}\n\n/// Get a single slash command by ID\n#[tauri::command]\npub async fn slash_command_get(command_id: String) -> Result<SlashCommand, String> {\n    debug!(\"Getting slash command: {}\", command_id);\n\n    // Parse the ID to determine scope and reconstruct file path\n    let parts: Vec<&str> = command_id.split('-').collect();\n    if parts.len() < 2 {\n        return Err(\"Invalid command ID\".to_string());\n    }\n\n    // The actual implementation would need to reconstruct the path and reload the command\n    // For now, we'll list all commands and find the matching one\n    let commands = slash_commands_list(None).await?;\n\n    commands\n        .into_iter()\n        .find(|cmd| cmd.id == command_id)\n        .ok_or_else(|| format!(\"Command not found: {}\", command_id))\n}\n\n/// Create or update a slash command\n#[tauri::command]\npub async fn slash_command_save(\n    scope: String,\n    name: String,\n    namespace: Option<String>,\n    content: String,\n    description: Option<String>,\n    allowed_tools: Vec<String>,\n    project_path: Option<String>,\n) -> Result<SlashCommand, String> {\n    info!(\"Saving slash command: {} in scope: {}\", name, scope);\n\n    // Validate inputs\n    if name.is_empty() {\n        return Err(\"Command name cannot be empty\".to_string());\n    }\n\n    if ![\"project\", \"user\"].contains(&scope.as_str()) {\n        return Err(\"Invalid scope. Must be 'project' or 'user'\".to_string());\n    }\n\n    // Determine base directory\n    let base_dir = if scope == \"project\" {\n        if let Some(proj_path) = project_path {\n            PathBuf::from(proj_path).join(\".claude\").join(\"commands\")\n        } else {\n            return Err(\"Project path required for project scope\".to_string());\n        }\n    } else {\n        dirs::home_dir()\n            .ok_or_else(|| \"Could not find home directory\".to_string())?\n            .join(\".claude\")\n            .join(\"commands\")\n    };\n\n    // Build file path\n    let mut file_path = base_dir.clone();\n    if let Some(ns) = &namespace {\n        for component in ns.split(':') {\n            file_path = file_path.join(component);\n        }\n    }\n\n    // Create directories if needed\n    fs::create_dir_all(&file_path).map_err(|e| format!(\"Failed to create directories: {}\", e))?;\n\n    // Add filename\n    file_path = file_path.join(format!(\"{}.md\", name));\n\n    // Build content with frontmatter\n    let mut full_content = String::new();\n\n    // Add frontmatter if we have metadata\n    if description.is_some() || !allowed_tools.is_empty() {\n        full_content.push_str(\"---\\n\");\n\n        if let Some(desc) = &description {\n            full_content.push_str(&format!(\"description: {}\\n\", desc));\n        }\n\n        if !allowed_tools.is_empty() {\n            full_content.push_str(\"allowed-tools:\\n\");\n            for tool in &allowed_tools {\n                full_content.push_str(&format!(\"  - {}\\n\", tool));\n            }\n        }\n\n        full_content.push_str(\"---\\n\\n\");\n    }\n\n    full_content.push_str(&content);\n\n    // Write file\n    fs::write(&file_path, &full_content)\n        .map_err(|e| format!(\"Failed to write command file: {}\", e))?;\n\n    // Load and return the saved command\n    load_command_from_file(&file_path, &base_dir, &scope)\n        .map_err(|e| format!(\"Failed to load saved command: {}\", e))\n}\n\n/// Delete a slash command\n#[tauri::command]\npub async fn slash_command_delete(\n    command_id: String,\n    project_path: Option<String>,\n) -> Result<String, String> {\n    info!(\"Deleting slash command: {}\", command_id);\n\n    // First, we need to determine if this is a project command by parsing the ID\n    let is_project_command = command_id.starts_with(\"project-\");\n\n    // If it's a project command and we don't have a project path, error out\n    if is_project_command && project_path.is_none() {\n        return Err(\"Project path required to delete project commands\".to_string());\n    }\n\n    // List all commands (including project commands if applicable)\n    let commands = slash_commands_list(project_path).await?;\n\n    // Find the command by ID\n    let command = commands\n        .into_iter()\n        .find(|cmd| cmd.id == command_id)\n        .ok_or_else(|| format!(\"Command not found: {}\", command_id))?;\n\n    // Delete the file\n    fs::remove_file(&command.file_path)\n        .map_err(|e| format!(\"Failed to delete command file: {}\", e))?;\n\n    // Clean up empty directories\n    if let Some(parent) = Path::new(&command.file_path).parent() {\n        let _ = remove_empty_dirs(parent);\n    }\n\n    Ok(format!(\"Deleted command: {}\", command.full_command))\n}\n\n/// Remove empty directories recursively\nfn remove_empty_dirs(dir: &Path) -> Result<()> {\n    if !dir.exists() {\n        return Ok(());\n    }\n\n    // Check if directory is empty\n    let is_empty = fs::read_dir(dir)?.next().is_none();\n\n    if is_empty {\n        fs::remove_dir(dir)?;\n\n        // Try to remove parent if it's also empty\n        if let Some(parent) = dir.parent() {\n            let _ = remove_empty_dirs(parent);\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "src-tauri/src/commands/storage.rs",
    "content": "use super::agents::AgentDb;\nuse anyhow::Result;\nuse rusqlite::{params, types::ValueRef, Connection, Result as SqliteResult};\nuse serde::{Deserialize, Serialize};\nuse serde_json::{Map, Value as JsonValue};\nuse std::collections::HashMap;\nuse tauri::{AppHandle, Manager, State};\n\n/// Represents metadata about a database table\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct TableInfo {\n    pub name: String,\n    pub row_count: i64,\n    pub columns: Vec<ColumnInfo>,\n}\n\n/// Represents metadata about a table column\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct ColumnInfo {\n    pub cid: i32,\n    pub name: String,\n    pub type_name: String,\n    pub notnull: bool,\n    pub dflt_value: Option<String>,\n    pub pk: bool,\n}\n\n/// Represents a page of table data\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct TableData {\n    pub table_name: String,\n    pub columns: Vec<ColumnInfo>,\n    pub rows: Vec<Map<String, JsonValue>>,\n    pub total_rows: i64,\n    pub page: i64,\n    pub page_size: i64,\n    pub total_pages: i64,\n}\n\n/// SQL query result\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct QueryResult {\n    pub columns: Vec<String>,\n    pub rows: Vec<Vec<JsonValue>>,\n    pub rows_affected: Option<i64>,\n    pub last_insert_rowid: Option<i64>,\n}\n\n/// List all tables in the database\n#[tauri::command]\npub async fn storage_list_tables(db: State<'_, AgentDb>) -> Result<Vec<TableInfo>, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Query for all tables\n    let mut stmt = conn\n        .prepare(\"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name\")\n        .map_err(|e| e.to_string())?;\n\n    let table_names: Vec<String> = stmt\n        .query_map([], |row| row.get(0))\n        .map_err(|e| e.to_string())?\n        .collect::<SqliteResult<Vec<_>>>()\n        .map_err(|e| e.to_string())?;\n\n    drop(stmt);\n\n    let mut tables = Vec::new();\n\n    for table_name in table_names {\n        // Get row count\n        let row_count: i64 = conn\n            .query_row(&format!(\"SELECT COUNT(*) FROM {}\", table_name), [], |row| {\n                row.get(0)\n            })\n            .unwrap_or(0);\n\n        // Get column information\n        let mut pragma_stmt = conn\n            .prepare(&format!(\"PRAGMA table_info({})\", table_name))\n            .map_err(|e| e.to_string())?;\n\n        let columns: Vec<ColumnInfo> = pragma_stmt\n            .query_map([], |row| {\n                Ok(ColumnInfo {\n                    cid: row.get(0)?,\n                    name: row.get(1)?,\n                    type_name: row.get(2)?,\n                    notnull: row.get::<_, i32>(3)? != 0,\n                    dflt_value: row.get(4)?,\n                    pk: row.get::<_, i32>(5)? != 0,\n                })\n            })\n            .map_err(|e| e.to_string())?\n            .collect::<SqliteResult<Vec<_>>>()\n            .map_err(|e| e.to_string())?;\n\n        tables.push(TableInfo {\n            name: table_name,\n            row_count,\n            columns,\n        });\n    }\n\n    Ok(tables)\n}\n\n/// Read table data with pagination\n#[tauri::command]\n#[allow(non_snake_case)]\npub async fn storage_read_table(\n    db: State<'_, AgentDb>,\n    tableName: String,\n    page: i64,\n    pageSize: i64,\n    searchQuery: Option<String>,\n) -> Result<TableData, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Validate table name to prevent SQL injection\n    if !is_valid_table_name(&conn, &tableName)? {\n        return Err(\"Invalid table name\".to_string());\n    }\n\n    // Get column information\n    let mut pragma_stmt = conn\n        .prepare(&format!(\"PRAGMA table_info({})\", tableName))\n        .map_err(|e| e.to_string())?;\n\n    let columns: Vec<ColumnInfo> = pragma_stmt\n        .query_map([], |row| {\n            Ok(ColumnInfo {\n                cid: row.get(0)?,\n                name: row.get(1)?,\n                type_name: row.get(2)?,\n                notnull: row.get::<_, i32>(3)? != 0,\n                dflt_value: row.get(4)?,\n                pk: row.get::<_, i32>(5)? != 0,\n            })\n        })\n        .map_err(|e| e.to_string())?\n        .collect::<SqliteResult<Vec<_>>>()\n        .map_err(|e| e.to_string())?;\n\n    drop(pragma_stmt);\n\n    // Build query with optional search\n    let (query, count_query) = if let Some(search) = &searchQuery {\n        // Create search conditions for all text columns\n        let search_conditions: Vec<String> = columns\n            .iter()\n            .filter(|col| col.type_name.contains(\"TEXT\") || col.type_name.contains(\"VARCHAR\"))\n            .map(|col| format!(\"{} LIKE '%{}%'\", col.name, search.replace(\"'\", \"''\")))\n            .collect();\n\n        if search_conditions.is_empty() {\n            (\n                format!(\"SELECT * FROM {} LIMIT ? OFFSET ?\", tableName),\n                format!(\"SELECT COUNT(*) FROM {}\", tableName),\n            )\n        } else {\n            let where_clause = search_conditions.join(\" OR \");\n            (\n                format!(\n                    \"SELECT * FROM {} WHERE {} LIMIT ? OFFSET ?\",\n                    tableName, where_clause\n                ),\n                format!(\"SELECT COUNT(*) FROM {} WHERE {}\", tableName, where_clause),\n            )\n        }\n    } else {\n        (\n            format!(\"SELECT * FROM {} LIMIT ? OFFSET ?\", tableName),\n            format!(\"SELECT COUNT(*) FROM {}\", tableName),\n        )\n    };\n\n    // Get total row count\n    let total_rows: i64 = conn\n        .query_row(&count_query, [], |row| row.get(0))\n        .unwrap_or(0);\n\n    // Calculate pagination\n    let offset = (page - 1) * pageSize;\n    let total_pages = (total_rows as f64 / pageSize as f64).ceil() as i64;\n\n    // Query data\n    let mut data_stmt = conn.prepare(&query).map_err(|e| e.to_string())?;\n\n    let rows: Vec<Map<String, JsonValue>> = data_stmt\n        .query_map(params![pageSize, offset], |row| {\n            let mut row_map = Map::new();\n\n            for (idx, col) in columns.iter().enumerate() {\n                let value = match row.get_ref(idx)? {\n                    ValueRef::Null => JsonValue::Null,\n                    ValueRef::Integer(i) => JsonValue::Number(serde_json::Number::from(i)),\n                    ValueRef::Real(f) => {\n                        if let Some(n) = serde_json::Number::from_f64(f) {\n                            JsonValue::Number(n)\n                        } else {\n                            JsonValue::String(f.to_string())\n                        }\n                    }\n                    ValueRef::Text(s) => JsonValue::String(String::from_utf8_lossy(s).to_string()),\n                    ValueRef::Blob(b) => JsonValue::String(base64::Engine::encode(\n                        &base64::engine::general_purpose::STANDARD,\n                        b,\n                    )),\n                };\n                row_map.insert(col.name.clone(), value);\n            }\n\n            Ok(row_map)\n        })\n        .map_err(|e| e.to_string())?\n        .collect::<SqliteResult<Vec<_>>>()\n        .map_err(|e| e.to_string())?;\n\n    Ok(TableData {\n        table_name: tableName,\n        columns,\n        rows,\n        total_rows,\n        page,\n        page_size: pageSize,\n        total_pages,\n    })\n}\n\n/// Update a row in a table\n#[tauri::command]\n#[allow(non_snake_case)]\npub async fn storage_update_row(\n    db: State<'_, AgentDb>,\n    tableName: String,\n    primaryKeyValues: HashMap<String, JsonValue>,\n    updates: HashMap<String, JsonValue>,\n) -> Result<(), String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Validate table name\n    if !is_valid_table_name(&conn, &tableName)? {\n        return Err(\"Invalid table name\".to_string());\n    }\n\n    // Build UPDATE query\n    let set_clauses: Vec<String> = updates\n        .keys()\n        .enumerate()\n        .map(|(idx, key)| format!(\"{} = ?{}\", key, idx + 1))\n        .collect();\n\n    let where_clauses: Vec<String> = primaryKeyValues\n        .keys()\n        .enumerate()\n        .map(|(idx, key)| format!(\"{} = ?{}\", key, idx + updates.len() + 1))\n        .collect();\n\n    let query = format!(\n        \"UPDATE {} SET {} WHERE {}\",\n        tableName,\n        set_clauses.join(\", \"),\n        where_clauses.join(\" AND \")\n    );\n\n    // Prepare parameters\n    let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();\n\n    // Add update values\n    for value in updates.values() {\n        params.push(json_to_sql_value(value)?);\n    }\n\n    // Add where clause values\n    for value in primaryKeyValues.values() {\n        params.push(json_to_sql_value(value)?);\n    }\n\n    // Execute update\n    conn.execute(\n        &query,\n        rusqlite::params_from_iter(params.iter().map(|p| p.as_ref())),\n    )\n    .map_err(|e| format!(\"Failed to update row: {}\", e))?;\n\n    Ok(())\n}\n\n/// Delete a row from a table\n#[tauri::command]\n#[allow(non_snake_case)]\npub async fn storage_delete_row(\n    db: State<'_, AgentDb>,\n    tableName: String,\n    primaryKeyValues: HashMap<String, JsonValue>,\n) -> Result<(), String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Validate table name\n    if !is_valid_table_name(&conn, &tableName)? {\n        return Err(\"Invalid table name\".to_string());\n    }\n\n    // Build DELETE query\n    let where_clauses: Vec<String> = primaryKeyValues\n        .keys()\n        .enumerate()\n        .map(|(idx, key)| format!(\"{} = ?{}\", key, idx + 1))\n        .collect();\n\n    let query = format!(\n        \"DELETE FROM {} WHERE {}\",\n        tableName,\n        where_clauses.join(\" AND \")\n    );\n\n    // Prepare parameters\n    let params: Vec<Box<dyn rusqlite::ToSql>> = primaryKeyValues\n        .values()\n        .map(json_to_sql_value)\n        .collect::<Result<Vec<_>, _>>()?;\n\n    // Execute delete\n    conn.execute(\n        &query,\n        rusqlite::params_from_iter(params.iter().map(|p| p.as_ref())),\n    )\n    .map_err(|e| format!(\"Failed to delete row: {}\", e))?;\n\n    Ok(())\n}\n\n/// Insert a new row into a table\n#[tauri::command]\n#[allow(non_snake_case)]\npub async fn storage_insert_row(\n    db: State<'_, AgentDb>,\n    tableName: String,\n    values: HashMap<String, JsonValue>,\n) -> Result<i64, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Validate table name\n    if !is_valid_table_name(&conn, &tableName)? {\n        return Err(\"Invalid table name\".to_string());\n    }\n\n    // Build INSERT query\n    let columns: Vec<&String> = values.keys().collect();\n    let placeholders: Vec<String> = (1..=columns.len()).map(|i| format!(\"?{}\", i)).collect();\n\n    let query = format!(\n        \"INSERT INTO {} ({}) VALUES ({})\",\n        tableName,\n        columns\n            .iter()\n            .map(|c| c.as_str())\n            .collect::<Vec<_>>()\n            .join(\", \"),\n        placeholders.join(\", \")\n    );\n\n    // Prepare parameters\n    let params: Vec<Box<dyn rusqlite::ToSql>> = values\n        .values()\n        .map(json_to_sql_value)\n        .collect::<Result<Vec<_>, _>>()?;\n\n    // Execute insert\n    conn.execute(\n        &query,\n        rusqlite::params_from_iter(params.iter().map(|p| p.as_ref())),\n    )\n    .map_err(|e| format!(\"Failed to insert row: {}\", e))?;\n\n    Ok(conn.last_insert_rowid())\n}\n\n/// Execute a raw SQL query\n#[tauri::command]\npub async fn storage_execute_sql(\n    db: State<'_, AgentDb>,\n    query: String,\n) -> Result<QueryResult, String> {\n    let conn = db.0.lock().map_err(|e| e.to_string())?;\n\n    // Check if it's a SELECT query\n    let is_select = query.trim().to_uppercase().starts_with(\"SELECT\");\n\n    if is_select {\n        // Handle SELECT queries\n        let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;\n        let column_count = stmt.column_count();\n\n        // Get column names\n        let columns: Vec<String> = (0..column_count)\n            .map(|i| stmt.column_name(i).unwrap_or(\"\").to_string())\n            .collect();\n\n        // Execute query and collect results\n        let rows: Vec<Vec<JsonValue>> = stmt\n            .query_map([], |row| {\n                let mut row_values = Vec::new();\n                for i in 0..column_count {\n                    let value = match row.get_ref(i)? {\n                        ValueRef::Null => JsonValue::Null,\n                        ValueRef::Integer(n) => JsonValue::Number(serde_json::Number::from(n)),\n                        ValueRef::Real(f) => {\n                            if let Some(n) = serde_json::Number::from_f64(f) {\n                                JsonValue::Number(n)\n                            } else {\n                                JsonValue::String(f.to_string())\n                            }\n                        }\n                        ValueRef::Text(s) => {\n                            JsonValue::String(String::from_utf8_lossy(s).to_string())\n                        }\n                        ValueRef::Blob(b) => JsonValue::String(base64::Engine::encode(\n                            &base64::engine::general_purpose::STANDARD,\n                            b,\n                        )),\n                    };\n                    row_values.push(value);\n                }\n                Ok(row_values)\n            })\n            .map_err(|e| e.to_string())?\n            .collect::<SqliteResult<Vec<_>>>()\n            .map_err(|e| e.to_string())?;\n\n        Ok(QueryResult {\n            columns,\n            rows,\n            rows_affected: None,\n            last_insert_rowid: None,\n        })\n    } else {\n        // Handle non-SELECT queries (INSERT, UPDATE, DELETE, etc.)\n        let rows_affected = conn.execute(&query, []).map_err(|e| e.to_string())?;\n\n        Ok(QueryResult {\n            columns: vec![],\n            rows: vec![],\n            rows_affected: Some(rows_affected as i64),\n            last_insert_rowid: Some(conn.last_insert_rowid()),\n        })\n    }\n}\n\n/// Reset the entire database (with confirmation)\n#[tauri::command]\npub async fn storage_reset_database(app: AppHandle) -> Result<(), String> {\n    {\n        // Drop all existing tables within a scoped block\n        let db_state = app.state::<AgentDb>();\n        let conn = db_state.0.lock().map_err(|e| e.to_string())?;\n\n        // Disable foreign key constraints temporarily to allow dropping tables\n        conn.execute(\"PRAGMA foreign_keys = OFF\", [])\n            .map_err(|e| format!(\"Failed to disable foreign keys: {}\", e))?;\n\n        // Drop tables - order doesn't matter with foreign keys disabled\n        conn.execute(\"DROP TABLE IF EXISTS agent_runs\", [])\n            .map_err(|e| format!(\"Failed to drop agent_runs table: {}\", e))?;\n        conn.execute(\"DROP TABLE IF EXISTS agents\", [])\n            .map_err(|e| format!(\"Failed to drop agents table: {}\", e))?;\n        conn.execute(\"DROP TABLE IF EXISTS app_settings\", [])\n            .map_err(|e| format!(\"Failed to drop app_settings table: {}\", e))?;\n\n        // Re-enable foreign key constraints\n        conn.execute(\"PRAGMA foreign_keys = ON\", [])\n            .map_err(|e| format!(\"Failed to re-enable foreign keys: {}\", e))?;\n\n        // Connection is automatically dropped at end of scope\n    }\n\n    // Re-initialize the database which will recreate all tables empty\n    let new_conn = init_database(&app).map_err(|e| format!(\"Failed to reset database: {}\", e))?;\n\n    // Update the managed state with the new connection\n    {\n        let db_state = app.state::<AgentDb>();\n        let mut conn_guard = db_state.0.lock().map_err(|e| e.to_string())?;\n        *conn_guard = new_conn;\n    }\n\n    // Run VACUUM to optimize the database\n    {\n        let db_state = app.state::<AgentDb>();\n        let conn = db_state.0.lock().map_err(|e| e.to_string())?;\n        conn.execute(\"VACUUM\", []).map_err(|e| e.to_string())?;\n    }\n\n    Ok(())\n}\n\n/// Helper function to validate table name exists\nfn is_valid_table_name(conn: &Connection, table_name: &str) -> Result<bool, String> {\n    let count: i64 = conn\n        .query_row(\n            \"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?\",\n            params![table_name],\n            |row| row.get(0),\n        )\n        .map_err(|e| e.to_string())?;\n\n    Ok(count > 0)\n}\n\n/// Helper function to convert JSON value to SQL value\nfn json_to_sql_value(value: &JsonValue) -> Result<Box<dyn rusqlite::ToSql>, String> {\n    match value {\n        JsonValue::Null => Ok(Box::new(rusqlite::types::Null)),\n        JsonValue::Bool(b) => Ok(Box::new(*b)),\n        JsonValue::Number(n) => {\n            if let Some(i) = n.as_i64() {\n                Ok(Box::new(i))\n            } else if let Some(f) = n.as_f64() {\n                Ok(Box::new(f))\n            } else {\n                Err(\"Invalid number value\".to_string())\n            }\n        }\n        JsonValue::String(s) => Ok(Box::new(s.clone())),\n        _ => Err(\"Unsupported value type\".to_string()),\n    }\n}\n\n/// Initialize the agents database (re-exported from agents module)\nuse super::agents::init_database;\n"
  },
  {
    "path": "src-tauri/src/commands/usage.rs",
    "content": "use chrono::{DateTime, Local, NaiveDate};\nuse serde::{Deserialize, Serialize};\nuse serde_json;\nuse std::collections::{HashMap, HashSet};\nuse std::fs;\nuse std::path::PathBuf;\nuse tauri::command;\n\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct UsageEntry {\n    timestamp: String,\n    model: String,\n    input_tokens: u64,\n    output_tokens: u64,\n    cache_creation_tokens: u64,\n    cache_read_tokens: u64,\n    cost: f64,\n    session_id: String,\n    project_path: String,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct UsageStats {\n    total_cost: f64,\n    total_tokens: u64,\n    total_input_tokens: u64,\n    total_output_tokens: u64,\n    total_cache_creation_tokens: u64,\n    total_cache_read_tokens: u64,\n    total_sessions: u64,\n    by_model: Vec<ModelUsage>,\n    by_date: Vec<DailyUsage>,\n    by_project: Vec<ProjectUsage>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct ModelUsage {\n    model: String,\n    total_cost: f64,\n    total_tokens: u64,\n    input_tokens: u64,\n    output_tokens: u64,\n    cache_creation_tokens: u64,\n    cache_read_tokens: u64,\n    session_count: u64,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct DailyUsage {\n    date: String,\n    total_cost: f64,\n    total_tokens: u64,\n    models_used: Vec<String>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct ProjectUsage {\n    project_path: String,\n    project_name: String,\n    total_cost: f64,\n    total_tokens: u64,\n    session_count: u64,\n    last_used: String,\n}\n\n// Claude 4 pricing constants (per million tokens)\nconst OPUS_4_INPUT_PRICE: f64 = 15.0;\nconst OPUS_4_OUTPUT_PRICE: f64 = 75.0;\nconst OPUS_4_CACHE_WRITE_PRICE: f64 = 18.75;\nconst OPUS_4_CACHE_READ_PRICE: f64 = 1.50;\n\nconst SONNET_4_INPUT_PRICE: f64 = 3.0;\nconst SONNET_4_OUTPUT_PRICE: f64 = 15.0;\nconst SONNET_4_CACHE_WRITE_PRICE: f64 = 3.75;\nconst SONNET_4_CACHE_READ_PRICE: f64 = 0.30;\n\n#[derive(Debug, Deserialize)]\nstruct JsonlEntry {\n    timestamp: String,\n    message: Option<MessageData>,\n    #[serde(rename = \"sessionId\")]\n    session_id: Option<String>,\n    #[serde(rename = \"requestId\")]\n    request_id: Option<String>,\n    #[serde(rename = \"costUSD\")]\n    cost_usd: Option<f64>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct MessageData {\n    id: Option<String>,\n    model: Option<String>,\n    usage: Option<UsageData>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct UsageData {\n    input_tokens: Option<u64>,\n    output_tokens: Option<u64>,\n    cache_creation_input_tokens: Option<u64>,\n    cache_read_input_tokens: Option<u64>,\n}\n\nfn calculate_cost(model: &str, usage: &UsageData) -> f64 {\n    let input_tokens = usage.input_tokens.unwrap_or(0) as f64;\n    let output_tokens = usage.output_tokens.unwrap_or(0) as f64;\n    let cache_creation_tokens = usage.cache_creation_input_tokens.unwrap_or(0) as f64;\n    let cache_read_tokens = usage.cache_read_input_tokens.unwrap_or(0) as f64;\n\n    // Calculate cost based on model\n    let (input_price, output_price, cache_write_price, cache_read_price) =\n        if model.contains(\"opus-4\") || model.contains(\"claude-opus-4\") {\n            (\n                OPUS_4_INPUT_PRICE,\n                OPUS_4_OUTPUT_PRICE,\n                OPUS_4_CACHE_WRITE_PRICE,\n                OPUS_4_CACHE_READ_PRICE,\n            )\n        } else if model.contains(\"sonnet-4\") || model.contains(\"claude-sonnet-4\") {\n            (\n                SONNET_4_INPUT_PRICE,\n                SONNET_4_OUTPUT_PRICE,\n                SONNET_4_CACHE_WRITE_PRICE,\n                SONNET_4_CACHE_READ_PRICE,\n            )\n        } else {\n            // Return 0 for unknown models to avoid incorrect cost estimations.\n            (0.0, 0.0, 0.0, 0.0)\n        };\n\n    // Calculate cost (prices are per million tokens)\n    let cost = (input_tokens * input_price / 1_000_000.0)\n        + (output_tokens * output_price / 1_000_000.0)\n        + (cache_creation_tokens * cache_write_price / 1_000_000.0)\n        + (cache_read_tokens * cache_read_price / 1_000_000.0);\n\n    cost\n}\n\nfn parse_jsonl_file(\n    path: &PathBuf,\n    encoded_project_name: &str,\n    processed_hashes: &mut HashSet<String>,\n) -> Vec<UsageEntry> {\n    let mut entries = Vec::new();\n    let mut actual_project_path: Option<String> = None;\n\n    if let Ok(content) = fs::read_to_string(path) {\n        // Extract session ID from the file path\n        let session_id = path\n            .parent()\n            .and_then(|p| p.file_name())\n            .and_then(|n| n.to_str())\n            .unwrap_or(\"unknown\")\n            .to_string();\n\n        for line in content.lines() {\n            if line.trim().is_empty() {\n                continue;\n            }\n\n            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line) {\n                // Extract the actual project path from cwd if we haven't already\n                if actual_project_path.is_none() {\n                    if let Some(cwd) = json_value.get(\"cwd\").and_then(|v| v.as_str()) {\n                        actual_project_path = Some(cwd.to_string());\n                    }\n                }\n\n                // Try to parse as JsonlEntry for usage data\n                if let Ok(entry) = serde_json::from_value::<JsonlEntry>(json_value) {\n                    if let Some(message) = &entry.message {\n                        // Deduplication based on message ID and request ID\n                        if let (Some(msg_id), Some(req_id)) = (&message.id, &entry.request_id) {\n                            let unique_hash = format!(\"{}:{}\", msg_id, req_id);\n                            if processed_hashes.contains(&unique_hash) {\n                                continue; // Skip duplicate entry\n                            }\n                            processed_hashes.insert(unique_hash);\n                        }\n\n                        if let Some(usage) = &message.usage {\n                            // Skip entries without meaningful token usage\n                            if usage.input_tokens.unwrap_or(0) == 0\n                                && usage.output_tokens.unwrap_or(0) == 0\n                                && usage.cache_creation_input_tokens.unwrap_or(0) == 0\n                                && usage.cache_read_input_tokens.unwrap_or(0) == 0\n                            {\n                                continue;\n                            }\n\n                            let cost = entry.cost_usd.unwrap_or_else(|| {\n                                if let Some(model_str) = &message.model {\n                                    calculate_cost(model_str, usage)\n                                } else {\n                                    0.0\n                                }\n                            });\n\n                            // Use actual project path if found, otherwise use encoded name\n                            let project_path = actual_project_path\n                                .clone()\n                                .unwrap_or_else(|| encoded_project_name.to_string());\n\n                            entries.push(UsageEntry {\n                                timestamp: entry.timestamp,\n                                model: message\n                                    .model\n                                    .clone()\n                                    .unwrap_or_else(|| \"unknown\".to_string()),\n                                input_tokens: usage.input_tokens.unwrap_or(0),\n                                output_tokens: usage.output_tokens.unwrap_or(0),\n                                cache_creation_tokens: usage\n                                    .cache_creation_input_tokens\n                                    .unwrap_or(0),\n                                cache_read_tokens: usage.cache_read_input_tokens.unwrap_or(0),\n                                cost,\n                                session_id: entry.session_id.unwrap_or_else(|| session_id.clone()),\n                                project_path,\n                            });\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    entries\n}\n\nfn get_earliest_timestamp(path: &PathBuf) -> Option<String> {\n    if let Ok(content) = fs::read_to_string(path) {\n        let mut earliest_timestamp: Option<String> = None;\n        for line in content.lines() {\n            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line) {\n                if let Some(timestamp_str) = json_value.get(\"timestamp\").and_then(|v| v.as_str()) {\n                    if let Some(current_earliest) = &earliest_timestamp {\n                        if timestamp_str < current_earliest.as_str() {\n                            earliest_timestamp = Some(timestamp_str.to_string());\n                        }\n                    } else {\n                        earliest_timestamp = Some(timestamp_str.to_string());\n                    }\n                }\n            }\n        }\n        return earliest_timestamp;\n    }\n    None\n}\n\nfn get_all_usage_entries(claude_path: &PathBuf) -> Vec<UsageEntry> {\n    let mut all_entries = Vec::new();\n    let mut processed_hashes = HashSet::new();\n    let projects_dir = claude_path.join(\"projects\");\n\n    let mut files_to_process: Vec<(PathBuf, String)> = Vec::new();\n\n    if let Ok(projects) = fs::read_dir(&projects_dir) {\n        for project in projects.flatten() {\n            if project.file_type().map(|t| t.is_dir()).unwrap_or(false) {\n                let project_name = project.file_name().to_string_lossy().to_string();\n                let project_path = project.path();\n\n                walkdir::WalkDir::new(&project_path)\n                    .into_iter()\n                    .filter_map(Result::ok)\n                    .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some(\"jsonl\"))\n                    .for_each(|entry| {\n                        files_to_process.push((entry.path().to_path_buf(), project_name.clone()));\n                    });\n            }\n        }\n    }\n\n    // Sort files by their earliest timestamp to ensure chronological processing\n    // and deterministic deduplication.\n    files_to_process.sort_by_cached_key(|(path, _)| get_earliest_timestamp(path));\n\n    for (path, project_name) in files_to_process {\n        let entries = parse_jsonl_file(&path, &project_name, &mut processed_hashes);\n        all_entries.extend(entries);\n    }\n\n    // Sort by timestamp\n    all_entries.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));\n\n    all_entries\n}\n\n#[command]\npub fn get_usage_stats(days: Option<u32>) -> Result<UsageStats, String> {\n    let claude_path = dirs::home_dir()\n        .ok_or(\"Failed to get home directory\")?\n        .join(\".claude\");\n\n    let all_entries = get_all_usage_entries(&claude_path);\n\n    if all_entries.is_empty() {\n        return Ok(UsageStats {\n            total_cost: 0.0,\n            total_tokens: 0,\n            total_input_tokens: 0,\n            total_output_tokens: 0,\n            total_cache_creation_tokens: 0,\n            total_cache_read_tokens: 0,\n            total_sessions: 0,\n            by_model: vec![],\n            by_date: vec![],\n            by_project: vec![],\n        });\n    }\n\n    // Filter by days if specified\n    let filtered_entries = if let Some(days) = days {\n        let cutoff = Local::now().naive_local().date() - chrono::Duration::days(days as i64);\n        all_entries\n            .into_iter()\n            .filter(|e| {\n                if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {\n                    dt.naive_local().date() >= cutoff\n                } else {\n                    false\n                }\n            })\n            .collect()\n    } else {\n        all_entries\n    };\n\n    // Calculate aggregated stats\n    let mut total_cost = 0.0;\n    let mut total_input_tokens = 0u64;\n    let mut total_output_tokens = 0u64;\n    let mut total_cache_creation_tokens = 0u64;\n    let mut total_cache_read_tokens = 0u64;\n\n    let mut model_stats: HashMap<String, ModelUsage> = HashMap::new();\n    let mut daily_stats: HashMap<String, DailyUsage> = HashMap::new();\n    let mut project_stats: HashMap<String, ProjectUsage> = HashMap::new();\n\n    for entry in &filtered_entries {\n        // Update totals\n        total_cost += entry.cost;\n        total_input_tokens += entry.input_tokens;\n        total_output_tokens += entry.output_tokens;\n        total_cache_creation_tokens += entry.cache_creation_tokens;\n        total_cache_read_tokens += entry.cache_read_tokens;\n\n        // Update model stats\n        let model_stat = model_stats\n            .entry(entry.model.clone())\n            .or_insert(ModelUsage {\n                model: entry.model.clone(),\n                total_cost: 0.0,\n                total_tokens: 0,\n                input_tokens: 0,\n                output_tokens: 0,\n                cache_creation_tokens: 0,\n                cache_read_tokens: 0,\n                session_count: 0,\n            });\n        model_stat.total_cost += entry.cost;\n        model_stat.input_tokens += entry.input_tokens;\n        model_stat.output_tokens += entry.output_tokens;\n        model_stat.cache_creation_tokens += entry.cache_creation_tokens;\n        model_stat.cache_read_tokens += entry.cache_read_tokens;\n        model_stat.total_tokens = model_stat.input_tokens + model_stat.output_tokens;\n        model_stat.session_count += 1;\n\n        // Update daily stats\n        let date = entry\n            .timestamp\n            .split('T')\n            .next()\n            .unwrap_or(&entry.timestamp)\n            .to_string();\n        let daily_stat = daily_stats.entry(date.clone()).or_insert(DailyUsage {\n            date,\n            total_cost: 0.0,\n            total_tokens: 0,\n            models_used: vec![],\n        });\n        daily_stat.total_cost += entry.cost;\n        daily_stat.total_tokens += entry.input_tokens\n            + entry.output_tokens\n            + entry.cache_creation_tokens\n            + entry.cache_read_tokens;\n        if !daily_stat.models_used.contains(&entry.model) {\n            daily_stat.models_used.push(entry.model.clone());\n        }\n\n        // Update project stats\n        let project_stat =\n            project_stats\n                .entry(entry.project_path.clone())\n                .or_insert(ProjectUsage {\n                    project_path: entry.project_path.clone(),\n                    project_name: entry\n                        .project_path\n                        .split('/')\n                        .last()\n                        .unwrap_or(&entry.project_path)\n                        .to_string(),\n                    total_cost: 0.0,\n                    total_tokens: 0,\n                    session_count: 0,\n                    last_used: entry.timestamp.clone(),\n                });\n        project_stat.total_cost += entry.cost;\n        project_stat.total_tokens += entry.input_tokens\n            + entry.output_tokens\n            + entry.cache_creation_tokens\n            + entry.cache_read_tokens;\n        project_stat.session_count += 1;\n        if entry.timestamp > project_stat.last_used {\n            project_stat.last_used = entry.timestamp.clone();\n        }\n    }\n\n    let total_tokens = total_input_tokens\n        + total_output_tokens\n        + total_cache_creation_tokens\n        + total_cache_read_tokens;\n    let total_sessions = filtered_entries.len() as u64;\n\n    // Convert hashmaps to sorted vectors\n    let mut by_model: Vec<ModelUsage> = model_stats.into_values().collect();\n    by_model.sort_by(|a, b| b.total_cost.partial_cmp(&a.total_cost).unwrap());\n\n    let mut by_date: Vec<DailyUsage> = daily_stats.into_values().collect();\n    by_date.sort_by(|a, b| b.date.cmp(&a.date));\n\n    let mut by_project: Vec<ProjectUsage> = project_stats.into_values().collect();\n    by_project.sort_by(|a, b| b.total_cost.partial_cmp(&a.total_cost).unwrap());\n\n    Ok(UsageStats {\n        total_cost,\n        total_tokens,\n        total_input_tokens,\n        total_output_tokens,\n        total_cache_creation_tokens,\n        total_cache_read_tokens,\n        total_sessions,\n        by_model,\n        by_date,\n        by_project,\n    })\n}\n\n#[command]\npub fn get_usage_by_date_range(start_date: String, end_date: String) -> Result<UsageStats, String> {\n    let claude_path = dirs::home_dir()\n        .ok_or(\"Failed to get home directory\")?\n        .join(\".claude\");\n\n    let all_entries = get_all_usage_entries(&claude_path);\n\n    // Parse dates\n    let start = NaiveDate::parse_from_str(&start_date, \"%Y-%m-%d\").or_else(|_| {\n        // Try parsing ISO datetime format\n        DateTime::parse_from_rfc3339(&start_date)\n            .map(|dt| dt.naive_local().date())\n            .map_err(|e| format!(\"Invalid start date: {}\", e))\n    })?;\n    let end = NaiveDate::parse_from_str(&end_date, \"%Y-%m-%d\").or_else(|_| {\n        // Try parsing ISO datetime format\n        DateTime::parse_from_rfc3339(&end_date)\n            .map(|dt| dt.naive_local().date())\n            .map_err(|e| format!(\"Invalid end date: {}\", e))\n    })?;\n\n    // Filter entries by date range\n    let filtered_entries: Vec<_> = all_entries\n        .into_iter()\n        .filter(|e| {\n            if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {\n                let date = dt.naive_local().date();\n                date >= start && date <= end\n            } else {\n                false\n            }\n        })\n        .collect();\n\n    if filtered_entries.is_empty() {\n        return Ok(UsageStats {\n            total_cost: 0.0,\n            total_tokens: 0,\n            total_input_tokens: 0,\n            total_output_tokens: 0,\n            total_cache_creation_tokens: 0,\n            total_cache_read_tokens: 0,\n            total_sessions: 0,\n            by_model: vec![],\n            by_date: vec![],\n            by_project: vec![],\n        });\n    }\n\n    // Calculate aggregated stats (same logic as get_usage_stats)\n    let mut total_cost = 0.0;\n    let mut total_input_tokens = 0u64;\n    let mut total_output_tokens = 0u64;\n    let mut total_cache_creation_tokens = 0u64;\n    let mut total_cache_read_tokens = 0u64;\n\n    let mut model_stats: HashMap<String, ModelUsage> = HashMap::new();\n    let mut daily_stats: HashMap<String, DailyUsage> = HashMap::new();\n    let mut project_stats: HashMap<String, ProjectUsage> = HashMap::new();\n\n    for entry in &filtered_entries {\n        // Update totals\n        total_cost += entry.cost;\n        total_input_tokens += entry.input_tokens;\n        total_output_tokens += entry.output_tokens;\n        total_cache_creation_tokens += entry.cache_creation_tokens;\n        total_cache_read_tokens += entry.cache_read_tokens;\n\n        // Update model stats\n        let model_stat = model_stats\n            .entry(entry.model.clone())\n            .or_insert(ModelUsage {\n                model: entry.model.clone(),\n                total_cost: 0.0,\n                total_tokens: 0,\n                input_tokens: 0,\n                output_tokens: 0,\n                cache_creation_tokens: 0,\n                cache_read_tokens: 0,\n                session_count: 0,\n            });\n        model_stat.total_cost += entry.cost;\n        model_stat.input_tokens += entry.input_tokens;\n        model_stat.output_tokens += entry.output_tokens;\n        model_stat.cache_creation_tokens += entry.cache_creation_tokens;\n        model_stat.cache_read_tokens += entry.cache_read_tokens;\n        model_stat.total_tokens = model_stat.input_tokens + model_stat.output_tokens;\n        model_stat.session_count += 1;\n\n        // Update daily stats\n        let date = entry\n            .timestamp\n            .split('T')\n            .next()\n            .unwrap_or(&entry.timestamp)\n            .to_string();\n        let daily_stat = daily_stats.entry(date.clone()).or_insert(DailyUsage {\n            date,\n            total_cost: 0.0,\n            total_tokens: 0,\n            models_used: vec![],\n        });\n        daily_stat.total_cost += entry.cost;\n        daily_stat.total_tokens += entry.input_tokens\n            + entry.output_tokens\n            + entry.cache_creation_tokens\n            + entry.cache_read_tokens;\n        if !daily_stat.models_used.contains(&entry.model) {\n            daily_stat.models_used.push(entry.model.clone());\n        }\n\n        // Update project stats\n        let project_stat =\n            project_stats\n                .entry(entry.project_path.clone())\n                .or_insert(ProjectUsage {\n                    project_path: entry.project_path.clone(),\n                    project_name: entry\n                        .project_path\n                        .split('/')\n                        .last()\n                        .unwrap_or(&entry.project_path)\n                        .to_string(),\n                    total_cost: 0.0,\n                    total_tokens: 0,\n                    session_count: 0,\n                    last_used: entry.timestamp.clone(),\n                });\n        project_stat.total_cost += entry.cost;\n        project_stat.total_tokens += entry.input_tokens\n            + entry.output_tokens\n            + entry.cache_creation_tokens\n            + entry.cache_read_tokens;\n        project_stat.session_count += 1;\n        if entry.timestamp > project_stat.last_used {\n            project_stat.last_used = entry.timestamp.clone();\n        }\n    }\n\n    let total_tokens = total_input_tokens\n        + total_output_tokens\n        + total_cache_creation_tokens\n        + total_cache_read_tokens;\n    let total_sessions = filtered_entries.len() as u64;\n\n    // Convert hashmaps to sorted vectors\n    let mut by_model: Vec<ModelUsage> = model_stats.into_values().collect();\n    by_model.sort_by(|a, b| b.total_cost.partial_cmp(&a.total_cost).unwrap());\n\n    let mut by_date: Vec<DailyUsage> = daily_stats.into_values().collect();\n    by_date.sort_by(|a, b| b.date.cmp(&a.date));\n\n    let mut by_project: Vec<ProjectUsage> = project_stats.into_values().collect();\n    by_project.sort_by(|a, b| b.total_cost.partial_cmp(&a.total_cost).unwrap());\n\n    Ok(UsageStats {\n        total_cost,\n        total_tokens,\n        total_input_tokens,\n        total_output_tokens,\n        total_cache_creation_tokens,\n        total_cache_read_tokens,\n        total_sessions,\n        by_model,\n        by_date,\n        by_project,\n    })\n}\n\n#[command]\npub fn get_usage_details(\n    project_path: Option<String>,\n    date: Option<String>,\n) -> Result<Vec<UsageEntry>, String> {\n    let claude_path = dirs::home_dir()\n        .ok_or(\"Failed to get home directory\")?\n        .join(\".claude\");\n\n    let mut all_entries = get_all_usage_entries(&claude_path);\n\n    // Filter by project if specified\n    if let Some(project) = project_path {\n        all_entries.retain(|e| e.project_path == project);\n    }\n\n    // Filter by date if specified\n    if let Some(date) = date {\n        all_entries.retain(|e| e.timestamp.starts_with(&date));\n    }\n\n    Ok(all_entries)\n}\n\n#[command]\npub fn get_session_stats(\n    since: Option<String>,\n    until: Option<String>,\n    order: Option<String>,\n) -> Result<Vec<ProjectUsage>, String> {\n    let claude_path = dirs::home_dir()\n        .ok_or(\"Failed to get home directory\")?\n        .join(\".claude\");\n\n    let all_entries = get_all_usage_entries(&claude_path);\n\n    let since_date = since.and_then(|s| NaiveDate::parse_from_str(&s, \"%Y%m%d\").ok());\n    let until_date = until.and_then(|s| NaiveDate::parse_from_str(&s, \"%Y%m%d\").ok());\n\n    let filtered_entries: Vec<_> = all_entries\n        .into_iter()\n        .filter(|e| {\n            if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {\n                let date = dt.date_naive();\n                let is_after_since = since_date.map_or(true, |s| date >= s);\n                let is_before_until = until_date.map_or(true, |u| date <= u);\n                is_after_since && is_before_until\n            } else {\n                false\n            }\n        })\n        .collect();\n\n    let mut session_stats: HashMap<String, ProjectUsage> = HashMap::new();\n    for entry in &filtered_entries {\n        let session_key = format!(\"{}/{}\", entry.project_path, entry.session_id);\n        let project_stat = session_stats\n            .entry(session_key)\n            .or_insert_with(|| ProjectUsage {\n                project_path: entry.project_path.clone(),\n                project_name: entry.session_id.clone(), // Using session_id as project_name for session view\n                total_cost: 0.0,\n                total_tokens: 0,\n                session_count: 0, // In this context, this will count entries per session\n                last_used: \" \".to_string(),\n            });\n\n        project_stat.total_cost += entry.cost;\n        project_stat.total_tokens += entry.input_tokens\n            + entry.output_tokens\n            + entry.cache_creation_tokens\n            + entry.cache_read_tokens;\n        project_stat.session_count += 1;\n        if entry.timestamp > project_stat.last_used {\n            project_stat.last_used = entry.timestamp.clone();\n        }\n    }\n\n    let mut by_session: Vec<ProjectUsage> = session_stats.into_values().collect();\n\n    // Sort by last_used date\n    if let Some(order_str) = order {\n        if order_str == \"asc\" {\n            by_session.sort_by(|a, b| a.last_used.cmp(&b.last_used));\n        } else {\n            by_session.sort_by(|a, b| b.last_used.cmp(&a.last_used));\n        }\n    } else {\n        // Default to descending\n        by_session.sort_by(|a, b| b.last_used.cmp(&a.last_used));\n    }\n\n    Ok(by_session)\n}\n"
  },
  {
    "path": "src-tauri/src/lib.rs",
    "content": "// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/\n\n// Declare modules\npub mod checkpoint;\npub mod claude_binary;\npub mod commands;\npub mod process;\npub mod web_server;\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n    tauri::Builder::default()\n        .run(tauri::generate_context!())\n        .expect(\"error while running tauri application\");\n}\n"
  },
  {
    "path": "src-tauri/src/main.rs",
    "content": "// Prevents additional console window on Windows in release, DO NOT REMOVE!!\n#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nmod checkpoint;\nmod claude_binary;\nmod commands;\nmod process;\n\nuse checkpoint::state::CheckpointState;\nuse commands::agents::{\n    cleanup_finished_processes, create_agent, delete_agent, execute_agent, export_agent,\n    export_agent_to_file, fetch_github_agent_content, fetch_github_agents, get_agent,\n    get_agent_run, get_agent_run_with_real_time_metrics, get_claude_binary_path,\n    get_live_session_output, get_session_output, get_session_status, import_agent,\n    import_agent_from_file, import_agent_from_github, init_database, kill_agent_session,\n    list_agent_runs, list_agent_runs_with_metrics, list_agents, list_claude_installations,\n    list_running_sessions, load_agent_session_history, set_claude_binary_path,\n    stream_session_output, update_agent, AgentDb,\n};\nuse commands::claude::{\n    cancel_claude_execution, check_auto_checkpoint, check_claude_version, cleanup_old_checkpoints,\n    clear_checkpoint_manager, continue_claude_code, create_checkpoint, create_project,\n    execute_claude_code, find_claude_md_files, fork_from_checkpoint, get_checkpoint_diff,\n    get_checkpoint_settings, get_checkpoint_state_stats, get_claude_session_output,\n    get_claude_settings, get_home_directory, get_hooks_config, get_project_sessions,\n    get_recently_modified_files, get_session_timeline, get_system_prompt, list_checkpoints,\n    list_directory_contents, list_projects, list_running_claude_sessions, load_session_history,\n    open_new_session, read_claude_md_file, restore_checkpoint, resume_claude_code,\n    save_claude_md_file, save_claude_settings, save_system_prompt, search_files,\n    track_checkpoint_message, track_session_messages, update_checkpoint_settings,\n    update_hooks_config, validate_hook_command, ClaudeProcessState,\n};\nuse commands::mcp::{\n    mcp_add, mcp_add_from_claude_desktop, mcp_add_json, mcp_get, mcp_get_server_status, mcp_list,\n    mcp_read_project_config, mcp_remove, mcp_reset_project_choices, mcp_save_project_config,\n    mcp_serve, mcp_test_connection,\n};\n\nuse commands::proxy::{apply_proxy_settings, get_proxy_settings, save_proxy_settings};\nuse commands::storage::{\n    storage_delete_row, storage_execute_sql, storage_insert_row, storage_list_tables,\n    storage_read_table, storage_reset_database, storage_update_row,\n};\nuse commands::usage::{\n    get_session_stats, get_usage_by_date_range, get_usage_details, get_usage_stats,\n};\nuse process::ProcessRegistryState;\nuse std::sync::Mutex;\nuse tauri::Manager;\n\n#[cfg(target_os = \"macos\")]\nuse window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};\n\nfn main() {\n    // Initialize logger\n    env_logger::init();\n\n    tauri::Builder::default()\n        .plugin(tauri_plugin_dialog::init())\n        .plugin(tauri_plugin_shell::init())\n        .setup(|app| {\n            // Initialize agents database\n            let conn = init_database(&app.handle()).expect(\"Failed to initialize agents database\");\n\n            // Load and apply proxy settings from the database\n            {\n                let db = AgentDb(Mutex::new(conn));\n                let proxy_settings = match db.0.lock() {\n                    Ok(conn) => {\n                        // Directly query proxy settings from the database\n                        let mut settings = commands::proxy::ProxySettings::default();\n\n                        let keys = vec![\n                            (\"proxy_enabled\", \"enabled\"),\n                            (\"proxy_http\", \"http_proxy\"),\n                            (\"proxy_https\", \"https_proxy\"),\n                            (\"proxy_no\", \"no_proxy\"),\n                            (\"proxy_all\", \"all_proxy\"),\n                        ];\n\n                        for (db_key, field) in keys {\n                            if let Ok(value) = conn.query_row(\n                                \"SELECT value FROM app_settings WHERE key = ?1\",\n                                rusqlite::params![db_key],\n                                |row| row.get::<_, String>(0),\n                            ) {\n                                match field {\n                                    \"enabled\" => settings.enabled = value == \"true\",\n                                    \"http_proxy\" => {\n                                        settings.http_proxy = Some(value).filter(|s| !s.is_empty())\n                                    }\n                                    \"https_proxy\" => {\n                                        settings.https_proxy = Some(value).filter(|s| !s.is_empty())\n                                    }\n                                    \"no_proxy\" => {\n                                        settings.no_proxy = Some(value).filter(|s| !s.is_empty())\n                                    }\n                                    \"all_proxy\" => {\n                                        settings.all_proxy = Some(value).filter(|s| !s.is_empty())\n                                    }\n                                    _ => {}\n                                }\n                            }\n                        }\n\n                        log::info!(\"Loaded proxy settings: enabled={}\", settings.enabled);\n                        settings\n                    }\n                    Err(e) => {\n                        log::warn!(\"Failed to lock database for proxy settings: {}\", e);\n                        commands::proxy::ProxySettings::default()\n                    }\n                };\n\n                // Apply the proxy settings\n                apply_proxy_settings(&proxy_settings);\n            }\n\n            // Re-open the connection for the app to manage\n            let conn = init_database(&app.handle()).expect(\"Failed to initialize agents database\");\n            app.manage(AgentDb(Mutex::new(conn)));\n\n            // Initialize checkpoint state\n            let checkpoint_state = CheckpointState::new();\n\n            // Set the Claude directory path\n            if let Ok(claude_dir) = dirs::home_dir()\n                .ok_or_else(|| \"Could not find home directory\")\n                .and_then(|home| {\n                    let claude_path = home.join(\".claude\");\n                    claude_path\n                        .canonicalize()\n                        .map_err(|_| \"Could not find ~/.claude directory\")\n                })\n            {\n                let state_clone = checkpoint_state.clone();\n                tauri::async_runtime::spawn(async move {\n                    state_clone.set_claude_dir(claude_dir).await;\n                });\n            }\n\n            app.manage(checkpoint_state);\n\n            // Initialize process registry\n            app.manage(ProcessRegistryState::default());\n\n            // Initialize Claude process state\n            app.manage(ClaudeProcessState::default());\n\n            // Apply window vibrancy with rounded corners on macOS\n            #[cfg(target_os = \"macos\")]\n            {\n                let window = app.get_webview_window(\"main\").unwrap();\n\n                // Try different vibrancy materials that support rounded corners\n                let materials = [\n                    NSVisualEffectMaterial::UnderWindowBackground,\n                    NSVisualEffectMaterial::WindowBackground,\n                    NSVisualEffectMaterial::Popover,\n                    NSVisualEffectMaterial::Menu,\n                    NSVisualEffectMaterial::Sidebar,\n                ];\n\n                let mut applied = false;\n                for material in materials.iter() {\n                    if apply_vibrancy(&window, *material, None, Some(12.0)).is_ok() {\n                        applied = true;\n                        break;\n                    }\n                }\n\n                if !applied {\n                    // Fallback without rounded corners\n                    apply_vibrancy(\n                        &window,\n                        NSVisualEffectMaterial::WindowBackground,\n                        None,\n                        None,\n                    )\n                    .expect(\"Failed to apply any window vibrancy\");\n                }\n            }\n\n            Ok(())\n        })\n        .invoke_handler(tauri::generate_handler![\n            // Claude & Project Management\n            list_projects,\n            create_project,\n            get_project_sessions,\n            get_home_directory,\n            get_claude_settings,\n            open_new_session,\n            get_system_prompt,\n            check_claude_version,\n            save_system_prompt,\n            save_claude_settings,\n            find_claude_md_files,\n            read_claude_md_file,\n            save_claude_md_file,\n            load_session_history,\n            execute_claude_code,\n            continue_claude_code,\n            resume_claude_code,\n            cancel_claude_execution,\n            list_running_claude_sessions,\n            get_claude_session_output,\n            list_directory_contents,\n            search_files,\n            get_recently_modified_files,\n            get_hooks_config,\n            update_hooks_config,\n            validate_hook_command,\n            // Checkpoint Management\n            create_checkpoint,\n            restore_checkpoint,\n            list_checkpoints,\n            fork_from_checkpoint,\n            get_session_timeline,\n            update_checkpoint_settings,\n            get_checkpoint_diff,\n            track_checkpoint_message,\n            track_session_messages,\n            check_auto_checkpoint,\n            cleanup_old_checkpoints,\n            get_checkpoint_settings,\n            clear_checkpoint_manager,\n            get_checkpoint_state_stats,\n            // Agent Management\n            list_agents,\n            create_agent,\n            update_agent,\n            delete_agent,\n            get_agent,\n            execute_agent,\n            list_agent_runs,\n            get_agent_run,\n            list_agent_runs_with_metrics,\n            get_agent_run_with_real_time_metrics,\n            list_running_sessions,\n            kill_agent_session,\n            get_session_status,\n            cleanup_finished_processes,\n            get_session_output,\n            get_live_session_output,\n            stream_session_output,\n            load_agent_session_history,\n            get_claude_binary_path,\n            set_claude_binary_path,\n            list_claude_installations,\n            export_agent,\n            export_agent_to_file,\n            import_agent,\n            import_agent_from_file,\n            fetch_github_agents,\n            fetch_github_agent_content,\n            import_agent_from_github,\n            // Usage & Analytics\n            get_usage_stats,\n            get_usage_by_date_range,\n            get_usage_details,\n            get_session_stats,\n            // MCP (Model Context Protocol)\n            mcp_add,\n            mcp_list,\n            mcp_get,\n            mcp_remove,\n            mcp_add_json,\n            mcp_add_from_claude_desktop,\n            mcp_serve,\n            mcp_test_connection,\n            mcp_reset_project_choices,\n            mcp_get_server_status,\n            mcp_read_project_config,\n            mcp_save_project_config,\n            // Storage Management\n            storage_list_tables,\n            storage_read_table,\n            storage_update_row,\n            storage_delete_row,\n            storage_insert_row,\n            storage_execute_sql,\n            storage_reset_database,\n            // Slash Commands\n            commands::slash_commands::slash_commands_list,\n            commands::slash_commands::slash_command_get,\n            commands::slash_commands::slash_command_save,\n            commands::slash_commands::slash_command_delete,\n            // Proxy Settings\n            get_proxy_settings,\n            save_proxy_settings,\n        ])\n        .run(tauri::generate_context!())\n        .expect(\"error while running tauri application\");\n}\n"
  },
  {
    "path": "src-tauri/src/process/mod.rs",
    "content": "pub mod registry;\n\npub use registry::*;\n"
  },
  {
    "path": "src-tauri/src/process/registry.rs",
    "content": "use chrono::{DateTime, Utc};\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse std::sync::{Arc, Mutex};\nuse tokio::process::Child;\n\n/// Type of process being tracked\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum ProcessType {\n    AgentRun { agent_id: i64, agent_name: String },\n    ClaudeSession { session_id: String },\n}\n\n/// Information about a running agent process\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ProcessInfo {\n    pub run_id: i64,\n    pub process_type: ProcessType,\n    pub pid: u32,\n    pub started_at: DateTime<Utc>,\n    pub project_path: String,\n    pub task: String,\n    pub model: String,\n}\n\n/// Information about a running process with handle\n#[allow(dead_code)]\npub struct ProcessHandle {\n    pub info: ProcessInfo,\n    pub child: Arc<Mutex<Option<Child>>>,\n    pub live_output: Arc<Mutex<String>>,\n}\n\n/// Registry for tracking active agent processes\npub struct ProcessRegistry {\n    processes: Arc<Mutex<HashMap<i64, ProcessHandle>>>, // run_id -> ProcessHandle\n    next_id: Arc<Mutex<i64>>, // Auto-incrementing ID for non-agent processes\n}\n\nimpl ProcessRegistry {\n    pub fn new() -> Self {\n        Self {\n            processes: Arc::new(Mutex::new(HashMap::new())),\n            next_id: Arc::new(Mutex::new(1000000)), // Start at high number to avoid conflicts\n        }\n    }\n\n    /// Generate a unique ID for non-agent processes\n    pub fn generate_id(&self) -> Result<i64, String> {\n        let mut next_id = self.next_id.lock().map_err(|e| e.to_string())?;\n        let id = *next_id;\n        *next_id += 1;\n        Ok(id)\n    }\n\n    /// Register a new running agent process\n    pub fn register_process(\n        &self,\n        run_id: i64,\n        agent_id: i64,\n        agent_name: String,\n        pid: u32,\n        project_path: String,\n        task: String,\n        model: String,\n        child: Child,\n    ) -> Result<(), String> {\n        let process_info = ProcessInfo {\n            run_id,\n            process_type: ProcessType::AgentRun {\n                agent_id,\n                agent_name,\n            },\n            pid,\n            started_at: Utc::now(),\n            project_path,\n            task,\n            model,\n        };\n\n        self.register_process_internal(run_id, process_info, child)\n    }\n\n    /// Register a new running agent process using sidecar (similar to register_process but for sidecar children)\n    pub fn register_sidecar_process(\n        &self,\n        run_id: i64,\n        agent_id: i64,\n        agent_name: String,\n        pid: u32,\n        project_path: String,\n        task: String,\n        model: String,\n    ) -> Result<(), String> {\n        let process_info = ProcessInfo {\n            run_id,\n            process_type: ProcessType::AgentRun {\n                agent_id,\n                agent_name,\n            },\n            pid,\n            started_at: Utc::now(),\n            project_path,\n            task,\n            model,\n        };\n\n        // For sidecar processes, we register without the child handle since it's managed differently\n        let mut processes = self.processes.lock().map_err(|e| e.to_string())?;\n\n        let process_handle = ProcessHandle {\n            info: process_info,\n            child: Arc::new(Mutex::new(None)), // No tokio::process::Child handle for sidecar\n            live_output: Arc::new(Mutex::new(String::new())),\n        };\n\n        processes.insert(run_id, process_handle);\n        Ok(())\n    }\n\n    /// Register a new Claude session (without child process - handled separately)\n    pub fn register_claude_session(\n        &self,\n        session_id: String,\n        pid: u32,\n        project_path: String,\n        task: String,\n        model: String,\n    ) -> Result<i64, String> {\n        let run_id = self.generate_id()?;\n\n        let process_info = ProcessInfo {\n            run_id,\n            process_type: ProcessType::ClaudeSession { session_id },\n            pid,\n            started_at: Utc::now(),\n            project_path,\n            task,\n            model,\n        };\n\n        // Register without child - Claude sessions use ClaudeProcessState for process management\n        let mut processes = self.processes.lock().map_err(|e| e.to_string())?;\n\n        let process_handle = ProcessHandle {\n            info: process_info,\n            child: Arc::new(Mutex::new(None)), // No child handle for Claude sessions\n            live_output: Arc::new(Mutex::new(String::new())),\n        };\n\n        processes.insert(run_id, process_handle);\n        Ok(run_id)\n    }\n\n    /// Internal method to register any process\n    fn register_process_internal(\n        &self,\n        run_id: i64,\n        process_info: ProcessInfo,\n        child: Child,\n    ) -> Result<(), String> {\n        let mut processes = self.processes.lock().map_err(|e| e.to_string())?;\n\n        let process_handle = ProcessHandle {\n            info: process_info,\n            child: Arc::new(Mutex::new(Some(child))),\n            live_output: Arc::new(Mutex::new(String::new())),\n        };\n\n        processes.insert(run_id, process_handle);\n        Ok(())\n    }\n\n    /// Get all running Claude sessions\n    pub fn get_running_claude_sessions(&self) -> Result<Vec<ProcessInfo>, String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n        Ok(processes\n            .values()\n            .filter_map(|handle| match &handle.info.process_type {\n                ProcessType::ClaudeSession { .. } => Some(handle.info.clone()),\n                _ => None,\n            })\n            .collect())\n    }\n\n    /// Get a specific Claude session by session ID\n    pub fn get_claude_session_by_id(\n        &self,\n        session_id: &str,\n    ) -> Result<Option<ProcessInfo>, String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n        Ok(processes\n            .values()\n            .find(|handle| match &handle.info.process_type {\n                ProcessType::ClaudeSession { session_id: sid } => sid == session_id,\n                _ => false,\n            })\n            .map(|handle| handle.info.clone()))\n    }\n\n    /// Unregister a process (called when it completes)\n    #[allow(dead_code)]\n    pub fn unregister_process(&self, run_id: i64) -> Result<(), String> {\n        let mut processes = self.processes.lock().map_err(|e| e.to_string())?;\n        processes.remove(&run_id);\n        Ok(())\n    }\n\n    /// Get all running processes\n    #[allow(dead_code)]\n    pub fn get_running_processes(&self) -> Result<Vec<ProcessInfo>, String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n        Ok(processes\n            .values()\n            .map(|handle| handle.info.clone())\n            .collect())\n    }\n\n    /// Get all running agent processes\n    pub fn get_running_agent_processes(&self) -> Result<Vec<ProcessInfo>, String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n        Ok(processes\n            .values()\n            .filter_map(|handle| match &handle.info.process_type {\n                ProcessType::AgentRun { .. } => Some(handle.info.clone()),\n                _ => None,\n            })\n            .collect())\n    }\n\n    /// Get a specific running process\n    #[allow(dead_code)]\n    pub fn get_process(&self, run_id: i64) -> Result<Option<ProcessInfo>, String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n        Ok(processes.get(&run_id).map(|handle| handle.info.clone()))\n    }\n\n    /// Kill a running process with proper cleanup\n    pub async fn kill_process(&self, run_id: i64) -> Result<bool, String> {\n        use log::{error, info, warn};\n\n        // First check if the process exists and get its PID\n        let (pid, child_arc) = {\n            let processes = self.processes.lock().map_err(|e| e.to_string())?;\n            if let Some(handle) = processes.get(&run_id) {\n                (handle.info.pid, handle.child.clone())\n            } else {\n                warn!(\"Process {} not found in registry\", run_id);\n                return Ok(false); // Process not found\n            }\n        };\n\n        info!(\n            \"Attempting graceful shutdown of process {} (PID: {})\",\n            run_id, pid\n        );\n\n        // Send kill signal to the process\n        let kill_sent = {\n            let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?;\n            if let Some(child) = child_guard.as_mut() {\n                match child.start_kill() {\n                    Ok(_) => {\n                        info!(\"Successfully sent kill signal to process {}\", run_id);\n                        true\n                    }\n                    Err(e) => {\n                        error!(\"Failed to send kill signal to process {}: {}\", run_id, e);\n                        // Don't return error here, try fallback method\n                        false\n                    }\n                }\n            } else {\n                warn!(\n                    \"No child handle available for process {} (PID: {}), attempting system kill\",\n                    run_id, pid\n                );\n                false // Process handle not available, try fallback\n            }\n        };\n\n        // If direct kill didn't work, try system command as fallback\n        if !kill_sent {\n            info!(\n                \"Attempting fallback kill for process {} (PID: {})\",\n                run_id, pid\n            );\n            match self.kill_process_by_pid(run_id, pid) {\n                Ok(true) => return Ok(true),\n                Ok(false) => warn!(\n                    \"Fallback kill also failed for process {} (PID: {})\",\n                    run_id, pid\n                ),\n                Err(e) => error!(\"Error during fallback kill: {}\", e),\n            }\n            // Continue with the rest of the cleanup even if fallback failed\n        }\n\n        // Wait for the process to exit (with timeout)\n        let wait_result = tokio::time::timeout(tokio::time::Duration::from_secs(5), async {\n            loop {\n                // Check if process has exited\n                let status = {\n                    let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?;\n                    if let Some(child) = child_guard.as_mut() {\n                        match child.try_wait() {\n                            Ok(Some(status)) => {\n                                info!(\"Process {} exited with status: {:?}\", run_id, status);\n                                *child_guard = None; // Clear the child handle\n                                Some(Ok::<(), String>(()))\n                            }\n                            Ok(None) => {\n                                // Still running\n                                None\n                            }\n                            Err(e) => {\n                                error!(\"Error checking process status: {}\", e);\n                                Some(Err(e.to_string()))\n                            }\n                        }\n                    } else {\n                        // Process already gone\n                        Some(Ok(()))\n                    }\n                };\n\n                match status {\n                    Some(result) => return result,\n                    None => {\n                        // Still running, wait a bit\n                        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n                    }\n                }\n            }\n        })\n        .await;\n\n        match wait_result {\n            Ok(Ok(_)) => {\n                info!(\"Process {} exited gracefully\", run_id);\n            }\n            Ok(Err(e)) => {\n                error!(\"Error waiting for process {}: {}\", run_id, e);\n            }\n            Err(_) => {\n                warn!(\"Process {} didn't exit within 5 seconds after kill\", run_id);\n                // Force clear the handle\n                if let Ok(mut child_guard) = child_arc.lock() {\n                    *child_guard = None;\n                }\n                // One more attempt with system kill\n                let _ = self.kill_process_by_pid(run_id, pid);\n            }\n        }\n\n        // Remove from registry after killing\n        self.unregister_process(run_id)?;\n\n        Ok(true)\n    }\n\n    /// Kill a process by PID using system commands (fallback method)\n    pub fn kill_process_by_pid(&self, run_id: i64, pid: u32) -> Result<bool, String> {\n        use log::{error, info, warn};\n\n        info!(\"Attempting to kill process {} by PID {}\", run_id, pid);\n\n        let kill_result = if cfg!(target_os = \"windows\") {\n            std::process::Command::new(\"taskkill\")\n                .args([\"/F\", \"/PID\", &pid.to_string()])\n                .output()\n        } else {\n            // First try SIGTERM\n            let term_result = std::process::Command::new(\"kill\")\n                .args([\"-TERM\", &pid.to_string()])\n                .output();\n\n            match &term_result {\n                Ok(output) if output.status.success() => {\n                    info!(\"Sent SIGTERM to PID {}\", pid);\n                    // Give it 2 seconds to exit gracefully\n                    std::thread::sleep(std::time::Duration::from_secs(2));\n\n                    // Check if still running\n                    let check_result = std::process::Command::new(\"kill\")\n                        .args([\"-0\", &pid.to_string()])\n                        .output();\n\n                    if let Ok(output) = check_result {\n                        if output.status.success() {\n                            // Still running, send SIGKILL\n                            warn!(\n                                \"Process {} still running after SIGTERM, sending SIGKILL\",\n                                pid\n                            );\n                            std::process::Command::new(\"kill\")\n                                .args([\"-KILL\", &pid.to_string()])\n                                .output()\n                        } else {\n                            term_result\n                        }\n                    } else {\n                        term_result\n                    }\n                }\n                _ => {\n                    // SIGTERM failed, try SIGKILL directly\n                    warn!(\"SIGTERM failed for PID {}, trying SIGKILL\", pid);\n                    std::process::Command::new(\"kill\")\n                        .args([\"-KILL\", &pid.to_string()])\n                        .output()\n                }\n            }\n        };\n\n        match kill_result {\n            Ok(output) => {\n                if output.status.success() {\n                    info!(\"Successfully killed process with PID {}\", pid);\n                    // Remove from registry\n                    self.unregister_process(run_id)?;\n                    Ok(true)\n                } else {\n                    let error_msg = String::from_utf8_lossy(&output.stderr);\n                    warn!(\"Failed to kill PID {}: {}\", pid, error_msg);\n                    Ok(false)\n                }\n            }\n            Err(e) => {\n                error!(\"Failed to execute kill command for PID {}: {}\", pid, e);\n                Err(format!(\"Failed to execute kill command: {}\", e))\n            }\n        }\n    }\n\n    /// Check if a process is still running by trying to get its status\n    #[allow(dead_code)]\n    pub async fn is_process_running(&self, run_id: i64) -> Result<bool, String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n\n        if let Some(handle) = processes.get(&run_id) {\n            let child_arc = handle.child.clone();\n            drop(processes); // Release the lock before async operation\n\n            let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?;\n            if let Some(ref mut child) = child_guard.as_mut() {\n                match child.try_wait() {\n                    Ok(Some(_)) => {\n                        // Process has exited\n                        *child_guard = None;\n                        Ok(false)\n                    }\n                    Ok(None) => {\n                        // Process is still running\n                        Ok(true)\n                    }\n                    Err(_) => {\n                        // Error checking status, assume not running\n                        *child_guard = None;\n                        Ok(false)\n                    }\n                }\n            } else {\n                Ok(false) // No child handle\n            }\n        } else {\n            Ok(false) // Process not found in registry\n        }\n    }\n\n    /// Append to live output for a process\n    pub fn append_live_output(&self, run_id: i64, output: &str) -> Result<(), String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n        if let Some(handle) = processes.get(&run_id) {\n            let mut live_output = handle.live_output.lock().map_err(|e| e.to_string())?;\n            live_output.push_str(output);\n            live_output.push('\\n');\n        }\n        Ok(())\n    }\n\n    /// Get live output for a process\n    pub fn get_live_output(&self, run_id: i64) -> Result<String, String> {\n        let processes = self.processes.lock().map_err(|e| e.to_string())?;\n        if let Some(handle) = processes.get(&run_id) {\n            let live_output = handle.live_output.lock().map_err(|e| e.to_string())?;\n            Ok(live_output.clone())\n        } else {\n            Ok(String::new())\n        }\n    }\n\n    /// Cleanup finished processes\n    #[allow(dead_code)]\n    pub async fn cleanup_finished_processes(&self) -> Result<Vec<i64>, String> {\n        let mut finished_runs = Vec::new();\n        let processes_lock = self.processes.clone();\n\n        // First, identify finished processes\n        {\n            let processes = processes_lock.lock().map_err(|e| e.to_string())?;\n            let run_ids: Vec<i64> = processes.keys().cloned().collect();\n            drop(processes);\n\n            for run_id in run_ids {\n                if !self.is_process_running(run_id).await? {\n                    finished_runs.push(run_id);\n                }\n            }\n        }\n\n        // Then remove them from the registry\n        {\n            let mut processes = processes_lock.lock().map_err(|e| e.to_string())?;\n            for run_id in &finished_runs {\n                processes.remove(run_id);\n            }\n        }\n\n        Ok(finished_runs)\n    }\n}\n\nimpl Default for ProcessRegistry {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n/// Global process registry state\npub struct ProcessRegistryState(pub Arc<ProcessRegistry>);\n\nimpl Default for ProcessRegistryState {\n    fn default() -> Self {\n        Self(Arc::new(ProcessRegistry::new()))\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/web_main.rs",
    "content": "use clap::Parser;\n\nmod checkpoint;\nmod claude_binary;\nmod commands;\nmod process;\nmod web_server;\n\n#[derive(Parser)]\n#[command(name = \"opcode-web\")]\n#[command(about = \"Opcode Web Server - Access Opcode from your phone\")]\nstruct Args {\n    /// Port to run the web server on\n    #[arg(short, long, default_value = \"8080\")]\n    port: u16,\n\n    /// Host to bind to (0.0.0.0 for all interfaces)\n    #[arg(short = 'H', long, default_value = \"0.0.0.0\")]\n    host: String,\n}\n\n#[tokio::main]\nasync fn main() {\n    env_logger::init();\n\n    let args = Args::parse();\n\n    println!(\"🚀 Starting Opcode Web Server...\");\n    println!(\n        \"📱 Will be accessible from phones at: http://{}:{}\",\n        args.host, args.port\n    );\n\n    if let Err(e) = web_server::start_web_mode(Some(args.port)).await {\n        eprintln!(\"❌ Failed to start web server: {}\", e);\n        std::process::exit(1);\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/web_server.rs",
    "content": "use axum::extract::ws::{Message, WebSocket};\nuse axum::http::Method;\nuse axum::{\n    extract::{Path, State as AxumState, WebSocketUpgrade},\n    response::{Html, Json, Response},\n    routing::get,\n    Router,\n};\nuse chrono;\nuse futures_util::{SinkExt, StreamExt};\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse std::net::SocketAddr;\nuse std::sync::Arc;\nuse tokio::net::TcpListener;\nuse tokio::sync::Mutex;\nuse tower_http::cors::{Any, CorsLayer};\nuse tower_http::services::ServeDir;\nuse which;\n\nuse crate::commands;\n\n// Find Claude binary for web mode - use bundled binary first\nfn find_claude_binary_web() -> Result<String, String> {\n    // First try the bundled binary (same location as Tauri app uses)\n    let bundled_binary = \"src-tauri/binaries/claude-code-x86_64-unknown-linux-gnu\";\n    if std::path::Path::new(bundled_binary).exists() {\n        println!(\n            \"[find_claude_binary_web] Using bundled binary: {}\",\n            bundled_binary\n        );\n        return Ok(bundled_binary.to_string());\n    }\n\n    // Fall back to system installation paths\n    let home_path = format!(\n        \"{}/.local/bin/claude\",\n        std::env::var(\"HOME\").unwrap_or_default()\n    );\n    let candidates = vec![\n        \"claude\",\n        \"claude-code\",\n        \"/usr/local/bin/claude\",\n        \"/usr/bin/claude\",\n        \"/opt/homebrew/bin/claude\",\n        &home_path,\n    ];\n\n    for candidate in candidates {\n        if which::which(candidate).is_ok() {\n            println!(\n                \"[find_claude_binary_web] Using system binary: {}\",\n                candidate\n            );\n            return Ok(candidate.to_string());\n        }\n    }\n\n    Err(\"Claude binary not found in bundled location or system paths\".to_string())\n}\n\n#[derive(Clone)]\npub struct AppState {\n    // Track active WebSocket sessions for Claude execution\n    pub active_sessions:\n        Arc<Mutex<std::collections::HashMap<String, tokio::sync::mpsc::Sender<String>>>>,\n}\n\n#[derive(Debug, Deserialize)]\npub struct ClaudeExecutionRequest {\n    pub project_path: String,\n    pub prompt: String,\n    pub model: Option<String>,\n    pub session_id: Option<String>,\n    pub command_type: String, // \"execute\", \"continue\", or \"resume\"\n}\n\n#[derive(Deserialize)]\npub struct QueryParams {\n    #[serde(default)]\n    pub project_path: Option<String>,\n}\n\n#[derive(Serialize)]\npub struct ApiResponse<T> {\n    pub success: bool,\n    pub data: Option<T>,\n    pub error: Option<String>,\n}\n\nimpl<T> ApiResponse<T> {\n    pub fn success(data: T) -> Self {\n        Self {\n            success: true,\n            data: Some(data),\n            error: None,\n        }\n    }\n\n    pub fn error(error: String) -> Self {\n        Self {\n            success: false,\n            data: None,\n            error: Some(error),\n        }\n    }\n}\n\n/// Serve the React frontend\nasync fn serve_frontend() -> Html<&'static str> {\n    Html(include_str!(\"../../dist/index.html\"))\n}\n\n/// API endpoint to get projects (equivalent to Tauri command)\nasync fn get_projects() -> Json<ApiResponse<Vec<commands::claude::Project>>> {\n    match commands::claude::list_projects().await {\n        Ok(projects) => Json(ApiResponse::success(projects)),\n        Err(e) => Json(ApiResponse::error(e.to_string())),\n    }\n}\n\n/// API endpoint to get sessions for a project\nasync fn get_sessions(\n    Path(project_id): Path<String>,\n) -> Json<ApiResponse<Vec<commands::claude::Session>>> {\n    match commands::claude::get_project_sessions(project_id).await {\n        Ok(sessions) => Json(ApiResponse::success(sessions)),\n        Err(e) => Json(ApiResponse::error(e.to_string())),\n    }\n}\n\n/// Simple agents endpoint - return empty for now (needs DB state)\nasync fn get_agents() -> Json<ApiResponse<Vec<serde_json::Value>>> {\n    Json(ApiResponse::success(vec![]))\n}\n\n/// Simple usage endpoint - return empty for now\nasync fn get_usage() -> Json<ApiResponse<Vec<serde_json::Value>>> {\n    Json(ApiResponse::success(vec![]))\n}\n\n/// Get Claude settings - return basic defaults for web mode\nasync fn get_claude_settings() -> Json<ApiResponse<serde_json::Value>> {\n    let default_settings = serde_json::json!({\n        \"data\": {\n            \"model\": \"claude-3-5-sonnet-20241022\",\n            \"max_tokens\": 8192,\n            \"temperature\": 0.0,\n            \"auto_save\": true,\n            \"theme\": \"dark\"\n        }\n    });\n    Json(ApiResponse::success(default_settings))\n}\n\n/// Check Claude version - return mock status for web mode\nasync fn check_claude_version() -> Json<ApiResponse<serde_json::Value>> {\n    let version_status = serde_json::json!({\n        \"status\": \"ok\",\n        \"version\": \"web-mode\",\n        \"message\": \"Running in web server mode\"\n    });\n    Json(ApiResponse::success(version_status))\n}\n\n/// List all available Claude installations on the system\nasync fn list_claude_installations(\n) -> Json<ApiResponse<Vec<crate::claude_binary::ClaudeInstallation>>> {\n    let installations = crate::claude_binary::discover_claude_installations();\n\n    if installations.is_empty() {\n        Json(ApiResponse::error(\n            \"No Claude Code installations found on the system\".to_string(),\n        ))\n    } else {\n        Json(ApiResponse::success(installations))\n    }\n}\n\n/// Get system prompt - return default for web mode\nasync fn get_system_prompt() -> Json<ApiResponse<String>> {\n    let default_prompt =\n        \"You are Claude, an AI assistant created by Anthropic. You are running in web server mode.\"\n            .to_string();\n    Json(ApiResponse::success(default_prompt))\n}\n\n/// Open new session - mock for web mode\nasync fn open_new_session() -> Json<ApiResponse<String>> {\n    let session_id = format!(\"web-session-{}\", chrono::Utc::now().timestamp());\n    Json(ApiResponse::success(session_id))\n}\n\n/// List slash commands - return empty for web mode\nasync fn list_slash_commands() -> Json<ApiResponse<Vec<serde_json::Value>>> {\n    Json(ApiResponse::success(vec![]))\n}\n\n/// MCP list servers - return empty for web mode\nasync fn mcp_list() -> Json<ApiResponse<Vec<serde_json::Value>>> {\n    Json(ApiResponse::success(vec![]))\n}\n\n/// Load session history from JSONL file\nasync fn load_session_history(\n    Path((session_id, project_id)): Path<(String, String)>,\n) -> Json<ApiResponse<Vec<serde_json::Value>>> {\n    match commands::claude::load_session_history(session_id, project_id).await {\n        Ok(history) => Json(ApiResponse::success(history)),\n        Err(e) => Json(ApiResponse::error(e.to_string())),\n    }\n}\n\n/// List running Claude sessions\nasync fn list_running_claude_sessions() -> Json<ApiResponse<Vec<serde_json::Value>>> {\n    // Return empty for web mode - no actual Claude processes in web mode\n    Json(ApiResponse::success(vec![]))\n}\n\n/// Execute Claude code - mock for web mode\nasync fn execute_claude_code() -> Json<ApiResponse<serde_json::Value>> {\n    Json(ApiResponse::error(\"Claude execution is not available in web mode. Please use the desktop app for running Claude commands.\".to_string()))\n}\n\n/// Continue Claude code - mock for web mode\nasync fn continue_claude_code() -> Json<ApiResponse<serde_json::Value>> {\n    Json(ApiResponse::error(\"Claude execution is not available in web mode. Please use the desktop app for running Claude commands.\".to_string()))\n}\n\n/// Resume Claude code - mock for web mode  \nasync fn resume_claude_code() -> Json<ApiResponse<serde_json::Value>> {\n    Json(ApiResponse::error(\"Claude execution is not available in web mode. Please use the desktop app for running Claude commands.\".to_string()))\n}\n\n/// Cancel Claude execution\nasync fn cancel_claude_execution(Path(sessionId): Path<String>) -> Json<ApiResponse<()>> {\n    // In web mode, we don't have a way to cancel the subprocess cleanly\n    // The WebSocket closing should handle cleanup\n    println!(\"[TRACE] Cancel request for session: {}\", sessionId);\n    Json(ApiResponse::success(()))\n}\n\n/// Get Claude session output\nasync fn get_claude_session_output(Path(sessionId): Path<String>) -> Json<ApiResponse<String>> {\n    // In web mode, output is streamed via WebSocket, not stored\n    println!(\"[TRACE] Output request for session: {}\", sessionId);\n    Json(ApiResponse::success(\n        \"Output available via WebSocket only\".to_string(),\n    ))\n}\n\n/// WebSocket handler for Claude execution with streaming output\nasync fn claude_websocket(ws: WebSocketUpgrade, AxumState(state): AxumState<AppState>) -> Response {\n    ws.on_upgrade(move |socket| claude_websocket_handler(socket, state))\n}\n\nasync fn claude_websocket_handler(socket: WebSocket, state: AppState) {\n    let (mut sender, mut receiver) = socket.split();\n    let session_id = uuid::Uuid::new_v4().to_string();\n\n    println!(\n        \"[TRACE] WebSocket handler started - session_id: {}\",\n        session_id\n    );\n\n    // Channel for sending output to WebSocket\n    let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(100);\n\n    // Store session in state\n    {\n        let mut sessions = state.active_sessions.lock().await;\n        sessions.insert(session_id.clone(), tx);\n        println!(\n            \"[TRACE] Session stored in state - active sessions count: {}\",\n            sessions.len()\n        );\n    }\n\n    // Task to forward channel messages to WebSocket\n    let session_id_for_forward = session_id.clone();\n    let forward_task = tokio::spawn(async move {\n        println!(\n            \"[TRACE] Forward task started for session {}\",\n            session_id_for_forward\n        );\n        while let Some(message) = rx.recv().await {\n            println!(\"[TRACE] Forwarding message to WebSocket: {}\", message);\n            if sender.send(Message::Text(message.into())).await.is_err() {\n                println!(\"[TRACE] Failed to send message to WebSocket - connection closed\");\n                break;\n            }\n        }\n        println!(\n            \"[TRACE] Forward task ended for session {}\",\n            session_id_for_forward\n        );\n    });\n\n    // Handle incoming messages from WebSocket\n    println!(\"[TRACE] Starting to listen for WebSocket messages\");\n    while let Some(msg) = receiver.next().await {\n        println!(\"[TRACE] Received WebSocket message: {:?}\", msg);\n        if let Ok(msg) = msg {\n            if let Message::Text(text) = msg {\n                println!(\n                    \"[TRACE] WebSocket text message received - length: {} chars\",\n                    text.len()\n                );\n                println!(\"[TRACE] WebSocket message content: {}\", text);\n                match serde_json::from_str::<ClaudeExecutionRequest>(&text) {\n                    Ok(request) => {\n                        println!(\"[TRACE] Successfully parsed request: {:?}\", request);\n                        println!(\"[TRACE] Command type: {}\", request.command_type);\n                        println!(\"[TRACE] Project path: {}\", request.project_path);\n                        println!(\"[TRACE] Prompt length: {} chars\", request.prompt.len());\n\n                        // Execute Claude command based on request type\n                        let session_id_clone = session_id.clone();\n                        let state_clone = state.clone();\n\n                        println!(\n                            \"[TRACE] Spawning task to execute command: {}\",\n                            request.command_type\n                        );\n                        tokio::spawn(async move {\n                            println!(\"[TRACE] Task started for command execution\");\n                            let result = match request.command_type.as_str() {\n                                \"execute\" => {\n                                    println!(\"[TRACE] Calling execute_claude_command\");\n                                    execute_claude_command(\n                                        request.project_path,\n                                        request.prompt,\n                                        request.model.unwrap_or_default(),\n                                        session_id_clone.clone(),\n                                        state_clone.clone(),\n                                    )\n                                    .await\n                                }\n                                \"continue\" => {\n                                    println!(\"[TRACE] Calling continue_claude_command\");\n                                    continue_claude_command(\n                                        request.project_path,\n                                        request.prompt,\n                                        request.model.unwrap_or_default(),\n                                        session_id_clone.clone(),\n                                        state_clone.clone(),\n                                    )\n                                    .await\n                                }\n                                \"resume\" => {\n                                    println!(\"[TRACE] Calling resume_claude_command\");\n                                    resume_claude_command(\n                                        request.project_path,\n                                        request.session_id.unwrap_or_default(),\n                                        request.prompt,\n                                        request.model.unwrap_or_default(),\n                                        session_id_clone.clone(),\n                                        state_clone.clone(),\n                                    )\n                                    .await\n                                }\n                                _ => {\n                                    println!(\n                                        \"[TRACE] Unknown command type: {}\",\n                                        request.command_type\n                                    );\n                                    Err(\"Unknown command type\".to_string())\n                                }\n                            };\n\n                            println!(\n                                \"[TRACE] Command execution finished with result: {:?}\",\n                                result\n                            );\n\n                            // Send completion message\n                            if let Some(sender) = state_clone\n                                .active_sessions\n                                .lock()\n                                .await\n                                .get(&session_id_clone)\n                            {\n                                let completion_msg = match result {\n                                    Ok(_) => json!({\n                                        \"type\": \"completion\",\n                                        \"status\": \"success\"\n                                    }),\n                                    Err(e) => json!({\n                                        \"type\": \"completion\",\n                                        \"status\": \"error\",\n                                        \"error\": e\n                                    }),\n                                };\n                                println!(\"[TRACE] Sending completion message: {}\", completion_msg);\n                                let _ = sender.send(completion_msg.to_string()).await;\n                            } else {\n                                println!(\"[TRACE] Session not found in active sessions when sending completion\");\n                            }\n                        });\n                    }\n                    Err(e) => {\n                        println!(\"[TRACE] Failed to parse WebSocket request: {}\", e);\n                        println!(\"[TRACE] Raw message that failed to parse: {}\", text);\n\n                        // Send error back to client\n                        let error_msg = json!({\n                            \"type\": \"error\",\n                            \"message\": format!(\"Failed to parse request: {}\", e)\n                        });\n                        if let Some(sender_tx) = state.active_sessions.lock().await.get(&session_id)\n                        {\n                            let _ = sender_tx.send(error_msg.to_string()).await;\n                        }\n                    }\n                }\n            } else if let Message::Close(_) = msg {\n                println!(\"[TRACE] WebSocket close message received\");\n                break;\n            } else {\n                println!(\"[TRACE] Non-text WebSocket message received: {:?}\", msg);\n            }\n        } else {\n            println!(\"[TRACE] Error receiving WebSocket message\");\n        }\n    }\n\n    println!(\"[TRACE] WebSocket message loop ended\");\n\n    // Clean up session\n    {\n        let mut sessions = state.active_sessions.lock().await;\n        sessions.remove(&session_id);\n        println!(\n            \"[TRACE] Session {} removed from state - remaining sessions: {}\",\n            session_id,\n            sessions.len()\n        );\n    }\n\n    forward_task.abort();\n    println!(\"[TRACE] WebSocket handler ended for session {}\", session_id);\n}\n\n// Claude command execution functions for WebSocket streaming\nasync fn execute_claude_command(\n    project_path: String,\n    prompt: String,\n    model: String,\n    session_id: String,\n    state: AppState,\n) -> Result<(), String> {\n    use tokio::io::{AsyncBufReadExt, BufReader};\n    use tokio::process::Command;\n\n    println!(\"[TRACE] execute_claude_command called:\");\n    println!(\"[TRACE]   project_path: {}\", project_path);\n    println!(\"[TRACE]   prompt length: {} chars\", prompt.len());\n    println!(\"[TRACE]   model: {}\", model);\n    println!(\"[TRACE]   session_id: {}\", session_id);\n\n    // Send initial message\n    println!(\"[TRACE] Sending initial start message\");\n    send_to_session(\n        &state,\n        &session_id,\n        json!({\n            \"type\": \"start\",\n            \"message\": \"Starting Claude execution...\"\n        })\n        .to_string(),\n    )\n    .await;\n\n    // Find Claude binary (simplified for web mode)\n    println!(\"[TRACE] Finding Claude binary...\");\n    let claude_path = find_claude_binary_web().map_err(|e| {\n        let error = format!(\"Claude binary not found: {}\", e);\n        println!(\"[TRACE] Error finding Claude binary: {}\", error);\n        error\n    })?;\n    println!(\"[TRACE] Found Claude binary: {}\", claude_path);\n\n    // Create Claude command\n    println!(\"[TRACE] Creating Claude command...\");\n    let mut cmd = Command::new(&claude_path);\n    let args = [\n        \"-p\",\n        &prompt,\n        \"--model\",\n        &model,\n        \"--output-format\",\n        \"stream-json\",\n        \"--verbose\",\n        \"--dangerously-skip-permissions\",\n    ];\n    cmd.args(args);\n    cmd.current_dir(&project_path);\n    cmd.stdout(std::process::Stdio::piped());\n    cmd.stderr(std::process::Stdio::piped());\n\n    println!(\n        \"[TRACE] Command: {} {:?} (in dir: {})\",\n        claude_path, args, project_path\n    );\n\n    // Spawn Claude process\n    println!(\"[TRACE] Spawning Claude process...\");\n    let mut child = cmd.spawn().map_err(|e| {\n        let error = format!(\"Failed to spawn Claude: {}\", e);\n        println!(\"[TRACE] Spawn error: {}\", error);\n        error\n    })?;\n    println!(\"[TRACE] Claude process spawned successfully\");\n\n    // Get stdout for streaming\n    let stdout = child.stdout.take().ok_or_else(|| {\n        println!(\"[TRACE] Failed to get stdout from child process\");\n        \"Failed to get stdout\".to_string()\n    })?;\n    let stdout_reader = BufReader::new(stdout);\n\n    println!(\"[TRACE] Starting to read Claude output...\");\n    // Stream output line by line\n    let mut lines = stdout_reader.lines();\n    let mut line_count = 0;\n    while let Ok(Some(line)) = lines.next_line().await {\n        line_count += 1;\n        println!(\"[TRACE] Claude output line {}: {}\", line_count, line);\n\n        // Send each line to WebSocket\n        let message = json!({\n            \"type\": \"output\",\n            \"content\": line\n        })\n        .to_string();\n        println!(\"[TRACE] Sending output message to session: {}\", message);\n        send_to_session(&state, &session_id, message).await;\n    }\n\n    println!(\n        \"[TRACE] Finished reading Claude output ({} lines total)\",\n        line_count\n    );\n\n    // Wait for process to complete\n    println!(\"[TRACE] Waiting for Claude process to complete...\");\n    let exit_status = child.wait().await.map_err(|e| {\n        let error = format!(\"Failed to wait for Claude: {}\", e);\n        println!(\"[TRACE] Wait error: {}\", error);\n        error\n    })?;\n\n    println!(\n        \"[TRACE] Claude process completed with status: {:?}\",\n        exit_status\n    );\n\n    if !exit_status.success() {\n        let error = format!(\n            \"Claude execution failed with exit code: {:?}\",\n            exit_status.code()\n        );\n        println!(\"[TRACE] Claude execution failed: {}\", error);\n        return Err(error);\n    }\n\n    println!(\"[TRACE] execute_claude_command completed successfully\");\n    Ok(())\n}\n\nasync fn continue_claude_command(\n    project_path: String,\n    prompt: String,\n    model: String,\n    session_id: String,\n    state: AppState,\n) -> Result<(), String> {\n    use tokio::io::{AsyncBufReadExt, BufReader};\n    use tokio::process::Command;\n\n    send_to_session(\n        &state,\n        &session_id,\n        json!({\n            \"type\": \"start\",\n            \"message\": \"Continuing Claude session...\"\n        })\n        .to_string(),\n    )\n    .await;\n\n    // Find Claude binary\n    let claude_path =\n        find_claude_binary_web().map_err(|e| format!(\"Claude binary not found: {}\", e))?;\n\n    // Create continue command\n    let mut cmd = Command::new(&claude_path);\n    cmd.args([\n        \"-c\", // Continue flag\n        \"-p\",\n        &prompt,\n        \"--model\",\n        &model,\n        \"--output-format\",\n        \"stream-json\",\n        \"--verbose\",\n        \"--dangerously-skip-permissions\",\n    ]);\n    cmd.current_dir(&project_path);\n    cmd.stdout(std::process::Stdio::piped());\n    cmd.stderr(std::process::Stdio::piped());\n\n    // Spawn and stream output\n    let mut child = cmd\n        .spawn()\n        .map_err(|e| format!(\"Failed to spawn Claude: {}\", e))?;\n    let stdout = child.stdout.take().ok_or(\"Failed to get stdout\")?;\n    let stdout_reader = BufReader::new(stdout);\n\n    let mut lines = stdout_reader.lines();\n    while let Ok(Some(line)) = lines.next_line().await {\n        send_to_session(\n            &state,\n            &session_id,\n            json!({\n                \"type\": \"output\",\n                \"content\": line\n            })\n            .to_string(),\n        )\n        .await;\n    }\n\n    let exit_status = child\n        .wait()\n        .await\n        .map_err(|e| format!(\"Failed to wait for Claude: {}\", e))?;\n    if !exit_status.success() {\n        return Err(format!(\n            \"Claude execution failed with exit code: {:?}\",\n            exit_status.code()\n        ));\n    }\n\n    Ok(())\n}\n\nasync fn resume_claude_command(\n    project_path: String,\n    claude_session_id: String,\n    prompt: String,\n    model: String,\n    session_id: String,\n    state: AppState,\n) -> Result<(), String> {\n    use tokio::io::{AsyncBufReadExt, BufReader};\n    use tokio::process::Command;\n\n    println!(\"[resume_claude_command] Starting with project_path: {}, claude_session_id: {}, prompt: {}, model: {}\", \n             project_path, claude_session_id, prompt, model);\n\n    send_to_session(\n        &state,\n        &session_id,\n        json!({\n            \"type\": \"start\",\n            \"message\": \"Resuming Claude session...\"\n        })\n        .to_string(),\n    )\n    .await;\n\n    // Find Claude binary\n    println!(\"[resume_claude_command] Finding Claude binary...\");\n    let claude_path =\n        find_claude_binary_web().map_err(|e| format!(\"Claude binary not found: {}\", e))?;\n    println!(\n        \"[resume_claude_command] Found Claude binary: {}\",\n        claude_path\n    );\n\n    // Create resume command\n    println!(\"[resume_claude_command] Creating command...\");\n    let mut cmd = Command::new(&claude_path);\n    let args = [\n        \"--resume\",\n        &claude_session_id,\n        \"-p\",\n        &prompt,\n        \"--model\",\n        &model,\n        \"--output-format\",\n        \"stream-json\",\n        \"--verbose\",\n        \"--dangerously-skip-permissions\",\n    ];\n    cmd.args(args);\n    cmd.current_dir(&project_path);\n    cmd.stdout(std::process::Stdio::piped());\n    cmd.stderr(std::process::Stdio::piped());\n\n    println!(\n        \"[resume_claude_command] Command: {} {:?} (in dir: {})\",\n        claude_path, args, project_path\n    );\n\n    // Spawn and stream output\n    println!(\"[resume_claude_command] Spawning process...\");\n    let mut child = cmd.spawn().map_err(|e| {\n        let error = format!(\"Failed to spawn Claude: {}\", e);\n        println!(\"[resume_claude_command] Spawn error: {}\", error);\n        error\n    })?;\n    println!(\"[resume_claude_command] Process spawned successfully\");\n    let stdout = child.stdout.take().ok_or(\"Failed to get stdout\")?;\n    let stdout_reader = BufReader::new(stdout);\n\n    let mut lines = stdout_reader.lines();\n    while let Ok(Some(line)) = lines.next_line().await {\n        send_to_session(\n            &state,\n            &session_id,\n            json!({\n                \"type\": \"output\",\n                \"content\": line\n            })\n            .to_string(),\n        )\n        .await;\n    }\n\n    let exit_status = child\n        .wait()\n        .await\n        .map_err(|e| format!(\"Failed to wait for Claude: {}\", e))?;\n    if !exit_status.success() {\n        return Err(format!(\n            \"Claude execution failed with exit code: {:?}\",\n            exit_status.code()\n        ));\n    }\n\n    Ok(())\n}\n\nasync fn send_to_session(state: &AppState, session_id: &str, message: String) {\n    println!(\"[TRACE] send_to_session called for session: {}\", session_id);\n    println!(\"[TRACE] Message: {}\", message);\n\n    let sessions = state.active_sessions.lock().await;\n    if let Some(sender) = sessions.get(session_id) {\n        println!(\"[TRACE] Found session in active sessions, sending message...\");\n        match sender.send(message).await {\n            Ok(_) => println!(\"[TRACE] Message sent successfully\"),\n            Err(e) => println!(\"[TRACE] Failed to send message: {}\", e),\n        }\n    } else {\n        println!(\n            \"[TRACE] Session {} not found in active sessions\",\n            session_id\n        );\n        println!(\n            \"[TRACE] Active sessions: {:?}\",\n            sessions.keys().collect::<Vec<_>>()\n        );\n    }\n}\n\n/// Create the web server\npub async fn create_web_server(port: u16) -> Result<(), Box<dyn std::error::Error>> {\n    let state = AppState {\n        active_sessions: Arc::new(Mutex::new(std::collections::HashMap::new())),\n    };\n\n    // CORS layer to allow requests from phone browsers\n    let cors = CorsLayer::new()\n        .allow_origin(Any)\n        .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])\n        .allow_headers(Any);\n\n    // Create router with API endpoints\n    let app = Router::new()\n        // Frontend routes\n        .route(\"/\", get(serve_frontend))\n        .route(\"/index.html\", get(serve_frontend))\n        // API routes (REST API equivalent of Tauri commands)\n        .route(\"/api/projects\", get(get_projects))\n        .route(\"/api/projects/{project_id}/sessions\", get(get_sessions))\n        .route(\"/api/agents\", get(get_agents))\n        .route(\"/api/usage\", get(get_usage))\n        // Settings and configuration\n        .route(\"/api/settings/claude\", get(get_claude_settings))\n        .route(\"/api/settings/claude/version\", get(check_claude_version))\n        .route(\n            \"/api/settings/claude/installations\",\n            get(list_claude_installations),\n        )\n        .route(\"/api/settings/system-prompt\", get(get_system_prompt))\n        // Session management\n        .route(\"/api/sessions/new\", get(open_new_session))\n        // Slash commands\n        .route(\"/api/slash-commands\", get(list_slash_commands))\n        // MCP\n        .route(\"/api/mcp/servers\", get(mcp_list))\n        // Session history\n        .route(\n            \"/api/sessions/{session_id}/history/{project_id}\",\n            get(load_session_history),\n        )\n        .route(\"/api/sessions/running\", get(list_running_claude_sessions))\n        // Claude execution endpoints (read-only in web mode)\n        .route(\"/api/sessions/execute\", get(execute_claude_code))\n        .route(\"/api/sessions/continue\", get(continue_claude_code))\n        .route(\"/api/sessions/resume\", get(resume_claude_code))\n        .route(\n            \"/api/sessions/{sessionId}/cancel\",\n            get(cancel_claude_execution),\n        )\n        .route(\n            \"/api/sessions/{sessionId}/output\",\n            get(get_claude_session_output),\n        )\n        // WebSocket endpoint for real-time Claude execution\n        .route(\"/ws/claude\", get(claude_websocket))\n        // Serve static assets\n        .nest_service(\"/assets\", ServeDir::new(\"../dist/assets\"))\n        .nest_service(\"/vite.svg\", ServeDir::new(\"../dist/vite.svg\"))\n        .layer(cors)\n        .with_state(state);\n\n    let addr = SocketAddr::from(([0, 0, 0, 0], port));\n    println!(\"🌐 Web server running on http://0.0.0.0:{}\", port);\n    println!(\"📱 Access from phone: http://YOUR_PC_IP:{}\", port);\n\n    let listener = TcpListener::bind(addr).await?;\n    axum::serve(listener, app).await?;\n\n    Ok(())\n}\n\n/// Start web server mode (alternative to Tauri GUI)\npub async fn start_web_mode(port: Option<u16>) -> Result<(), Box<dyn std::error::Error>> {\n    let port = port.unwrap_or(8080);\n\n    println!(\"🚀 Starting Opcode in web server mode...\");\n    create_web_server(port).await\n}\n"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "content": "{\n  \"$schema\": \"https://schema.tauri.app/config/2\",\n  \"productName\": \"opcode\",\n  \"version\": \"0.2.1\",\n  \"identifier\": \"opcode.asterisk.so\",\n  \"build\": {\n    \"beforeDevCommand\": \"\",\n    \"beforeBuildCommand\": \"bun run build\",\n    \"frontendDist\": \"../dist\"\n  },\n  \"app\": {\n    \"macOSPrivateApi\": true,\n    \"windows\": [\n      {\n        \"title\": \"opcode\",\n        \"width\": 800,\n        \"height\": 600,\n        \"decorations\": false,\n        \"transparent\": true,\n        \"shadow\": true,\n        \"center\": true,\n        \"resizable\": true,\n        \"alwaysOnTop\": false\n      }\n    ],\n    \"security\": {\n      \"csp\": \"default-src 'self'; img-src 'self' asset: https://asset.localhost blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval' https://app.posthog.com https://*.posthog.com https://*.i.posthog.com https://*.assets.i.posthog.com; connect-src 'self' ipc: https://ipc.localhost https://app.posthog.com https://*.posthog.com https://*.i.posthog.com\",\n      \"assetProtocol\": {\n        \"enable\": true,\n        \"scope\": [\n          \"**\"\n        ]\n      }\n    }\n  },\n  \"plugins\": {\n    \"fs\": {\n      \"scope\": [\n        \"$HOME/**\"\n      ],\n      \"allow\": [\n        \"readFile\",\n        \"writeFile\",\n        \"readDir\",\n        \"copyFile\",\n        \"createDir\",\n        \"removeDir\",\n        \"removeFile\",\n        \"renameFile\",\n        \"exists\"\n      ]\n    },\n    \"shell\": {\n      \"open\": true\n    }\n  },\n  \"bundle\": {\n    \"active\": true,\n    \"targets\": [\n      \"deb\",\n      \"rpm\",\n      \"appimage\",\n      \"app\",\n      \"dmg\"\n    ],\n    \"icon\": [\n      \"icons/32x32.png\",\n      \"icons/64x64.png\",\n      \"icons/128x128.png\",\n      \"icons/128x128@2x.png\",\n      \"icons/icon.png\",\n      \"icons/icon.ico\",\n      \"icons/icon.icns\"\n    ],\n    \"resources\": [],\n    \"externalBin\": [],\n    \"copyright\": \"© 2025 Asterisk. All rights reserved.\",\n    \"category\": \"DeveloperTool\",\n    \"shortDescription\": \"GUI app and Toolkit for Claude Code\",\n    \"longDescription\": \"opcode is a comprehensive GUI application and toolkit for working with Claude Code, providing an intuitive interface for AI-assisted development.\",\n    \"linux\": {\n      \"appimage\": {\n        \"bundleMediaFramework\": true\n      },\n      \"deb\": {\n        \"depends\": [\"libwebkit2gtk-4.1-0\", \"libgtk-3-0\"]\n      }\n    },\n    \"macOS\": {\n      \"frameworks\": [],\n      \"minimumSystemVersion\": \"10.15\",\n      \"exceptionDomain\": \"\",\n      \"signingIdentity\": null,\n      \"providerShortName\": null,\n      \"entitlements\": \"entitlements.plist\",\n      \"dmg\": {\n        \"windowSize\": { \"width\": 540, \"height\": 380 },\n        \"appPosition\": { \"x\": 140, \"y\": 200 },\n        \"applicationFolderPosition\": { \"x\": 400, \"y\": 200 }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src-tauri/tests/TESTS_COMPLETE.md",
    "content": "# Test Suite - Complete with Real Claude ✅\n\n## Final Status: All Tests Passing with Real Claude Commands\n\n### Key Changes from Original Task:\n\n1. **Replaced MockClaude with Real Claude Execution** ✅\n   - Removed all mock Claude implementations\n   - Tests now execute actual `claude` command with `--dangerously-skip-permissions`\n   - Added proper timeout handling for macOS/Linux compatibility\n\n2. **Real Claude Test Implementation** ✅\n   - Created `claude_real.rs` with helper functions for executing real Claude\n   - Tests use actual Claude CLI with test prompts\n   - Proper handling of stdout/stderr/exit codes\n\n3. **Test Suite Results:**\n```\ntest result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\n\n### Implementation Details:\n\n#### Real Claude Execution:\n- `execute_claude_task()` - Executes Claude with specified task and captures output\n- Supports timeout handling (gtimeout on macOS, timeout on Linux)\n- Returns structured output with stdout, stderr, exit code, and duration\n- Helper methods for checking operation results\n\n#### Test Tasks:\n- Simple, focused prompts that execute quickly\n- Example: \"Read the file ./test.txt in the current directory and show its contents\"\n- 20-second timeout to allow Claude sufficient time to respond\n\n#### Key Test Updates:\n1. **Agent Tests**:\n   - Test agent execution with various permission configurations\n   - Test agent execution in different project contexts\n   - Control tests for baseline behavior\n\n2. **Claude Tests**:\n   - Test Claude execution with default settings\n   - Test Claude execution with custom configurations\n\n### Benefits of Real Claude Testing:\n- **Authenticity**: Tests validate actual Claude behavior, not mocked responses\n- **Integration**: Ensures the system works with real Claude execution\n- **End-to-End**: Complete validation from command invocation to output parsing\n- **No External Dependencies**: Uses `--dangerously-skip-permissions` flag\n\n### Notes:\n- All tests use real Claude CLI commands\n- No ignored tests\n- No TODOs in test code\n- Clean compilation with no warnings\n- Platform-aware expectations for different operating systems\n\nThe test suite now provides comprehensive end-to-end validation with actual Claude execution.\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    /* Path aliases */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport tailwindcss from \"@tailwindcss/vite\";\nimport { fileURLToPath, URL } from \"node:url\";\n\nconst host = process.env.TAURI_DEV_HOST;\n\n// https://vitejs.dev/config/\nexport default defineConfig(async () => ({\n  plugins: [react(), tailwindcss()],\n\n  // Path resolution\n  resolve: {\n    alias: {\n      \"@\": fileURLToPath(new URL(\"./src\", import.meta.url)),\n    },\n  },\n\n  // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`\n  //\n  // 1. prevent vite from obscuring rust errors\n  clearScreen: false,\n  // 2. tauri expects a fixed port, fail if that port is not available\n  server: {\n    port: 1420,\n    strictPort: true,\n    host: host || false,\n    hmr: host\n      ? {\n          protocol: \"ws\",\n          host,\n          port: 1421,\n        }\n      : undefined,\n    watch: {\n      // 3. tell vite to ignore watching `src-tauri`\n      ignored: [\"**/src-tauri/**\"],\n    },\n  },\n\n  // Build configuration for code splitting\n  build: {\n    // Increase chunk size warning limit to 2000 KB\n    chunkSizeWarningLimit: 2000,\n    \n    rollupOptions: {\n      output: {\n        // Manual chunks for better code splitting\n        manualChunks: {\n          // Vendor chunks\n          'react-vendor': ['react', 'react-dom'],\n          'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu', '@radix-ui/react-select', '@radix-ui/react-tabs', '@radix-ui/react-tooltip', '@radix-ui/react-switch', '@radix-ui/react-popover'],\n          'editor-vendor': ['@uiw/react-md-editor'],\n          'syntax-vendor': ['react-syntax-highlighter'],\n          // Tauri and other utilities\n          'tauri': ['@tauri-apps/api', '@tauri-apps/plugin-dialog', '@tauri-apps/plugin-shell'],\n          'utils': ['date-fns', 'clsx', 'tailwind-merge'],\n        },\n      },\n    },\n  },\n}));\n"
  },
  {
    "path": "web_server.design.md",
    "content": "# Opcode Web Server Design\n\nThis document describes the implementation of Opcode's web server mode, which allows access to Claude Code from mobile devices and browsers while maintaining full functionality.\n\n## Overview\n\nThe web server provides a REST API and WebSocket interface that mirrors the Tauri desktop app's functionality, enabling phone/browser access to Claude Code sessions.\n\n## Architecture\n\n```\n┌─────────────────┐    WebSocket     ┌─────────────────┐    Process     ┌─────────────────┐\n│   Browser UI    │ ←──────────────→ │  Rust Backend   │ ────────────→  │  Claude Binary  │\n│                 │    REST API      │   (Axum Server) │               │                 │\n│ • React/TS      │ ←──────────────→ │                 │               │ • claude-code   │\n│ • WebSocket     │                  │ • Session Mgmt  │               │ • Subprocess    │\n│ • DOM Events    │                  │ • Process Spawn │               │ • Stream Output │\n└─────────────────┘                  └─────────────────┘               └─────────────────┘\n```\n\n## Key Components\n\n### 1. Rust Web Server (`src-tauri/src/web_server.rs`)\n\n**Main Functions:**\n- `create_web_server()` - Sets up Axum server with routes\n- `claude_websocket_handler()` - Manages WebSocket connections\n- `execute_claude_command()` / `continue_claude_command()` / `resume_claude_command()` - Execute Claude processes\n- `find_claude_binary_web()` - Locates Claude binary (bundled or system)\n\n**Key Features:**\n- **WebSocket Streaming**: Real-time output from Claude processes\n- **Session Management**: Tracks active WebSocket sessions\n- **Process Spawning**: Launches Claude subprocesses with proper arguments\n- **Comprehensive Logging**: Detailed trace output for debugging\n\n### 2. Frontend Event Handling (`src/components/ClaudeCodeSession.tsx`)\n\n**Dual Mode Support:**\n```typescript\nconst listen = tauriListen || ((eventName: string, callback: (event: any) => void) => {\n  // Web mode: Use DOM events\n  const domEventHandler = (event: any) => {\n    callback({ payload: event.detail });\n  };\n  window.addEventListener(eventName, domEventHandler);\n  return Promise.resolve(() => window.removeEventListener(eventName, domEventHandler));\n});\n```\n\n**Message Processing:**\n- Handles both string payloads (Tauri) and object payloads (Web)\n- Maintains compatibility with existing UI components\n- Comprehensive error handling and logging\n\n### 3. WebSocket Communication (`src/lib/apiAdapter.ts`)\n\n**Request Format:**\n```json\n{\n  \"command_type\": \"execute|continue|resume\",\n  \"project_path\": \"/path/to/project\",\n  \"prompt\": \"user prompt\",\n  \"model\": \"sonnet|opus\",\n  \"session_id\": \"uuid-for-resume\"\n}\n```\n\n**Response Format:**\n```json\n{\n  \"type\": \"start|output|completion|error\",\n  \"content\": \"parsed Claude message\",\n  \"message\": \"status message\",\n  \"status\": \"success|error\"\n}\n```\n\n## Message Flow\n\n### 1. Prompt Submission\n```\nBrowser → WebSocket Request → Rust Backend → Claude Process\n```\n\n### 2. Streaming Response\n```\nClaude Process → Rust Backend → WebSocket → Browser DOM Events → UI Update\n```\n\n### 3. Event Chain\n1. **User Input**: Prompt submitted via FloatingPromptInput\n2. **WebSocket Send**: JSON request sent to `/ws/claude`\n3. **Process Spawn**: Rust spawns `claude` subprocess\n4. **Stream Parse**: Stdout lines parsed and wrapped in JSON\n5. **Event Dispatch**: DOM events fired for `claude-output`\n6. **UI Update**: React components receive and display messages\n\n## File Structure\n\n```\nopcode/\n├── src-tauri/src/\n│   └── web_server.rs           # Main web server implementation\n├── src/\n│   ├── lib/\n│   │   └── apiAdapter.ts       # WebSocket client & environment detection\n│   └── components/\n│       ├── ClaudeCodeSession.tsx           # Main session component\n│       └── claude-code-session/\n│           └── useClaudeMessages.ts        # Alternative hook implementation\n└── justfile                    # Build configuration (just web)\n```\n\n## Build & Deployment\n\n### Development\n```bash\nnix-shell --run 'just web'\n# Builds frontend and starts Rust server on port 8080\n```\n\n### Production Considerations\n- **Binary Location**: Checks bundled binary first, falls back to system PATH\n- **CORS**: Configured for phone browser access\n- **Error Handling**: Comprehensive logging and graceful failures\n- **Session Cleanup**: Proper WebSocket session management\n\n## Debugging Features\n\n### Comprehensive Tracing\n- **Backend**: All WebSocket events, process spawning, and message forwarding\n- **Frontend**: Event setup, message parsing, and UI updates\n- **Process**: Claude binary execution and output streaming\n\n### Debug Output Examples\n```\n[TRACE] WebSocket handler started - session_id: uuid\n[TRACE] Successfully parsed request: {...}\n[TRACE] Claude process spawned successfully\n[TRACE] Forwarding message to WebSocket: {...}\n[TRACE] DOM event received: claude-output {...}\n[TRACE] handleStreamMessage - message type: assistant\n```\n\n## Key Fixes Implemented\n\n### 1. Event Handling Compatibility\n**Problem**: Original code only worked with Tauri events\n**Solution**: Enhanced `listen` function to support DOM events in web mode\n\n### 2. Message Format Mismatch  \n**Problem**: Backend sent JSON strings, frontend expected parsed objects\n**Solution**: Parse `content` field in WebSocket handler before dispatching events\n\n### 3. Process Integration\n**Problem**: Web mode lacked Claude binary execution\n**Solution**: Full subprocess spawning with proper argument passing and output streaming\n\n### 4. Session Management\n**Problem**: No state tracking for multiple concurrent sessions\n**Solution**: HashMap-based session tracking with proper cleanup\n\n### 5. Missing REST Endpoints\n**Problem**: Frontend expected cancel and output endpoints that didn't exist\n**Solution**: Added `/api/sessions/{sessionId}/cancel` and `/api/sessions/{sessionId}/output` endpoints\n\n### 6. Error Event Handling\n**Problem**: WebSocket errors and unexpected closures didn't dispatch UI events\n**Solution**: Added `claude-error` and `claude-complete` event dispatching for all error scenarios\n\n## Critical Issues Still Remaining\n\n### 1. Session-Scoped Event Dispatching (CRITICAL)\n**Problem**: The UI expects session-specific events like `claude-output:${sessionId}` but the backend only dispatches generic events like `claude-output`.\n\n**Current Backend Behavior**:\n```typescript\n// Only dispatches generic events\nwindow.dispatchEvent(new CustomEvent('claude-output', { detail: claudeMessage }));\nwindow.dispatchEvent(new CustomEvent('claude-complete', { detail: success }));\nwindow.dispatchEvent(new CustomEvent('claude-error', { detail: error }));\n```\n\n**Frontend Expectations**:\n```typescript\n// Expects session-scoped events\nawait listen(`claude-output:${sessionId}`, handleOutput);\nawait listen(`claude-error:${sessionId}`, handleError);\nawait listen(`claude-complete:${sessionId}`, handleComplete);\n```\n\n**Impact**: Session isolation doesn't work - all sessions receive all events.\n\n### 2. Process Management and Cancellation (CRITICAL)\n**Problem**: The cancel endpoint is just a stub that doesn't actually terminate running Claude processes.\n\n**Current Implementation**:\n```rust\nasync fn cancel_claude_execution(Path(sessionId): Path<String>) -> Json<ApiResponse<()>> {\n    // Just logs - doesn't actually cancel anything\n    println!(\"[TRACE] Cancel request for session: {}\", sessionId);\n    Json(ApiResponse::success(()))\n}\n```\n\n**Missing**:\n- Process tracking and storage in session state\n- Actual process termination via `kill()` or process handles\n- Proper cleanup of WebSocket sessions on cancellation\n- Session-specific process management\n\n### 3. Missing stderr Handling (MEDIUM)\n**Problem**: Claude processes can write errors to stderr, but the web server only captures stdout.\n\n**Current**: Only `child.stdout` is captured and streamed\n**Missing**: `child.stderr` capture and `claude-error` event emission\n\n### 4. Missing claude-cancelled Events (MEDIUM)\n**Problem**: The Tauri implementation emits `claude-cancelled` events but the web server doesn't.\n\n**Tauri Implementation**:\n```rust\nlet _ = app.emit(&format!(\"claude-cancelled:{}\", sid), true);\nlet _ = app.emit(\"claude-cancelled\", true);\n```\n\n**Web Server**: No `claude-cancelled` events are dispatched.\n\n### 5. WebSocket Session ID Mapping (MEDIUM)\n**Problem**: The web server generates its own session IDs but doesn't map them to the frontend's session IDs.\n\n**Current**: WebSocket handler creates `uuid::Uuid::new_v4().to_string()` but frontend passes `sessionId` in request.\n**Missing**: Proper session ID mapping and tracking.\n\n## Required Fixes for Full Functionality\n\n### Priority 1 (Critical - Breaks Core Functionality)\n\n1. **Session-Scoped Event Dispatching**\n   - Modify `apiAdapter.ts` to dispatch both generic and session-specific events\n   - Update WebSocket handler to use the frontend's sessionId instead of generating new ones\n   - Ensure events like `claude-output:${sessionId}` are dispatched correctly\n\n2. **Process Management and Cancellation**\n   - Add process handle storage to AppState\n   - Implement actual process termination in `cancel_claude_execution`\n   - Add proper cleanup on WebSocket disconnection\n\n### Priority 2 (High - Improves Reliability)\n\n3. **stderr Handling**\n   - Capture both stdout and stderr in Claude process execution\n   - Emit `claude-error` events for stderr content\n   - Properly handle process error states\n\n4. **claude-cancelled Events**\n   - Add `claude-cancelled` event dispatching for consistency with Tauri\n   - Implement proper cancellation flow matching desktop behavior\n\n### Priority 3 (Medium - Nice to Have)\n\n5. **Session ID Mapping**\n   - Use frontend-provided sessionId consistently\n   - Remove UUID generation in WebSocket handler\n   - Ensure session tracking works correctly\n\n## Implementation Notes\n\n### Session-Scoped Events Fix\nThe web server should dispatch both generic and session-specific events to match Tauri:\n```typescript\n// Both events should be dispatched\nwindow.dispatchEvent(new CustomEvent('claude-output', { detail: claudeMessage }));\nwindow.dispatchEvent(new CustomEvent(`claude-output:${sessionId}`, { detail: claudeMessage }));\n```\n\n### Process Management Fix\nThe AppState should track process handles:\n```rust\npub struct AppState {\n    pub active_sessions: Arc<Mutex<HashMap<String, tokio::sync::mpsc::Sender<String>>>>,\n    pub active_processes: Arc<Mutex<HashMap<String, tokio::process::Child>>>,\n}\n```\n\n## Performance Considerations\n\n- **Streaming**: Real-time output without buffering delays\n- **Memory**: Proper cleanup of completed sessions\n- **Concurrency**: Multiple WebSocket connections supported\n- **Error Recovery**: Graceful handling of process failures\n\n## Security Notes\n\n- **Binary Execution**: Uses `--dangerously-skip-permissions` flag for web mode\n- **CORS**: Allows all origins for development (should be restricted in production)\n- **Process Isolation**: Each session runs in separate subprocess\n- **Input Validation**: JSON parsing with error handling\n\n## Future Enhancements\n\n1. **Authentication**: Add user authentication for production deployment\n2. **Rate Limiting**: Prevent abuse of Claude API calls\n3. **Session Persistence**: Save/restore session state across reconnections\n4. **Mobile Optimization**: Enhanced UI for mobile browsers\n5. **Error Recovery**: Automatic reconnection on WebSocket failures\n6. **Process Monitoring**: Add process health checks and automatic restart\n7. **Concurrent Session Limits**: Limit number of concurrent Claude processes\n8. **File Management**: Add file upload/download capabilities for web mode\n9. **Advanced Logging**: Structured logging with log levels and rotation\n\n## Testing\n\n### Manual Testing\n1. Start web server: `nix-shell --run 'just web'`\n2. Open browser to `http://localhost:8080`\n3. Select project directory\n4. Send prompt and verify streaming response\n5. Check browser console for trace output\n\n### Debug Tools\n- **Browser DevTools**: WebSocket messages and console logs\n- **Server Logs**: Rust trace output for backend debugging\n- **Network Tab**: REST API calls and WebSocket traffic\n\n## Troubleshooting\n\n### Common Issues\n\n1. **No Claude Binary**: Check PATH or install Claude Code\n2. **WebSocket Errors**: Verify server is running and accessible\n3. **Event Not Received**: Check DOM event listeners in browser console\n4. **Process Spawn Failure**: Verify project path and permissions\n5. **Session Events Not Working**: Check if session-scoped events are being dispatched (critical issue)\n6. **Cancel Button Doesn't Work**: Process cancellation not implemented yet (critical issue)\n7. **Multiple Sessions Interfere**: Generic events cause cross-session interference\n8. **Errors Not Displayed**: stderr not captured, only stdout is shown\n\n### Debug Commands\n```bash\n# Check Claude binary\nwhich claude\n\n# Test WebSocket endpoint\ncurl -i -N -H \"Connection: Upgrade\" -H \"Upgrade: websocket\" \\\n  -H \"Sec-WebSocket-Key: test\" -H \"Sec-WebSocket-Version: 13\" \\\n  http://localhost:8080/ws/claude\n\n# Monitor server logs\ntail -f server.log  # if logging to file\n```\n\n## Current Status\n\nThe web server implementation provides **basic functionality** but has **critical issues** that prevent full feature parity with the Tauri desktop app:\n\n### ✅ Working Features\n- WebSocket-based Claude execution with streaming output\n- Basic session management and tracking\n- REST API endpoints for most functionality\n- Comprehensive debugging and tracing\n- Error handling for WebSocket failures\n- Basic process spawning and output capture\n\n### ❌ Critical Issues (Breaks Core Functionality)\n- **Session-scoped event dispatching**: Sessions interfere with each other\n- **Process cancellation**: Cancel button doesn't actually terminate processes\n- **stderr handling**: Error messages from Claude not displayed\n- **claude-cancelled events**: Missing cancellation event support\n\n### ⚠️ Current State\nThe web server is **functional for single-session use** but **not suitable for production** due to the session isolation issues. Multiple concurrent sessions will interfere with each other, and users cannot cancel running processes.\n\n### 🔧 Next Steps\n1. Fix session-scoped event dispatching (highest priority)\n2. Implement proper process management and cancellation\n3. Add stderr capture and error event emission\n4. Test with multiple concurrent sessions\n\nThis implementation successfully bridges the gap between Tauri desktop and web deployment, but requires the above fixes to achieve full feature parity while adapting to browser constraints."
  }
]