12Particles/Pivo/main 413k tokens More Tools
```
├── .claude/
   ├── settings.local.json
├── .github/
   ├── workflows/
      ├── release.yml (1400 tokens)
├── .gitignore
├── .vscode/
   ├── extensions.json
├── LICENSE (omitted)
├── README-zh.md (800 tokens)
├── README.md (1400 tokens)
├── assets/
   ├── logo.png
   ├── screenshot.jpg
├── docs/
   ├── ARCHITECTURE.md (500 tokens)
   ├── component-structure-plan.md (400 tokens)
   ├── migration/
      ├── phase1-foundation.md (900 tokens)
   ├── rfcs/
      ├── RFC-20250122-001-architecture-decision-records.md (1500 tokens)
      ├── RFC-20250122-002-appendix-code-examples.md (4.3k tokens)
      ├── RFC-20250122-002-frontend-architecture-refactoring.md (5.6k tokens)
      ├── RFC-20250124-001-message-processing-architecture.md (2.4k tokens)
      ├── RFC-20250131-architecture-cleanup.md (300 tokens)
      ├── RFC-20250131-architecture-validation.md (400 tokens)
      ├── RFC-20250131-complete-architecture-review.md (300 tokens)
      ├── RFC-20250131-final-assessment.md (500 tokens)
      ├── RFC-20250131-final-cleanup-summary.md (300 tokens)
      ├── RFC-20250131-implementation-gaps.md (500 tokens)
      ├── RFC-20250131-implementation-summary.md (400 tokens)
      ├── RFC-20250131-task-attempt-execution-architecture.md (1600 tokens)
      ├── RFC-20250131-third-review-findings.md (300 tokens)
      ├── concepts.md (100 tokens)
   ├── userinterface/
      ├── main.drawio.svg (178.5k tokens)
├── index.html (100 tokens)
├── log-viewer.html (100 tokens)
├── package-lock.json (omitted)
├── package.json (400 tokens)
├── pnpm-lock.yaml (omitted)
├── postcss.config.js
├── public/
   ├── tauri.svg (500 tokens)
   ├── vite.svg (300 tokens)
├── scripts/
   ├── notarize.sh (400 tokens)
   ├── update-execution-types.mjs (500 tokens)
├── src-tauri/
   ├── .gitignore
   ├── Cargo.lock (omitted)
   ├── Cargo.toml (300 tokens)
   ├── FIX_GIT_LFS_ENVIRONMENT.md (200 tokens)
   ├── build.rs
   ├── capabilities/
      ├── default.json (200 tokens)
      ├── project-window.json (200 tokens)
   ├── entitlements.plist (200 tokens)
   ├── icons/
      ├── 128x128.png
      ├── 128x128@2x.png
      ├── 32x32.png
      ├── 64x64.png
      ├── Square107x107Logo.png
      ├── Square142x142Logo.png
      ├── Square150x150Logo.png
      ├── Square284x284Logo.png
      ├── Square30x30Logo.png
      ├── Square310x310Logo.png
      ├── Square44x44Logo.png
      ├── Square71x71Logo.png
      ├── Square89x89Logo.png
      ├── StoreLogo.png
      ├── android/
         ├── mipmap-hdpi/
            ├── ic_launcher.png
            ├── ic_launcher_foreground.png
            ├── ic_launcher_round.png
         ├── mipmap-mdpi/
            ├── ic_launcher.png
            ├── ic_launcher_foreground.png
            ├── ic_launcher_round.png
         ├── mipmap-xhdpi/
            ├── ic_launcher.png
            ├── ic_launcher_foreground.png
            ├── ic_launcher_round.png
         ├── mipmap-xxhdpi/
            ├── ic_launcher.png
            ├── ic_launcher_foreground.png
            ├── ic_launcher_round.png
         ├── mipmap-xxxhdpi/
            ├── ic_launcher.png
            ├── ic_launcher_foreground.png
            ├── ic_launcher_round.png
      ├── icon.icns
      ├── icon.ico
      ├── icon.png
      ├── icon_backup.png
      ├── ios/
         ├── AppIcon-20x20@1x.png
         ├── AppIcon-20x20@2x-1.png
         ├── AppIcon-20x20@2x.png
         ├── AppIcon-20x20@3x.png
         ├── AppIcon-29x29@1x.png
         ├── AppIcon-29x29@2x-1.png
         ├── AppIcon-29x29@2x.png
         ├── AppIcon-29x29@3x.png
         ├── AppIcon-40x40@1x.png
         ├── AppIcon-40x40@2x-1.png
         ├── AppIcon-40x40@2x.png
         ├── AppIcon-40x40@3x.png
         ├── AppIcon-512@2x.png
         ├── AppIcon-60x60@2x.png
         ├── AppIcon-60x60@3x.png
         ├── AppIcon-76x76@1x.png
         ├── AppIcon-76x76@2x.png
         ├── AppIcon-83.5x83.5@2x.png
   ├── migrations/
      ├── 001_init.sql (500 tokens)
      ├── 002_fix_enum_values.sql (400 tokens)
      ├── 003_add_attempt_conversations.sql (200 tokens)
      ├── 20250121_add_git_tracking_fields.sql
      ├── 20250122_add_merge_requests_table.sql (300 tokens)
      ├── 20250123_add_git_provider_to_projects.sql (100 tokens)
      ├── 20250124_add_claude_session_id.sql
      ├── 20250128_add_last_opened_to_projects.sql (100 tokens)
      ├── 20250201_add_main_branch_to_projects.sql
   ├── src/
      ├── commands/
         ├── cli.rs (500 tokens)
         ├── command.rs (100 tokens)
         ├── dev_server.rs (2000 tokens)
         ├── filesystem.rs (800 tokens)
         ├── git.rs (1100 tokens)
         ├── git_info.rs
         ├── github.rs (3.1k tokens)
         ├── gitlab.rs (1300 tokens)
         ├── logging.rs (400 tokens)
         ├── mcp.rs (600 tokens)
         ├── mod.rs (100 tokens)
         ├── process.rs (200 tokens)
         ├── projects.rs (2.8k tokens)
         ├── system.rs (800 tokens)
         ├── task_attempts.rs (200 tokens)
         ├── task_commands.rs (2.8k tokens)
         ├── tasks.rs (500 tokens)
         ├── window.rs (300 tokens)
      ├── db/
         ├── mod.rs (500 tokens)
      ├── lib.rs (2.5k tokens)
      ├── logging.rs (400 tokens)
      ├── main.rs
      ├── menu.rs (1200 tokens)
      ├── models/
         ├── command.rs (100 tokens)
         ├── config.rs (300 tokens)
         ├── conversation.rs (100 tokens)
         ├── execution_process.rs (500 tokens)
         ├── git_diff.rs (500 tokens)
         ├── git_provider.rs (1100 tokens)
         ├── merge_request.rs (700 tokens)
         ├── mod.rs (100 tokens)
         ├── project.rs (500 tokens)
         ├── task.rs (600 tokens)
         ├── task_attempt.rs (500 tokens)
      ├── repository/
         ├── conversation_repository.rs (700 tokens)
         ├── database_repository.rs (100 tokens)
         ├── mod.rs
      ├── services/
         ├── ai_executor.rs (700 tokens)
         ├── coding_agent_executor/
            ├── agent.rs (200 tokens)
            ├── claude_agent.rs (4.6k tokens)
            ├── claude_converter.rs (1400 tokens)
            ├── gemini_agent.rs (1700 tokens)
            ├── gemini_converter.rs (600 tokens)
            ├── message.rs (800 tokens)
            ├── metadata.rs (700 tokens)
            ├── mod.rs (100 tokens)
            ├── service.rs (4.7k tokens)
            ├── stateful_claude_converter.rs (800 tokens)
            ├── types.rs (900 tokens)
         ├── command_service.rs (800 tokens)
         ├── config_service.rs (500 tokens)
         ├── file_watcher_service.rs (800 tokens)
         ├── git_info.rs (1000 tokens)
         ├── git_platform.rs (200 tokens)
         ├── git_service.rs (5.3k tokens)
         ├── github_service.rs (3k tokens)
         ├── gitlab_service.rs (2000 tokens)
         ├── mcp_server.rs (1500 tokens)
         ├── merge_request_service.rs (1300 tokens)
         ├── mod.rs (200 tokens)
         ├── process_service.rs (1800 tokens)
         ├── project_service.rs (1000 tokens)
         ├── task_service.rs (3.5k tokens)
         ├── vcs_sync_service.rs (2.4k tokens)
      ├── utils/
         ├── command.rs (700 tokens)
         ├── mod.rs
      ├── window_manager.rs (900 tokens)
   ├── tauri.conf.json (200 tokens)
├── src/
   ├── App.css (300 tokens)
   ├── App.tsx
   ├── app/
      ├── App.tsx
      ├── AppProviders.tsx (400 tokens)
      ├── AppRouter.tsx (300 tokens)
      ├── AppShell.tsx (100 tokens)
      ├── ThemeProvider.tsx
   ├── assets/
      ├── react.svg (800 tokens)
   ├── components/
      ├── ui/
         ├── alert.tsx (300 tokens)
         ├── badge.tsx (200 tokens)
         ├── button.tsx (300 tokens)
         ├── card.tsx (400 tokens)
         ├── checkbox.tsx (200 tokens)
         ├── confirm-dialog.tsx (300 tokens)
         ├── dialog.tsx (800 tokens)
         ├── diff-viewer.tsx (400 tokens)
         ├── dropdown-menu.tsx (1500 tokens)
         ├── enhanced-textarea.tsx (3k tokens)
         ├── error-dialog.tsx (200 tokens)
         ├── image-upload.tsx (1400 tokens)
         ├── input.tsx (200 tokens)
         ├── label.tsx (100 tokens)
         ├── scroll-area.tsx (300 tokens)
         ├── select.tsx (1100 tokens)
         ├── separator.tsx (100 tokens)
         ├── switch.tsx (200 tokens)
         ├── tabs.tsx (400 tokens)
         ├── textarea.tsx (200 tokens)
         ├── toast.tsx (1000 tokens)
         ├── toaster.tsx (200 tokens)
         ├── todo-list.tsx (400 tokens)
         ├── tooltip.tsx (200 tokens)
   ├── contexts/
      ├── AppContext.tsx (400 tokens)
      ├── ErrorContext.tsx (400 tokens)
      ├── LayoutContext.tsx (700 tokens)
      ├── SettingsContext.tsx (400 tokens)
      ├── SimpleLayoutContext.tsx (500 tokens)
   ├── features/
      ├── dev/
         ├── components/
            ├── DevPanel.tsx (2.2k tokens)
      ├── error/
         ├── components/
            ├── ErrorBoundary.tsx (800 tokens)
            ├── ErrorNotification.tsx (200 tokens)
      ├── integration/
         ├── components/
            ├── IntegrationPanel.tsx (1800 tokens)
      ├── layout/
         ├── components/
            ├── LayoutToggleButtons.tsx (700 tokens)
            ├── ProjectMainView.tsx (900 tokens)
            ├── ResizableLayout.tsx (400 tokens)
            ├── StableLayoutPanel.tsx (1100 tokens)
            ├── TitleBar.tsx (300 tokens)
      ├── logs/
         ├── components/
            ├── LogViewer.tsx (1200 tokens)
            ├── LogViewerPage.tsx (1700 tokens)
      ├── mcp/
         ├── components/
            ├── McpServerManager.tsx (2.1k tokens)
      ├── projects/
         ├── ProjectsView.tsx (900 tokens)
         ├── components/
            ├── ProjectList.tsx (1100 tokens)
            ├── ProjectSettingsDialog.tsx (1700 tokens)
            ├── ProjectSettingsPage.tsx (1800 tokens)
      ├── settings/
         ├── components/
            ├── GeneralSettings.tsx (700 tokens)
            ├── GitHubSettings.tsx (1500 tokens)
            ├── GitLabSettings.tsx (1000 tokens)
            ├── GitServicesSettings.tsx (200 tokens)
            ├── McpConfigManager.tsx (1200 tokens)
            ├── SettingsPage.tsx (700 tokens)
      ├── tasks/
         ├── TasksView.tsx (2.2k tokens)
         ├── conversation/
            ├── TaskConversation.tsx (1100 tokens)
            ├── components/
               ├── ConversationHeader.tsx (200 tokens)
               ├── MessageInput.tsx (1300 tokens)
               ├── MessageList.tsx (800 tokens)
               ├── MessageRenderer.tsx (400 tokens)
               ├── ToolUseDisplay.tsx (1100 tokens)
               ├── messages/
                  ├── AssistantMessage.tsx (600 tokens)
                  ├── ErrorMessage.tsx (200 tokens)
                  ├── MessageHeader.tsx (200 tokens)
                  ├── SystemMessage.tsx (200 tokens)
                  ├── ThinkingMessage.tsx (200 tokens)
                  ├── ToolResultMessage.tsx (900 tokens)
                  ├── ToolUseMessage.tsx (400 tokens)
                  ├── UserMessage.tsx (300 tokens)
                  ├── index.ts (100 tokens)
                  ├── tools/
                     ├── BashToolMessage.tsx (300 tokens)
                     ├── DefaultToolMessage.tsx (500 tokens)
                     ├── EditToolMessage.tsx (300 tokens)
                     ├── GlobToolMessage.tsx (600 tokens)
                     ├── GrepToolMessage.tsx (600 tokens)
                     ├── LSToolMessage.tsx (300 tokens)
                     ├── MultiEditToolMessage.tsx (400 tokens)
                     ├── ReadToolMessage.tsx (300 tokens)
                     ├── TodoWriteToolMessage.tsx (200 tokens)
                     ├── ToolMessageHeader.tsx (200 tokens)
                     ├── WriteToolMessage.tsx (700 tokens)
                  ├── types.ts (100 tokens)
               ├── renderers/
                  ├── BashResultRenderer.tsx (300 tokens)
                  ├── CodeBlockRenderer.tsx (200 tokens)
                  ├── ContentRenderer.tsx (500 tokens)
                  ├── CustomDiffRenderer.tsx (800 tokens)
                  ├── EditResultRenderer.tsx (1000 tokens)
                  ├── FileTreeRenderer.tsx (100 tokens)
                  ├── GlobResultRenderer.tsx (600 tokens)
                  ├── GrepResultRenderer.tsx (300 tokens)
                  ├── JsonRenderer.tsx (100 tokens)
                  ├── LSResultRenderer.tsx (300 tokens)
                  ├── MarkdownRenderer.tsx (800 tokens)
                  ├── README.md (200 tokens)
                  ├── TodoListRenderer.tsx (400 tokens)
                  ├── WriteResultRenderer.tsx (400 tokens)
                  ├── index.ts (100 tokens)
            ├── hooks/
               ├── useTaskCommand.ts (200 tokens)
               ├── useTaskConversationState.ts (1600 tokens)
            ├── index.ts (100 tokens)
            ├── types.ts (600 tokens)
            ├── types/
               ├── message-types.ts (600 tokens)
               ├── metadata.ts (300 tokens)
            ├── utils/
               ├── diffUtils.ts (1000 tokens)
               ├── messageIcons.tsx (500 tokens)
               ├── pathUtils.ts (700 tokens)
               ├── workTreePathUtils.ts (200 tokens)
         ├── details/
            ├── TaskDetailsPanel.tsx (2.6k tokens)
         ├── dialogs/
            ├── CreateTaskDialog.tsx (1500 tokens)
            ├── EditTaskDialog.tsx (1500 tokens)
         ├── hooks/
            ├── useTaskExecutionStatus.ts (600 tokens)
         ├── kanban/
            ├── TaskCard.tsx (1100 tokens)
            ├── TaskKanbanBoard.tsx (1600 tokens)
      ├── vcs/
         ├── components/
            ├── MergeRequestList.tsx (1600 tokens)
            ├── common/
               ├── CommentDialog.tsx (700 tokens)
               ├── CommentPanel.tsx (1100 tokens)
               ├── EnhancedDiffViewer.tsx (1800 tokens)
               ├── FileListItem.tsx (300 tokens)
               ├── FileTreeDiff.tsx (6.5k tokens)
               ├── GitStatusPanel.tsx (1900 tokens)
               ├── WorktreeManager.tsx (1300 tokens)
            ├── github/
               ├── CreatePullRequestDialog.tsx (1400 tokens)
               ├── GitHubAuthDialog.tsx (1800 tokens)
               ├── PullRequestList.tsx (2.6k tokens)
            ├── gitlab/
               ├── CreateMergeRequestDialog.tsx (1300 tokens)
               ├── PipelineViewer.tsx (1600 tokens)
   ├── hooks/
      ├── domain/
         ├── useVcs.ts (700 tokens)
      ├── infrastructure/
         ├── useAppInitialization.ts (900 tokens)
         ├── useEventBus.ts (400 tokens)
      ├── ui/
         ├── useCopyHandler.ts (500 tokens)
         ├── useGlobalKeyboardShortcuts.ts (300 tokens)
      ├── use-error-dialog.ts (100 tokens)
      ├── use-file-context-menu.tsx (1000 tokens)
      ├── use-toast.tsx (400 tokens)
      ├── useTheme.ts (400 tokens)
   ├── index.css (400 tokens)
   ├── lib/
      ├── api.ts (1500 tokens)
      ├── events/
         ├── EventBus.ts (1300 tokens)
         ├── EventTypes.ts (600 tokens)
         ├── index.ts
         ├── useEvent.ts (200 tokens)
      ├── file-operations.ts (200 tokens)
      ├── git-diff-parser.ts (700 tokens)
      ├── gitApi.ts (900 tokens)
      ├── gitUrlUtils.ts (500 tokens)
      ├── i18n.ts (100 tokens)
      ├── logger.ts (800 tokens)
      ├── path-utils.ts (100 tokens)
      ├── react-query.ts (300 tokens)
      ├── services/
         ├── githubService.ts (700 tokens)
         ├── gitlabService.ts (400 tokens)
      ├── types/
         ├── mergeRequest.ts (300 tokens)
      ├── utils.ts
   ├── locales/
      ├── en.json (3k tokens)
      ├── zh.json (2.1k tokens)
   ├── log-viewer.tsx (100 tokens)
   ├── main.tsx (100 tokens)
   ├── services/
      ├── api/
         ├── CommandApi.ts (100 tokens)
         ├── FileSystemApi.ts (200 tokens)
         ├── GitApi.ts (500 tokens)
         ├── GitHubApi.ts (700 tokens)
         ├── GitLabApi.ts (900 tokens)
         ├── LoggingApi.ts (700 tokens)
         ├── McpApi.ts (700 tokens)
         ├── ProcessApi.ts (300 tokens)
         ├── ProjectApi.ts (400 tokens)
         ├── TaskApi.ts (200 tokens)
         ├── TaskAttemptApi.ts (200 tokens)
         ├── WindowApi.ts (200 tokens)
         ├── index.ts (200 tokens)
      ├── clipboard.service.ts (200 tokens)
      ├── index.ts
   ├── styles/
      ├── resizable-layout.css (600 tokens)
   ├── types/
      ├── command.ts
      ├── comment.ts (100 tokens)
      ├── execution.ts (300 tokens)
      ├── index.ts (1200 tokens)
   ├── vite-env.d.ts (omitted)
├── tailwind.config.js (400 tokens)
├── tsconfig.json (100 tokens)
├── tsconfig.node.json
├── vite.config.ts (200 tokens)
```


## /.claude/settings.local.json

```json path="/.claude/settings.local.json" 
{
  "permissions": {
    "allow": [
      "Bash(pnpm tauri:*)",
      "Bash(git tag:*)",
      "Bash(git push:*)",
      "Bash(git add:*)"
    ],
    "deny": []
  }
}
```

## /.github/workflows/release.yml

```yml path="/.github/workflows/release.yml" 
name: Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

permissions:
  contents: write

jobs:
  build-macos:
    runs-on: macos-latest
    environment: APPLE_CERTIFICATE
    outputs:
      version: ${{ steps.get_version.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Get version
        id: get_version
        run: |
          if [[ "${{ github.ref }}" == refs/tags/* ]]; then
            VERSION=${GITHUB_REF#refs/tags/}
          else
            VERSION=$(grep '"version"' package.json | cut -d '"' -f 4)
            VERSION="v$VERSION"
          fi
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Building version: $VERSION"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-apple-darwin,x86_64-apple-darwin

      - name: Setup Rust cache
        uses: Swatinem/rust-cache@v2
        with:
          workspaces: './src-tauri -> target'

      - name: Install dependencies
        run: pnpm install


      - name: Import certificates
        env:
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
        run: |
          # Create variables
          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

          # Import certificate from secrets
          echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH

          # Create temporary keychain
          security create-keychain -p actions $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p actions $KEYCHAIN_PATH

          # Import certificate to keychain
          security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH
          
          # Verify certificate
          security find-identity -v -p codesigning

      - name: Build for Apple Silicon
        run: |
          # Build with proper signing
          pnpm tauri build --target aarch64-apple-darwin
          
          # Verify signature
          codesign -dvv src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Pivo.app
          
          # Check entitlements
          codesign -d --entitlements - src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Pivo.app

      - name: Build for Intel
        run: pnpm tauri build --target x86_64-apple-darwin

      - name: Build Universal Binary
        run: pnpm tauri build --target universal-apple-darwin

      - name: Notarize macOS app
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: "9WZQSGSX3A"
        run: |
          # Universal app path
          APP_PATH="src-tauri/target/universal-apple-darwin/release/bundle/macos/Pivo.app"
          
          # Create ZIP for notarization
          echo "Creating ZIP for notarization..."
          ditto -c -k --keepParent "$APP_PATH" "Pivo.zip"
          
          # Submit for notarization
          echo "Submitting for notarization..."
          xcrun notarytool submit "Pivo.zip" \
            --apple-id "$APPLE_ID" \
            --password "$APPLE_PASSWORD" \
            --team-id "$APPLE_TEAM_ID" \
            --wait
          
          # Staple the notarization ticket
          echo "Stapling notarization ticket..."
          xcrun stapler staple "$APP_PATH"
          
          # Clean up
          rm -f "Pivo.zip"

      - name: Create DMG
        run: |
          # Install create-dmg
          npm install -g create-dmg
          
          # Create DMG for universal binary
          cd src-tauri/target/universal-apple-darwin/release/bundle/macos
          create-dmg Pivo.app || true
          
          # Rename to include version
          VERSION="${{ steps.get_version.outputs.version }}"
          mv "Pivo "*.dmg "Pivo-${VERSION}-universal.dmg" || mv Pivo.dmg "Pivo-${VERSION}-universal.dmg"
          
          # Also create architecture-specific DMGs
          cd $GITHUB_WORKSPACE
          
          # ARM64 DMG
          cd src-tauri/target/aarch64-apple-darwin/release/bundle/macos
          create-dmg Pivo.app || true
          mv "Pivo "*.dmg "Pivo-${VERSION}-arm64.dmg" || mv Pivo.dmg "Pivo-${VERSION}-arm64.dmg"
          
          cd $GITHUB_WORKSPACE
          
          # x86_64 DMG
          cd src-tauri/target/x86_64-apple-darwin/release/bundle/macos
          create-dmg Pivo.app || true
          mv "Pivo "*.dmg "Pivo-${VERSION}-x64.dmg" || mv Pivo.dmg "Pivo-${VERSION}-x64.dmg"

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: macos-binaries
          path: |
            src-tauri/target/*/release/bundle/macos/*.dmg
          retention-days: 1

  create-release:
    needs: build-macos
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: macos-binaries
          path: ./artifacts

      - name: Create Release
        id: create_release
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ needs.build-macos.outputs.version }}
          name: Pivo ${{ needs.build-macos.outputs.version }}
          draft: true
          prerelease: false
          generate_release_notes: true
          files: |
            ./artifacts/**/*.dmg
          body: |
            ## Downloads

            ### macOS
            - **Universal** (Recommended): `Pivo-${{ needs.build-macos.outputs.version }}-universal.dmg` - Works on both Intel and Apple Silicon Macs
            - **Apple Silicon**: `Pivo-${{ needs.build-macos.outputs.version }}-arm64.dmg` - For M1/M2/M3 Macs only
            - **Intel**: `Pivo-${{ needs.build-macos.outputs.version }}-x64.dmg` - For Intel Macs only

            ### Installation
            1. Download the DMG file for your system
            2. Double-click to open the DMG
            3. Drag Pivo to your Applications folder
            4. On first launch, right-click Pivo and select "Open"

            ### System Requirements
            - macOS 10.15 (Catalina) or later
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## /.gitignore

```gitignore path="/.gitignore" 
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

```

## /.vscode/extensions.json

```json path="/.vscode/extensions.json" 
{
  "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

```

## /README-zh.md

# Pivo - 以任务为中心的 Vibe 编程环境

![License](https://img.shields.io/badge/License-MIT-blue.svg)
![Tauri](https://img.shields.io/badge/Tauri-2.x-orange.svg)
![React](https://img.shields.io/badge/React-18.x-blue.svg)
![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)
![Rust](https://img.shields.io/badge/Rust-1.x-red.svg)

[English](README.md) | [中文](README-zh.md)

> **以任务为中心的 Vibe 编程环境**

**Pivo** 基于Tauri、React和Rust构建,为基于Git的项目管理提供无缝体验,集成AI助手和终端功能。

[![Pivo 演示视频](https://img.youtube.com/vi/gA0qbPZwuJg/0.jpg)](https://youtu.be/gA0qbPZwuJg)

*观看演示视频了解任务管理、文件变更和AI对话功能*

## ✨ 主要特性

### 🎯 项目管理
- **Git集成**:原生Git仓库支持,自动项目检测
- **多项目工作区**:同时管理多个项目
- **分支管理**:高级Git工作树支持,实现任务隔离

### 📋 任务管理
- **看板**:可视化任务管理,支持拖拽操作
- **任务层级**:支持父子任务关系
- **状态跟踪**:全面的任务状态和优先级管理
- **任务尝试**:为每次任务尝试提供隔离执行环境

### 🤖 AI集成
- **Claude助手**:集成Claude AI提供智能任务协助
- **Gemini支持**:支持替代AI模型以适应不同工作流程
- **对话历史**:每个任务的持久化AI对话跟踪
- **上下文感知**:AI理解项目结构和任务上下文

### 🖥️ 终端集成
- **嵌入式终端**:基于xterm.js的内置终端
- **进程管理**:跟踪和管理运行中的进程
- **命令历史**:持久化命令执行历史
- **多会话**:支持多个终端会话

### 🔧 高级功能
- **MCP服务器支持**:模型控制协议,支持可扩展的AI能力
- **文件监控**:实时文件系统监控
- **差异查看器**:内置代码差异可视化
- **合并请求集成**:支持GitLab和GitHub集成
- **多语言**:支持中英文界面

## 🚀 快速开始

### 环境要求

- **Node.js** (v18或更高版本)
- **Rust** (最新稳定版)
- **pnpm** (推荐的包管理器)
- **Git**

#### 在macOS上安装

**安装Node.js:**
```bash
# 使用Homebrew
brew install node

# 或从nodejs.org下载
# https://nodejs.org/en/download/
```

**安装Rust:**
```bash
# 使用rustup (推荐)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 按照屏幕提示操作,然后重新加载shell
source $HOME/.cargo/env
```

**安装pnpm:**
```bash
# 使用npm
npm install -g pnpm

# 或使用Homebrew
brew install pnpm
```

### 安装步骤

1. **克隆仓库**
   ```bash
   git clone https://github.com/12Particles/Pivo.git
   cd pivo
   ```

2. **安装依赖**
   ```bash
   pnpm install
   ```

3. **安装Rust依赖**
   ```bash
   cd src-tauri
   cargo build
   cd ..
   ```

4. **运行开发模式**
   ```bash
   pnpm tauri dev
   ```

### 生产构建

```bash
# 构建应用程序
pnpm tauri build

# 构建的应用程序将在 src-tauri/target/release/bundle/ 目录中
```

## 🛠️ 开发指南

### 项目结构

```
pivo/
├── src/                    # React前端源码
│   ├── components/         # React组件
│   ├── lib/               # 工具库
│   ├── hooks/             # 自定义React钩子
│   ├── types/             # TypeScript类型定义
│   └── locales/           # 国际化翻译
├── src-tauri/             # Tauri/Rust后端
│   ├── src/               # Rust源码
│   ├── migrations/        # 数据库迁移
│   └── capabilities/      # Tauri能力配置
├── public/                # 静态资源
└── docs/                  # 文档
```

### 开发脚本

```bash
# 启动开发服务器
pnpm dev

# 仅构建前端
pnpm build

# 运行Tauri开发模式
pnpm tauri dev

# 构建Tauri应用程序
pnpm tauri build

# 运行测试(如果可用)
pnpm test

# 代码检查
pnpm lint
```

### 数据库

Pivo使用SQLite进行数据持久化。数据库在首次运行时自动初始化,包含以下表:

- `projects`:项目信息和Git仓库详情
- `tasks`:任务管理和层级结构
- `task_attempts`:具有隔离环境的任务执行尝试
- `execution_processes`:进程执行跟踪

### 配置

应用程序将配置存储在系统的应用数据目录中:

- **macOS**:`~/Library/Application Support/com.living.pivo/`
- **Windows**:`%APPDATA%\com.living.pivo\`
- **Linux**:`~/.local/share/com.living.pivo/`

## 🔧 集成

### Git平台集成

配置Git平台集成以支持合并请求:

- **GitLab**:具有`api`权限的个人访问令牌
- **GitHub**:具有`repo`权限的个人访问令牌

## 📖 使用方法

1. **创建项目**:选择Git仓库目录来创建新项目
2. **管理任务**:使用看板创建和组织任务
3. **AI协助**:点击任何任务开始AI对话获取指导
4. **执行任务**:使用集成终端或AI执行任务相关命令
5. **跟踪进度**:监控任务尝试和执行历史

## 社区
### 微信群

<img width="1080" height="1596" alt="image" src="https://github.com/user-attachments/assets/f22c7204-7ed0-4ee9-9ab5-7077757ff0d3" />

## 🤝 贡献

我们欢迎贡献!请查看我们的[贡献指南](CONTRIBUTING.md)了解详情。

### 开发设置

1. Fork仓库
2. 创建功能分支:`git checkout -b feature/amazing-feature`
3. 进行更改并彻底测试
4. 提交更改:`git commit -m 'Add amazing feature'`
5. 推送到分支:`git push origin feature/amazing-feature`
6. 打开Pull Request

## 📝 许可证

本项目采用MIT许可证 - 详情请参见[LICENSE](LICENSE)文件。

## 🙏 致谢

- [Tauri](https://tauri.app/) 提供优秀的桌面应用框架
- [Radix UI](https://www.radix-ui.com/) 提供无障碍UI组件
- [Anthropic Claude](https://www.anthropic.com/) 提供AI能力
- [xterm.js](https://xtermjs.org/) 提供终端模拟

## ⭐ Star历史

[![Star History Chart](https://api.star-history.com/svg?repos=12Particles/Pivo&type=Date)](https://star-history.com/#12Particles/Pivo&Date)

## 💬 支持

如果这个项目对你有帮助,请考虑在GitHub上给它一个⭐!

如需支持,请提交issue


## /README.md

# Pivo - Task-focused programming environment designed for the right vibe


![License](https://img.shields.io/badge/License-MIT-blue.svg)
![Tauri](https://img.shields.io/badge/Tauri-2.x-orange.svg)
![React](https://img.shields.io/badge/React-18.x-blue.svg)
![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)
![Rust](https://img.shields.io/badge/Rust-1.x-red.svg)

[English](README.md) | [中文](README-zh.md)

> **Task-focused programming environment designed for the right vibe.**

**Pivo** is built with Tauri, React, and Rust, providing a seamless experience for managing Git-based projects with integrated AI assistance and terminal capabilities.

[![Pivo Demo Video](https://img.youtube.com/vi/gA0qbPZwuJg/0.jpg)](https://youtu.be/gA0qbPZwuJg)

*Watch the demo video showing task management, file changes, and AI conversation features*

## ✨ Key Features

### 🎯 Project Management
- **Git Integration**: Native Git repository support with automatic project detection
- **Multi-Project Workspace**: Manage multiple projects simultaneously
- **Branch Management**: Advanced Git worktree support for task isolation

### 📋 Task Management
- **Kanban Board**: Visual task management with drag-and-drop functionality
- **Task Hierarchy**: Support for parent-child task relationships
- **Status Tracking**: Comprehensive task status and priority management
- **Task Attempts**: Isolated execution environments for each task attempt

### 🤖 AI Integration
- **Claude Assistant**: Integrated Claude AI for intelligent task assistance
- **Gemini Support**: Alternative AI model support for diverse workflows
- **Conversation History**: Persistent AI conversation tracking per task
- **Context-Aware**: AI understands project structure and task context

### 🖥️ Terminal Integration
- **Embedded Terminal**: Built-in terminal with xterm.js
- **Process Management**: Track and manage running processes
- **Command History**: Persistent command execution history
- **Multi-Session**: Support for multiple terminal sessions

### 🔧 Advanced Features
- **MCP Server Support**: Model Control Protocol for extensible AI capabilities
- **File Watching**: Real-time file system monitoring
- **Diff Viewer**: Built-in code diff visualization
- **Merge Request Integration**: GitLab and GitHub integration support
- **Multi-language**: English and Chinese interface support

## 🚀 Quick Start

### Prerequisites

- **Node.js** (v18 or higher)
- **Rust** (latest stable)
- **pnpm** (recommended package manager)
- **Git**

#### Installing on macOS

**Install Node.js:**
```bash
# Using Homebrew
brew install node

# Or download from nodejs.org
# https://nodejs.org/en/download/
```

**Install Rust:**
```bash
# Using rustup (recommended)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Follow the on-screen instructions, then reload your shell
source $HOME/.cargo/env
```

**Install pnpm:**
```bash
# Using npm
npm install -g pnpm

# Or using Homebrew
brew install pnpm
```

### Installation

1. **Clone the repository**
   ```bash
   git clone https://github.com/12Particles/Pivo.git
   cd pivo
   ```

2. **Install dependencies**
   ```bash
   pnpm install
   ```

3. **Install Rust dependencies**
   ```bash
   cd src-tauri
   cargo build
   cd ..
   ```

4. **Run in development mode**
   ```bash
   pnpm tauri dev
   ```

### Building for Production

```bash
# Build the application
pnpm tauri build

# The built application will be in src-tauri/target/release/bundle/
```

## 🛠️ Development

### Project Structure

```
pivo/
├── src/                    # React frontend source
│   ├── components/         # React components
│   ├── lib/               # Utility libraries
│   ├── hooks/             # Custom React hooks
│   ├── types/             # TypeScript type definitions
│   └── locales/           # i18n translations
├── src-tauri/             # Tauri/Rust backend
│   ├── src/               # Rust source code
│   ├── migrations/        # Database migrations
│   └── capabilities/      # Tauri capabilities
├── public/                # Static assets
└── docs/                  # Documentation
```

### Development Scripts

```bash
# Start development server
pnpm dev

# Build frontend only
pnpm build

# Run Tauri development mode
pnpm tauri dev

# Build Tauri application
pnpm tauri build

# Run tests (if available)
pnpm test

# Lint code
pnpm lint
```

### Database

Pivo uses SQLite for data persistence. The database is automatically initialized on first run with the following tables:

- `projects`: Project information and Git repository details
- `tasks`: Task management and hierarchy
- `task_attempts`: Task execution attempts with isolated environments
- `execution_processes`: Process execution tracking

### Configuration

The application stores configuration in the system's app data directory:

- **macOS**: `~/Library/Application Support/com.living.pivo/`
- **Windows**: `%APPDATA%\com.living.pivo\`
- **Linux**: `~/.local/share/com.living.pivo/`

## 🔧 Integration

### Git Platform Integration

Configure Git platform integrations for merge request support:

- **GitLab**: Personal access token with `api` scope
- **GitHub**: Personal access token with `repo` scope

## 📖 Usage

1. **Create a Project**: Select a Git repository directory to create a new project
2. **Manage Tasks**: Use the Kanban board to create and organize tasks
3. **AI Assistance**: Click on any task to start an AI conversation for guidance
4. **Execute Tasks**: Use the integrated terminal or AI to execute task-related commands
5. **Track Progress**: Monitor task attempts and execution history

## 🤝 Community

### Discord

Join our Discord community for discussions, support, and updates:

[![Discord](https://img.shields.io/discord/YOUR_SERVER_ID?label=Discord&logo=discord&logoColor=white)](https://discord.gg/DrpbENmX)

## 🤝 Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.

### Development Setup

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Make your changes and test thoroughly
4. Commit your changes: `git commit -m 'Add amazing feature'`
5. Push to the branch: `git push origin feature/amazing-feature`
6. Open a Pull Request

## 📝 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## 🙏 Acknowledgments

- [Tauri](https://tauri.app/) for the excellent desktop app framework
- [Radix UI](https://www.radix-ui.com/) for accessible UI components
- [Anthropic Claude](https://www.anthropic.com/) for AI capabilities
- [xterm.js](https://xtermjs.org/) for terminal emulation

## ⭐ Star History

[![Star History Chart](https://api.star-history.com/svg?repos=12Particles/Pivo&type=Date)](https://star-history.com/#12Particles/Pivo&Date)

## 💬 Support

If you find this project helpful, please consider giving it a ⭐ on GitHub!

For support, please open an issue.


## /assets/logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/assets/logo.png

## /assets/screenshot.jpg

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/assets/screenshot.jpg

## /docs/ARCHITECTURE.md

# Pivo Architecture

## Core Concepts and Relationships

### 1. Data Model Hierarchy

```
Application
    └── Multiple Windows (one per Project)
         └── Project (1)
              └── Tasks (n)
                   └── TaskAttempts (n) - but only 1 active at a time
                        └── CliExecutions (n) - but only 1 running at a time
                             └── Messages (n) - conversation history
```

### 2. Key Relationships

#### Project → Task (1:n)
- Each project contains multiple tasks
- Tasks belong to exactly one project
- Each project opens in a separate window

#### Task → TaskAttempt (1:n)
- Each task can have multiple attempts (history of work)
- Only ONE attempt can be active per task at any time
- Switching attempts means switching work context (git worktree, conversation history)

#### TaskAttempt → CliExecution (1:n)
- Each attempt can have multiple executions over time
- Only ONE execution can be running per attempt at any time
- Each execution represents one Claude/Gemini session
- Messages accumulate across executions within the same attempt

### 3. State Management Architecture

#### Backend (Rust)
- **CliExecutorService**: Manages all active executions
  - Keyed by `attempt_id` (since executions belong to attempts)
  - Enforces single execution per attempt
  - Maintains message history per attempt
  - Broadcasts state changes via events

#### Frontend (React)
- **ExecutionStore**: Central state management
  - Subscribes to backend events
  - Provides convenient APIs for components
  - Manages state at both task and attempt levels

### 4. Event Flow

1. User Action → Frontend Component → ExecutionStore
2. ExecutionStore → Tauri Command → Backend Service
3. Backend Service → State Update → Event Broadcast
4. Event → ExecutionStore → Component Re-render

### 5. Key Invariants

1. **Single Active Attempt**: A task can only have one active attempt
2. **Single Running Execution**: An attempt can only have one running execution
3. **Message Continuity**: Messages persist across executions within an attempt
4. **Worktree Isolation**: Each attempt has its own git worktree

### 6. Multi-Window Support

- Each project opens in a separate Tauri window
- Windows are independent but share the same backend services
- State updates are window-specific through targeted events

## /docs/component-structure-plan.md

# Component Directory Structure Reorganization Plan

## Current Issues
- Task-related components are mixed in tasks/ directory
- Version control components are scattered (git/, github/, gitlab/)
- No clear functional grouping

## Proposed New Structure

```
src/components/
├── tasks/                        # 任务管理模块
│   ├── kanban/                  # 看板功能
│   │   ├── TaskKanbanBoard.tsx
│   │   └── TaskCard.tsx
│   ├── conversation/            # 会话功能 (已存在)
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── utils/
│   │   └── types.ts
│   ├── dialogs/                 # 任务相关对话框
│   │   ├── CreateTaskDialog.tsx
│   │   └── EditTaskDialog.tsx
│   ├── details/                 # 任务详情
│   │   └── TaskDetailsPanel.tsx
│   └── TaskConversation.tsx     # 主会话组件
│
├── vcs/                         # 版本控制系统 (Version Control System)
│   ├── common/                  # 通用 Git 组件
│   │   ├── DiffViewer.tsx
│   │   ├── EnhancedDiffViewer.tsx
│   │   ├── FileTreeDiff.tsx
│   │   ├── GitStatusPanel.tsx
│   │   ├── WorktreeManager.tsx
│   │   ├── CommentDialog.tsx
│   │   └── CommentPanel.tsx
│   ├── github/                  # GitHub 特定功能
│   │   ├── GitHubAuthDialog.tsx
│   │   ├── CreatePullRequestDialog.tsx
│   │   └── PullRequestList.tsx
│   ├── gitlab/                  # GitLab 特定功能
│   │   ├── CreateMergeRequestDialog.tsx
│   │   └── PipelineViewer.tsx
│   └── MergeRequestList.tsx     # 通用 MR/PR 列表
│
├── projects/                    # 项目管理 (保持不变)
├── settings/                    # 设置管理 (保持不变)
├── ai/                         # AI 助手 (保持不变)
├── terminal/                   # 终端功能 (保持不变)
├── logs/                       # 日志查看 (保持不变)
├── mcp/                        # MCP 服务器管理 (保持不变)
├── integration/                # 集成面板 (保持不变)
├── layout/                     # 布局组件 (保持不变)
└── ui/                         # 通用 UI 组件 (保持不变)
```

## Benefits
1. Clear functional grouping
2. Better code organization
3. Easier to find components
4. More maintainable structure

## /docs/migration/phase1-foundation.md

# Phase 1: Foundation Migration Guide

## Overview

Phase 1 establishes the foundation for the new architecture by implementing:
1. Centralized EventBus for all events
2. ApiClient with retry logic and error handling
3. Error handling infrastructure
4. State machines for execution management
5. Refactored ExecutionStore

## Completed Components

### 1. EventBus System

**Location**: `src/lib/events/`
- `EventTypes.ts` - Type-safe event definitions
- `EventBus.ts` - Centralized event management

**Usage Example**:
```typescript
import { useEvent, useEventEmitter } from '@/hooks/infrastructure/useEventBus';

// Subscribe to events
useEvent('task-status-updated', (task) => {
  console.log('Task updated:', task);
});

// Emit events
const emit = useEventEmitter();
await emit('task-created', { task: newTask });
```

### 2. ApiClient with Retry Logic

**Location**: `src/lib/api/`
- `ApiClient.ts` - Base client with automatic retry
- `errors.ts` - Error types and handling

**New API Services**: `src/services/api/`
- `TaskApi.ts` - Task-related API calls
- `ProjectApi.ts` - Project-related API calls

**Usage Example**:
```typescript
import { taskApi } from '@/services/api/TaskApi';

// Automatic retry on failure
const task = await taskApi.create({
  title: 'New Task',
  project_id: projectId,
});
```

### 3. Error Handling Infrastructure

**Components**:
- `ErrorBoundary.tsx` - React error boundary
- `ErrorNotification.tsx` - Global error notifications
- `useErrorStore.ts` - Error state management

**Usage**:
```typescript
// Wrap app with error boundary
<ErrorBoundary>
  <App />
</ErrorBoundary>

// Errors automatically shown as toasts
// Manual error handling
const { addError } = useErrorStore();
addError(new Error('Something went wrong'), 'Context');
```

### 4. Execution State Machine

**Location**: `src/services/execution/`
- `ExecutionStateMachine.ts` - XState machine definition
- `ExecutionService.ts` - Service managing executions

**State Flow**:
```
idle → starting → running → completed
         ↓          ↓  ↓
       error    stopping
         ↓
      retry/failed
```

### 5. Refactored ExecutionStore

**New Store**: `useExecutionStoreV2.ts`
- Simplified state structure
- Uses ExecutionService internally
- Automatic event integration

## Migration Steps

### Step 1: Update Imports

Replace old API imports:
```typescript
// Old
import { taskApi } from '@/lib/api';

// New
import { taskApi } from '@/services/api/TaskApi';
```

### Step 2: Replace Event Listeners

Replace direct Tauri listeners:
```typescript
// Old
useEffect(() => {
  const unlisten = listen('task-updated', handler);
  return () => { unlisten.then(fn => fn()); };
}, []);

// New
useEvent('task-status-updated', handler);
```

### Step 3: Update ExecutionStore Usage

```typescript
// Old
import { useExecutionStore } from '@/stores/useExecutionStore';

// New
import { useExecutionStoreV2 } from '@/stores/domain/useExecutionStoreV2';

// Initialize in App.tsx
import { initExecutionStoreV2 } from '@/stores/domain/useExecutionStoreV2';

useEffect(() => {
  initExecutionStoreV2();
}, []);
```

### Step 4: Add Error Handling

Wrap your app:
```typescript
// App.tsx
import { ErrorBoundary } from '@/components/error/ErrorBoundary';
import { ErrorNotification } from '@/components/error/ErrorNotification';

function App() {
  return (
    <ErrorBoundary>
      <AppProviders>
        <ErrorNotification />
        {/* Your app content */}
      </AppProviders>
    </ErrorBoundary>
  );
}
```

## Breaking Changes

1. **API Client**: All API calls now use the new client with automatic retry
2. **Event Names**: Some event names have changed (see EventTypes.ts)
3. **ExecutionStore**: Completely new API, requires migration
4. **Error Handling**: Errors now emit events instead of direct handling

## Next Steps

Phase 2 will focus on:
1. Extracting business logic to hooks
2. Simplifying TaskConversation component
3. Creating feature modules
4. Implementing remaining domain stores

## Testing

Run tests to ensure migration is successful:
```bash
pnpm test
pnpm type-check
pnpm lint
```

## Rollback Plan

If issues arise:
1. Keep old implementations alongside new ones
2. Use feature flags to toggle between old/new
3. Gradual migration by component

## /docs/rfcs/RFC-20250122-001-architecture-decision-records.md

# Architecture Decision Records for RFC 0001

## ADR-001: State Management - Zustand over Redux

**Date**: 2025-01-22  
**Status**: Accepted

### Context
We need a state management solution that balances simplicity with power for our refactored architecture.

### Decision
We will use Zustand for state management instead of Redux Toolkit or MobX.

### Rationale
1. **Simplicity**: Zustand has minimal boilerplate compared to Redux
2. **TypeScript**: Excellent TypeScript support out of the box
3. **Performance**: Fine-grained subscriptions prevent unnecessary re-renders
4. **Size**: Much smaller bundle size (8KB vs Redux Toolkit's 40KB+)
5. **Learning Curve**: Team can be productive immediately

### Consequences
- ✅ Faster development with less boilerplate
- ✅ Better performance with selective subscriptions
- ✅ Easier testing with simple stores
- ❌ Less ecosystem support compared to Redux
- ❌ No time-travel debugging (acceptable trade-off)

---

## ADR-002: State Machines for Complex Flows

**Date**: 2025-01-22  
**Status**: Accepted

### Context
The execution flow for coding agents is complex with many possible states and transitions.

### Decision
Use XState for modeling execution lifecycle as state machines.

### Rationale
1. **Explicit States**: Makes all possible states visible and documented
2. **Invalid State Prevention**: Impossible to reach invalid states
3. **Visual Debugging**: Can visualize state machines
4. **Event-Driven**: Natural fit for our event-based architecture
5. **Testing**: Easier to test all state transitions

### Implementation
```typescript
// Only use for complex flows:
- Execution lifecycle (starting → running → completed/error)
- Multi-step wizards
- Complex UI interactions

// Don't use for:
- Simple boolean states
- Basic CRUD operations
- UI toggle states
```

---

## ADR-003: Repository Pattern for Data Access

**Date**: 2025-01-22  
**Status**: Accepted

### Context
Backend services directly execute SQL queries, creating tight coupling between business logic and data access.

### Decision
Implement Repository pattern to abstract data access from business logic.

### Rationale
1. **Testability**: Can mock repositories for unit testing
2. **Flexibility**: Can change database without affecting business logic
3. **Consistency**: Standardized data access patterns
4. **Caching**: Central place to implement caching strategies

### Example
```rust
// Domain layer defines interface
trait TaskRepository {
    async fn find_by_id(&self, id: &TaskId) -> Result<Option<Task>>;
    async fn save(&self, task: &Task) -> Result<()>;
}

// Infrastructure layer implements
struct SqliteTaskRepository { ... }
impl TaskRepository for SqliteTaskRepository { ... }
```

---

## ADR-004: Event-Driven Architecture

**Date**: 2025-01-22  
**Status**: Accepted

### Context
Current event handling is scattered across components with potential memory leaks and race conditions.

### Decision
Implement centralized EventBus for both frontend and backend with clear event contracts.

### Rationale
1. **Loose Coupling**: Components don't need to know about each other
2. **Scalability**: Easy to add new event handlers
3. **Debugging**: Central place to log all events
4. **Consistency**: Single pattern for all events

### Implementation Guidelines
```typescript
// Frontend
- All Tauri events go through EventBus
- Type-safe event definitions
- Automatic cleanup on unmount

// Backend
- Domain events for business logic
- Integration events for external systems
- Event sourcing for audit trail (future)
```

---

## ADR-005: API Client Abstraction

**Date**: 2025-01-22  
**Status**: Accepted

### Context
400+ lines of repetitive `invoke` calls in api.ts with no error handling or retry logic.

### Decision
Create ApiClient abstraction with automatic retry, error handling, and type generation.

### Benefits
1. **DRY**: No more repetitive invoke calls
2. **Reliability**: Automatic retry with exponential backoff
3. **Type Safety**: Generated types from Rust
4. **Monitoring**: Central place for API metrics
5. **Testing**: Easy to mock for tests

---

## ADR-006: Domain-Driven Design for Backend

**Date**: 2025-01-22  
**Status**: Accepted

### Context
Business logic is mixed with infrastructure concerns in service layer.

### Decision
Adopt DDD tactical patterns: Entities, Value Objects, Repositories, and Domain Services.

### Structure
```
domain/           # Pure business logic
├── entities/     # Core business objects
├── value_objects/# Immutable values
├── services/     # Domain logic
└── repositories/ # Interfaces only

application/      # Use cases
├── commands/     # Write operations
└── queries/      # Read operations

infrastructure/   # Technical details
├── persistence/  # Repository implementations
└── external/     # Third-party integrations
```

---

## ADR-007: Optimistic Updates Strategy

**Date**: 2025-01-22  
**Status**: Accepted

### Context
Current implementation waits for backend confirmation for all updates, creating sluggish UX.

### Decision
Implement optimistic updates for non-critical operations with rollback on failure.

### Guidelines
**Use optimistic updates for:**
- Task status changes
- UI preference changes
- Non-destructive operations

**Don't use for:**
- Financial transactions
- Destructive operations (delete)
- Operations with complex side effects

### Implementation
```typescript
const updateStatus = async (taskId, newStatus) => {
  const previous = getStatus(taskId);
  
  // Optimistic update
  setStatus(taskId, newStatus);
  
  try {
    await api.updateStatus(taskId, newStatus);
  } catch (error) {
    // Rollback
    setStatus(taskId, previous);
    throw error;
  }
};
```

---

## ADR-008: Error Handling Strategy

**Date**: 2025-01-22  
**Status**: Accepted

### Context
Inconsistent error handling leads to poor user experience and difficult debugging.

### Decision
Implement layered error handling with user-friendly messages and proper logging.

### Layers
1. **Domain Errors**: Business rule violations
2. **Application Errors**: Use case failures  
3. **Infrastructure Errors**: Technical failures
4. **API Errors**: Communication failures

### User Experience
```typescript
// Transform technical errors to user-friendly messages
function toUserError(error: Error): UserError {
  if (error instanceof NetworkError) {
    return {
      title: "Connection Problem",
      message: "Please check your internet connection",
      retryable: true
    };
  }
  // ... other mappings
}
```

---

## ADR-009: Virtual Scrolling for Performance

**Date**: 2025-01-22  
**Status**: Accepted

### Context
Large conversation histories cause performance issues with thousands of messages.

### Decision
Implement virtual scrolling for message lists using @tanstack/react-virtual.

### Thresholds
- Use virtual scrolling when messages > 100
- Render 5 items above/below viewport
- Estimated item height: 100px (adjusted dynamically)

---

## ADR-010: Type Generation Strategy

**Date**: 2025-01-22  
**Status**: Accepted

### Context
Manual TypeScript types drift from Rust types causing runtime errors.

### Decision
Generate TypeScript types from Rust using ts-rs or similar tool.

### Process
1. Mark Rust types with `#[derive(TS)]`
2. Generate types on build
3. Commit generated types to version control
4. CI validates types match

### Benefits
- Single source of truth
- Compile-time type checking
- Reduced manual work
- Prevents type drift

## /docs/rfcs/RFC-20250122-002-appendix-code-examples.md

# RFC 0001 Appendix: Code Examples

This appendix provides detailed code examples for the frontend architecture refactoring.

## 1. Complete Store Examples

### ProjectStore with Error Handling

```typescript
// stores/domain/useProjectStore.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { Project, CreateProjectRequest } from '@/types';
import { projectApi, gitInfoApi } from '@/lib/api';
import { logger } from '@/lib/logger';

interface ProjectStore {
  // State
  currentProject: Project | null;
  projects: Project[];
  loading: boolean;
  error: string | null;
  
  // Actions
  setCurrentProject: (project: Project | null) => void;
  loadProjects: () => Promise<void>;
  createProject: (data: CreateProjectRequest) => Promise<Project>;
  createProjectFromGitDir: () => Promise<Project | null>;
  updateProject: (id: string, data: UpdateProjectRequest) => Promise<void>;
  deleteProject: (id: string) => Promise<void>;
  refreshGitProviders: () => Promise<void>;
  
  // Selectors
  getProjectById: (id: string) => Project | undefined;
  hasProjects: () => boolean;
}

export const useProjectStore = create<ProjectStore>()(
  devtools(
    (set, get) => ({
      // Initial state
      currentProject: null,
      projects: [],
      loading: false,
      error: null,
      
      // Actions
      setCurrentProject: (project) => {
        set({ currentProject: project });
        if (project) {
          logger.info('Project selected', { 
            projectId: project.id, 
            projectName: project.name 
          });
        }
      },
      
      createProjectFromGitDir: async () => {
        const { open } = await import('@tauri-apps/plugin-dialog');
        
        try {
          const selected = await open({
            directory: true,
            multiple: false,
            title: "Select Git Project Directory",
          });

          if (!selected) return null;

          const gitInfo = await gitInfoApi.extractGitInfo(selected as string);
          
          if (!gitInfo.is_git_repo) {
            throw new Error('Selected directory is not a Git repository');
          }

          const pathParts = (selected as string).split("/");
          const projectName = pathParts[pathParts.length - 1] || "Untitled Project";

          const projectData: CreateProjectRequest = {
            name: projectName,
            description: gitInfo.current_branch ? 
              `Current branch: ${gitInfo.current_branch}${gitInfo.has_uncommitted_changes ? ' (uncommitted changes)' : ''}` 
              : '',
            path: selected as string,
            git_repo: gitInfo.remote_url,
          };

          return await get().createProject(projectData);
        } catch (error) {
          logger.error('Failed to create project from git directory', error);
          throw error;
        }
      },
      
      // ... other actions
      
      // Selectors
      getProjectById: (id) => {
        return get().projects.find(p => p.id === id);
      },
      
      hasProjects: () => {
        return get().projects.length > 0;
      }
    }),
    {
      name: 'project-store',
    }
  )
);
```

### TaskStore with Optimistic Updates

```typescript
// stores/domain/useTaskStore.ts
interface TaskStore {
  // ... previous definitions ...
  
  // Optimistic update support
  optimisticUpdateStatus: (id: string, status: TaskStatus) => void;
  revertOptimisticUpdate: (id: string, previousTask: Task) => void;
}

export const useTaskStore = create<TaskStore>((set, get) => ({
  // ... previous implementation ...
  
  optimisticUpdateStatus: (id, status) => {
    set((state) => ({
      tasks: state.tasks.map(t => 
        t.id === id ? { ...t, status } : t
      ),
      selectedTask: state.selectedTask?.id === id 
        ? { ...state.selectedTask, status } 
        : state.selectedTask
    }));
  },
  
  revertOptimisticUpdate: (id, previousTask) => {
    set((state) => ({
      tasks: state.tasks.map(t => 
        t.id === id ? previousTask : t
      ),
      selectedTask: state.selectedTask?.id === id 
        ? previousTask 
        : state.selectedTask
    }));
  },
  
  updateTaskStatus: async (id, status) => {
    const previousTask = get().tasks.find(t => t.id === id);
    if (!previousTask) throw new Error('Task not found');
    
    // Optimistic update
    get().optimisticUpdateStatus(id, status);
    
    try {
      const updatedTask = await taskApi.updateStatus(id, status);
      get().updateTaskInList(updatedTask);
      logger.info('Task status updated', { taskId: id, status });
      return updatedTask;
    } catch (error) {
      // Revert on error
      get().revertOptimisticUpdate(id, previousTask);
      const errorMsg = error instanceof Error ? error.message : String(error);
      logger.error('Failed to update task status', { error: errorMsg });
      throw error;
    }
  }
}));
```

## 2. Business Logic Hooks

### Complete Task Operations Hook

```typescript
// hooks/domain/useTaskOperations.ts
import { useCallback } from 'react';
import { useTaskStore } from '@/stores/domain/useTaskStore';
import { useProjectStore } from '@/stores/domain/useProjectStore';
import { useExecutionStore } from '@/stores/useExecutionStore';
import { useToast } from '@/hooks/use-toast';
import { gitApi } from '@/lib/api';
import { TaskStatus, CreateTaskRequest } from '@/types';
import { useTranslation } from 'react-i18next';

export function useTaskOperations() {
  const { t } = useTranslation();
  const { toast } = useToast();
  const { currentProject } = useProjectStore();
  const { 
    createTask, 
    updateTaskStatus, 
    deleteTask,
    selectedTask,
    selectTask 
  } = useTaskStore();
  
  const handleCreateTask = useCallback(async (
    data: CreateTaskRequest, 
    shouldStart?: boolean
  ) => {
    if (!currentProject) {
      throw new Error('No project selected');
    }
    
    try {
      const newTask = await createTask(data);
      
      if (shouldStart) {
        await updateTaskStatus(newTask.id, TaskStatus.Working);
        selectTask(newTask);
        
        // Trigger execution start via event
        window.dispatchEvent(new CustomEvent('start-task-execution', {
          detail: { taskId: newTask.id }
        }));
        
        toast({
          title: t('task.createTaskSuccess'),
          description: t('task.taskStarted'),
        });
      } else {
        toast({
          title: t('common.success'),
          description: t('task.createTaskSuccess'),
        });
      }
      
      // Auto-create worktree if git repo
      if (currentProject.git_repo) {
        try {
          await gitApi.createWorktree(
            currentProject.path, 
            newTask.id, 
            "main"
          );
          toast({
            title: t('common.success'),
            description: t('task.worktreeCreated'),
          });
        } catch (error) {
          console.error("Failed to create worktree:", error);
        }
      }
      
      return newTask;
    } catch (error) {
      toast({
        title: t('common.error'),
        description: `${t('task.createTaskError')}: ${error}`,
        variant: "destructive",
      });
      throw error;
    }
  }, [currentProject, createTask, updateTaskStatus, selectTask, toast, t]);
  
  const handleRunTask = useCallback(async (taskId: string) => {
    if (!currentProject) return;
    
    try {
      const task = await updateTaskStatus(taskId, TaskStatus.Working);
      selectTask(task);
      
      // Trigger execution
      window.dispatchEvent(new CustomEvent('start-task-execution', {
        detail: { taskId }
      }));
      
      toast({
        title: t('task.taskStarted'),
        description: t('task.interactWithAi'),
      });
    } catch (error) {
      toast({
        title: t('common.error'),
        description: `${t('task.runTaskError')}: ${error}`,
        variant: "destructive",
      });
    }
  }, [currentProject, updateTaskStatus, selectTask, toast, t]);
  
  const handleDeleteTask = useCallback(async (taskId: string) => {
    const task = selectedTask?.id === taskId ? selectedTask : null;
    const confirmed = window.confirm(
      t('task.deleteConfirm', { title: task?.title || 'this task' })
    );
    
    if (!confirmed) return;
    
    try {
      await deleteTask(taskId);
      toast({
        title: t('common.success'),
        description: t('task.taskDeleted'),
      });
    } catch (error) {
      toast({
        title: t('common.error'),
        description: `${t('task.deleteTaskError')}: ${error}`,
        variant: "destructive",
      });
    }
  }, [selectedTask, deleteTask, toast, t]);
  
  return {
    handleCreateTask,
    handleRunTask,
    handleDeleteTask,
  };
}
```

### Keyboard Shortcuts Hook

```typescript
// hooks/ui/useKeyboardShortcuts.ts
import { useEffect } from 'react';
import { useLayoutStore } from '@/stores/ui/useLayoutStore';
import { invoke } from '@tauri-apps/api/core';
import { useToast } from '@/hooks/use-toast';

interface ShortcutConfig {
  key: string;
  ctrlOrCmd?: boolean;
  shift?: boolean;
  alt?: boolean;
  action: () => void;
  description: string;
}

export function useKeyboardShortcuts() {
  const { togglePanel } = useLayoutStore();
  const { toast } = useToast();
  
  const shortcuts: ShortcutConfig[] = [
    {
      key: 'b',
      ctrlOrCmd: true,
      action: () => togglePanel('left'),
      description: 'Toggle left panel'
    },
    {
      key: 'j',
      ctrlOrCmd: true,
      action: () => togglePanel('bottom'),
      description: 'Toggle bottom panel'
    },
    {
      key: 'k',
      ctrlOrCmd: true,
      action: () => togglePanel('right'),
      description: 'Toggle right panel'
    },
    {
      key: 'l',
      ctrlOrCmd: true,
      action: async () => {
        try {
          await invoke('show_log_viewer');
        } catch (error) {
          toast({
            title: 'Error',
            description: 'Failed to open log viewer',
            variant: 'destructive',
          });
        }
      },
      description: 'Open log viewer'
    }
  ];
  
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      const shortcut = shortcuts.find(s => {
        const keyMatch = e.key.toLowerCase() === s.key;
        const ctrlMatch = s.ctrlOrCmd ? (e.metaKey || e.ctrlKey) : true;
        const shiftMatch = s.shift ? e.shiftKey : !e.shiftKey;
        const altMatch = s.alt ? e.altKey : !e.altKey;
        
        return keyMatch && ctrlMatch && shiftMatch && altMatch;
      });
      
      if (shortcut) {
        e.preventDefault();
        shortcut.action();
      }
    };
    
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [shortcuts]);
  
  return { shortcuts };
}
```

## 3. Event System Implementation

### Complete EventBus with Type Safety

```typescript
// stores/infrastructure/useEventBus.ts
import { create } from 'zustand';
import { listen, Event as TauriEvent, emit } from '@tauri-apps/api/event';

type EventHandler<T = any> = (payload: T) => void;
type UnlistenFn = () => void;

interface EventBusStore {
  listeners: Map<string, Set<EventHandler>>;
  tauriUnlisteners: Map<string, UnlistenFn>;
  
  // Subscribe to events
  subscribe: <T>(event: string, handler: EventHandler<T>) => UnlistenFn;
  
  // Subscribe to Tauri events
  subscribeTauri: <T>(event: string, handler: EventHandler<T>) => Promise<UnlistenFn>;
  
  // Emit events
  emit: <T>(event: string, payload: T) => Promise<void>;
  
  // Cleanup
  cleanup: () => void;
}

export const useEventBus = create<EventBusStore>((set, get) => ({
  listeners: new Map(),
  tauriUnlisteners: new Map(),
  
  subscribe: (event, handler) => {
    const { listeners } = get();
    
    if (!listeners.has(event)) {
      listeners.set(event, new Set());
    }
    
    listeners.get(event)!.add(handler);
    
    // Return unsubscribe function
    return () => {
      const handlers = listeners.get(event);
      if (handlers) {
        handlers.delete(handler);
        if (handlers.size === 0) {
          listeners.delete(event);
        }
      }
    };
  },
  
  subscribeTauri: async (event, handler) => {
    const { tauriUnlisteners } = get();
    
    // Unsubscribe previous listener if exists
    const existingUnlisten = tauriUnlisteners.get(event);
    if (existingUnlisten) {
      existingUnlisten();
    }
    
    // Create new listener
    const unlisten = await listen<any>(event, (e: TauriEvent<any>) => {
      handler(e.payload);
    });
    
    tauriUnlisteners.set(event, unlisten);
    
    // Also subscribe locally
    const unsubscribe = get().subscribe(event, handler);
    
    // Return combined unsubscribe
    return () => {
      unlisten();
      unsubscribe();
      tauriUnlisteners.delete(event);
    };
  },
  
  emit: async (event, payload) => {
    // Emit to local listeners
    const { listeners } = get();
    const handlers = listeners.get(event);
    
    if (handlers) {
      handlers.forEach(handler => handler(payload));
    }
    
    // Also emit as Tauri event
    await emit(event, payload);
  },
  
  cleanup: () => {
    const { tauriUnlisteners, listeners } = get();
    
    // Cleanup Tauri listeners
    tauriUnlisteners.forEach(unlisten => unlisten());
    tauriUnlisteners.clear();
    
    // Clear local listeners
    listeners.clear();
    
    set({ listeners: new Map(), tauriUnlisteners: new Map() });
  }
}));

// Typed event definitions
export interface AppEvents {
  'task-status-updated': Task;
  'coding-agent-message': { execution_id: string; message: UnifiedMessage };
  'menu-settings': void;
  'menu-view-logs': void;
  'project-selected': { projectId: string };
  'start-task-execution': { taskId: string };
}

// Type-safe event bus hooks
export function useAppEvent<K extends keyof AppEvents>(
  event: K,
  handler: (payload: AppEvents[K]) => void
) {
  const { subscribeTauri, cleanup } = useEventBus();
  
  useEffect(() => {
    let unsubscribe: UnlistenFn | null = null;
    
    subscribeTauri(event, handler).then(unsub => {
      unsubscribe = unsub;
    });
    
    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, [event, handler]);
}
```

## 4. Feature Module Examples

### TasksView Feature Module

```typescript
// features/tasks/TasksView.tsx
import { useEffect } from 'react';
import { ResizableLayout } from '@/components/layout/ResizableLayout';
import { TaskKanbanBoard } from '@/components/tasks/kanban/TaskKanbanBoard';
import { TaskDetailsPanel } from '@/components/tasks/details/TaskDetailsPanel';
import { TaskConversation } from '@/components/tasks/TaskConversation';
import { useProjectStore } from '@/stores/domain/useProjectStore';
import { useTaskStore } from '@/stores/domain/useTaskStore';
import { useLayoutStore } from '@/stores/ui/useLayoutStore';
import { useTaskOperations } from '@/hooks/domain/useTaskOperations';
import { useKeyboardShortcuts } from '@/hooks/ui/useKeyboardShortcuts';

export function TasksView() {
  const { currentProject } = useProjectStore();
  const { tasks, selectedTask, loading, loadTasks } = useTaskStore();
  const { 
    leftPanelVisible, 
    rightPanelVisible, 
    bottomPanelVisible,
    leftPanelSize,
    centerPanelSize,
    rightPanelSize
  } = useLayoutStore();
  
  const { handleCreateTask, handleRunTask, handleDeleteTask } = useTaskOperations();
  
  // Initialize keyboard shortcuts
  useKeyboardShortcuts();
  
  // Load tasks when project changes
  useEffect(() => {
    if (currentProject) {
      loadTasks(currentProject.id);
    }
  }, [currentProject, loadTasks]);
  
  if (!currentProject) {
    return null;
  }
  
  return (
    <ResizableLayout
      direction="horizontal"
      defaultSizes={[leftPanelSize, centerPanelSize, rightPanelSize]}
      minSizes={[15, 30, 20]}
      maxSizes={[30, 100, 40]}
      storageKey="main-layout"
    >
      {/* Left Panel - Task Kanban */}
      {leftPanelVisible && (
        <TaskKanbanBoard
          tasks={tasks}
          loading={loading}
          onCreateTask={() => handleCreateTask({})}
          onRunTask={handleRunTask}
          onDeleteTask={handleDeleteTask}
        />
      )}
      
      {/* Center Panel - Task Details */}
      <TaskDetailsPanel
        task={selectedTask}
        project={currentProject}
        bottomPanelVisible={bottomPanelVisible}
      />
      
      {/* Right Panel - Task Conversation */}
      {rightPanelVisible && (
        <TaskConversation
          task={selectedTask}
          project={currentProject}
        />
      )}
    </ResizableLayout>
  );
}
```

### Minimal App.tsx

```typescript
// app/App.tsx
import { AppProviders } from './AppProviders';
import { AppShell } from './AppShell';

function App() {
  return (
    <AppProviders>
      <AppShell />
    </AppProviders>
  );
}

export default App;
```

### AppShell with Navigation

```typescript
// app/AppShell.tsx
import { useEffect } from 'react';
import { ProjectsView } from '@/features/projects/ProjectsView';
import { TasksView } from '@/features/tasks/TasksView';
import { SettingsView } from '@/features/settings/SettingsView';
import { useProjectStore } from '@/stores/domain/useProjectStore';
import { useSettingsStore } from '@/stores/ui/useSettingsStore';
import { useTauriEvents } from '@/hooks/infrastructure/useTauriEvents';
import { useLogger } from '@/hooks/infrastructure/useLogger';

export function AppShell() {
  const { currentProject } = useProjectStore();
  const { showSettings } = useSettingsStore();
  const { initLogger } = useLogger();
  
  // Initialize infrastructure
  useTauriEvents();
  
  useEffect(() => {
    initLogger();
  }, [initLogger]);
  
  // Settings view takes precedence
  if (showSettings) {
    return <SettingsView />;
  }
  
  // Project selection view
  if (!currentProject) {
    return <ProjectsView />;
  }
  
  // Main tasks view
  return <TasksView />;
}
```

## 5. Testing Examples

### Store Testing

```typescript
// __tests__/stores/domain/useTaskStore.test.ts
import { renderHook, act } from '@testing-library/react';
import { useTaskStore } from '@/stores/domain/useTaskStore';
import { taskApi } from '@/lib/api';

jest.mock('@/lib/api');

describe('useTaskStore', () => {
  beforeEach(() => {
    useTaskStore.setState({
      tasks: [],
      selectedTask: null,
      loading: false,
      error: null
    });
  });
  
  it('should load tasks', async () => {
    const mockTasks = [
      { id: '1', title: 'Task 1', status: 'backlog' },
      { id: '2', title: 'Task 2', status: 'working' }
    ];
    
    (taskApi.list as jest.Mock).mockResolvedValue(mockTasks);
    
    const { result } = renderHook(() => useTaskStore());
    
    await act(async () => {
      await result.current.loadTasks('project-1');
    });
    
    expect(result.current.tasks).toEqual(mockTasks);
    expect(result.current.loading).toBe(false);
  });
  
  it('should handle optimistic updates', async () => {
    const initialTask = { id: '1', title: 'Task 1', status: 'backlog' as const };
    useTaskStore.setState({ tasks: [initialTask] });
    
    const { result } = renderHook(() => useTaskStore());
    
    act(() => {
      result.current.optimisticUpdateStatus('1', 'working');
    });
    
    expect(result.current.tasks[0].status).toBe('working');
  });
});
```

### Hook Testing

```typescript
// __tests__/hooks/domain/useTaskOperations.test.ts
import { renderHook, act } from '@testing-library/react';
import { useTaskOperations } from '@/hooks/domain/useTaskOperations';
import { useTaskStore } from '@/stores/domain/useTaskStore';
import { useProjectStore } from '@/stores/domain/useProjectStore';

jest.mock('@/stores/domain/useTaskStore');
jest.mock('@/stores/domain/useProjectStore');

describe('useTaskOperations', () => {
  it('should create task with auto-start', async () => {
    const mockCreateTask = jest.fn().mockResolvedValue({ id: '1', title: 'New Task' });
    const mockUpdateStatus = jest.fn();
    
    (useTaskStore as jest.Mock).mockReturnValue({
      createTask: mockCreateTask,
      updateTaskStatus: mockUpdateStatus,
      selectTask: jest.fn()
    });
    
    (useProjectStore as jest.Mock).mockReturnValue({
      currentProject: { id: 'project-1', name: 'Test Project' }
    });
    
    const { result } = renderHook(() => useTaskOperations());
    
    await act(async () => {
      await result.current.handleCreateTask(
        { title: 'New Task', project_id: 'project-1' },
        true // shouldStart
      );
    });
    
    expect(mockCreateTask).toHaveBeenCalled();
    expect(mockUpdateStatus).toHaveBeenCalledWith('1', 'working');
  });
});
```

## Summary

This refactoring provides:

1. **Clear separation of concerns** with distinct layers
2. **Type-safe event system** with defined event types
3. **Testable architecture** with mockable dependencies
4. **Scalable structure** that can grow with the application
5. **Better developer experience** with intuitive organization

The migration can be done incrementally, starting with the store layer and gradually moving business logic out of components into hooks.

## /docs/rfcs/RFC-20250122-002-frontend-architecture-refactoring.md

# RFC 0001: Comprehensive Architecture Refactoring

**Status**: Draft  
**Date**: 2025-01-22  
**Author**: Architecture Team  

## Summary

This RFC proposes a comprehensive refactoring of the entire Pivo application architecture, addressing issues across frontend, backend, and integration layers. While the initial focus was on the monolithic `App.tsx` (680+ lines), deeper analysis reveals systemic architectural issues that require a holistic approach to create a truly scalable and maintainable application.

## Motivation

### Current Problems

#### Frontend Issues

1. **Monolithic Components**
   - `App.tsx`: 680+ lines mixing routing, state, UI logic, and business logic
   - `TaskConversation.tsx`: Complex component with imperative handles and mixed responsibilities
   - Components directly managing execution state, API calls, and UI rendering

2. **State Management Fragmentation**
   - ExecutionStore using complex Map-based structures difficult to debug
   - Local state scattered across components without clear ownership
   - Manual synchronization between attempt executions and task summaries
   - No proper state machines for execution lifecycle

3. **Event System Issues**
   - Tauri events handled in multiple places without centralization
   - Memory leaks from unmanaged event listeners
   - Race conditions in execution state management
   - Inconsistent event handling patterns

#### Backend Issues

1. **Service Layer Problems**
   - Fat services (e.g., TaskService handles tasks, attempts, and conversations)
   - Mixed concerns (database ops, business logic, git operations in services)
   - No repository pattern - direct SQL in service layer
   - No clear domain boundaries

2. **Event Emission**
   - Services directly emit events, coupling backend to UI
   - No event bus or mediator pattern
   - Complex state synchronization between backend and frontend

3. **Data Persistence**
   - JSON columns for complex data instead of proper relations
   - Ad-hoc migrations without versioning
   - Database schema tightly coupled to domain models

#### Integration Issues

1. **API Layer**
   - 400+ lines of repetitive invoke calls in `api.ts`
   - No request/response abstraction
   - Manual type definitions that can drift from backend

2. **Type System**
   - Duplicate type definitions across layers
   - String-based enums instead of proper TypeScript enums
   - Loose typing with `any` in critical places

3. **Error Handling**
   - Inconsistent error handling patterns
   - Silent failures in some operations
   - No retry logic for failed operations

## Proposed Architecture

### High-Level Overview

```
Application Architecture
├── Frontend Layer
│   ├── App Shell          (Bootstrap, providers, routing)
│   ├── State Management   (Domain stores, UI stores, infrastructure)
│   ├── Business Logic     (Hooks, services, orchestration)
│   ├── Feature Modules    (Self-contained feature areas)
│   └── Infrastructure     (API client, event bus, utilities)
│
├── Integration Layer
│   ├── Type Contracts     (Shared types, generated from backend)
│   ├── API Gateway        (Abstracted Tauri commands)
│   ├── Event Bus          (Bidirectional event system)
│   └── Error Handling     (Retry, circuit breaker patterns)
│
└── Backend Layer
    ├── Command Layer      (Thin command handlers)
    ├── Service Layer      (Business logic, orchestration)
    ├── Domain Layer       (Core business entities)
    ├── Repository Layer   (Data access abstraction)
    └── Infrastructure     (Events, logging, cross-cutting)
```

### Directory Structure

#### Frontend Structure
```
src/
├── app/                          # Application core
│   ├── App.tsx                   # Root component (minimal)
│   ├── AppProviders.tsx          # Provider composition
│   ├── AppShell.tsx              # Layout shell
│   └── AppRouter.tsx             # Route management
│
├── stores/                       # State management (Zustand)
│   ├── domain/                   # Business domain stores
│   │   ├── useProjectStore.ts
│   │   ├── useTaskStore.ts
│   │   ├── useExecutionStore.ts  # Refactored with state machines
│   │   └── useAttemptStore.ts
│   │
│   ├── ui/                       # UI state stores
│   │   ├── useLayoutStore.ts
│   │   ├── useDialogStore.ts
│   │   └── usePreferenceStore.ts
│   │
│   └── infrastructure/           # Infrastructure stores
│       ├── useEventBus.ts        # Centralized event management
│       ├── useApiClient.ts       # API abstraction layer
│       └── useErrorHandler.ts    # Global error handling
│
├── services/                     # Frontend services
│   ├── execution/                # Execution management
│   │   ├── ExecutionService.ts   # Orchestrates execution lifecycle
│   │   ├── ExecutionStateMachine.ts
│   │   └── MessageProcessor.ts   # Processes unified messages
│   │
│   └── api/                      # API services
│       ├── ApiClient.ts          # Base API client with retry
│       ├── TaskApi.ts            # Task-specific API calls
│       └── ProjectApi.ts         # Project-specific API calls
│
├── hooks/                        # Business logic hooks
│   ├── domain/
│   │   ├── useProjectManagement.ts
│   │   ├── useTaskOperations.ts
│   │   ├── useExecutionControl.ts # Uses ExecutionService
│   │   └── useConversation.ts    # Replaces complex conversation hooks
│   │
│   └── infrastructure/
│       ├── useTauriEvents.ts     # Centralized event subscriptions
│       ├── useKeyboardShortcuts.ts
│       └── useLogger.ts
│
├── features/                     # Feature modules
│   ├── projects/
│   │   ├── ProjectsView.tsx
│   │   ├── hooks/
│   │   └── components/
│   │
│   ├── tasks/
│   │   ├── TasksView.tsx
│   │   ├── TaskConversation.tsx  # Simplified, uses hooks
│   │   ├── hooks/
│   │   └── components/
│   │
│   └── settings/
│       ├── SettingsView.tsx
│       └── components/
│
├── lib/                          # Core utilities
│   ├── api/                      # API layer (refactored)
│   │   ├── client.ts             # Base Tauri client
│   │   ├── types.ts              # Generated from backend
│   │   └── endpoints/            # Organized endpoints
│   ├── events/                   # Event system
│   │   ├── EventBus.ts
│   │   ├── EventTypes.ts
│   │   └── handlers/
│   └── utils/
│       ├── logger.ts
│       └── errors.ts
```

#### Backend Structure
```
src-tauri/src/
├── commands/                     # Thin command layer
│   ├── mod.rs
│   ├── project_commands.rs       # Delegates to services
│   ├── task_commands.rs
│   └── execution_commands.rs
│
├── domain/                       # Core business logic
│   ├── mod.rs
│   ├── entities/                 # Domain entities
│   │   ├── project.rs
│   │   ├── task.rs
│   │   ├── attempt.rs
│   │   └── execution.rs
│   ├── value_objects/            # Value objects
│   │   ├── task_status.rs
│   │   └── agent_type.rs
│   └── services/                 # Domain services
│       ├── task_service.rs       # Business logic only
│       └── execution_service.rs
│
├── application/                  # Application services
│   ├── mod.rs
│   ├── project_app_service.rs    # Orchestration
│   ├── task_app_service.rs
│   └── execution_app_service.rs
│
├── infrastructure/               # Infrastructure layer
│   ├── mod.rs
│   ├── persistence/              # Data access
│   │   ├── repositories/         # Repository pattern
│   │   │   ├── project_repository.rs
│   │   │   ├── task_repository.rs
│   │   │   └── attempt_repository.rs
│   │   ├── migrations/           # Versioned migrations
│   │   └── db.rs                 # Database setup
│   ├── events/                   # Event system
│   │   ├── event_bus.rs          # Central event dispatcher
│   │   ├── event_types.rs
│   │   └── handlers/
│   └── external/                 # External integrations
│       ├── git_service.rs
│       └── ai_agents/
│           ├── claude_agent.rs
│           └── gemini_agent.rs
│
└── shared/                       # Shared types
    ├── mod.rs
    └── types.rs                  # Types shared with frontend
```

## Detailed Design

### 1. Frontend Architecture

#### State Management with State Machines

Replace the complex Map-based ExecutionStore with state machines:

```typescript
// services/execution/ExecutionStateMachine.ts
import { createMachine, interpret } from 'xstate';

export const executionMachine = createMachine({
  id: 'execution',
  initial: 'idle',
  context: {
    attemptId: null,
    executionId: null,
    messages: [],
    error: null,
  },
  states: {
    idle: {
      on: {
        START: {
          target: 'starting',
          actions: 'setAttemptContext'
        }
      }
    },
    starting: {
      invoke: {
        src: 'startExecution',
        onDone: {
          target: 'running',
          actions: 'setExecutionId'
        },
        onError: {
          target: 'error',
          actions: 'setError'
        }
      }
    },
    running: {
      on: {
        MESSAGE: {
          actions: 'addMessage'
        },
        STOP: {
          target: 'stopping'
        },
        COMPLETE: {
          target: 'completed'
        },
        ERROR: {
          target: 'error',
          actions: 'setError'
        }
      }
    },
    stopping: {
      invoke: {
        src: 'stopExecution',
        onDone: 'idle',
        onError: 'error'
      }
    },
    completed: {
      type: 'final'
    },
    error: {
      on: {
        RETRY: 'starting',
        RESET: 'idle'
      }
    }
  }
});
```

#### Simplified Store Layer

```typescript
// stores/domain/useExecutionStore.ts (refactored)
import { create } from 'zustand';
import { ExecutionService } from '@/services/execution/ExecutionService';

interface ExecutionStore {
  // Simplified state
  activeExecutions: Map<string, ExecutionState>; // attemptId -> state
  
  // Actions delegated to service
  startExecution: (attemptId: string, config: ExecutionConfig) => Promise<void>;
  stopExecution: (attemptId: string) => Promise<void>;
  sendMessage: (attemptId: string, message: string, images?: string[]) => Promise<void>;
  
  // Selectors
  getExecutionState: (attemptId: string) => ExecutionState | undefined;
  isExecutionRunning: (attemptId: string) => boolean;
}

export const useExecutionStore = create<ExecutionStore>((set, get) => {
  const executionService = new ExecutionService();
  
  return {
    activeExecutions: new Map(),
    
    startExecution: async (attemptId, config) => {
      const machine = executionService.createExecution(attemptId, config);
      
      machine.subscribe((state) => {
        set((store) => {
          const newMap = new Map(store.activeExecutions);
          newMap.set(attemptId, state.context);
          return { activeExecutions: newMap };
        });
      });
      
      machine.send('START');
    },
    
    // ... other actions
  };
});
```

#### UI Stores
Manage UI-specific state:

```typescript
// stores/ui/useLayoutStore.ts
interface LayoutStore {
  leftPanelVisible: boolean;
  rightPanelVisible: boolean;
  bottomPanelVisible: boolean;
  
  togglePanel: (panel: 'left' | 'right' | 'bottom') => void;
  resetLayout: () => void;
}
```

#### Simplified Component Architecture

```typescript
// features/tasks/TaskConversation.tsx (refactored)
export function TaskConversation({ task, project }: TaskConversationProps) {
  const { currentAttempt, messages, sendMessage } = useConversation(task.id);
  const { isRunning, startExecution, stopExecution } = useExecutionControl(currentAttempt?.id);
  
  // No imperative handles, no complex state management
  // Just declarative UI based on hooks
  
  return (
    <ConversationContainer>
      <ConversationHeader 
        isRunning={isRunning}
        onStop={stopExecution}
      />
      <MessageList messages={messages} />
      <MessageInput 
        onSend={sendMessage}
        disabled={!currentAttempt || isRunning}
      />
    </ConversationContainer>
  );
}
```

### 2. Backend Architecture

#### Domain-Driven Design with Clean Architecture

```rust
// domain/entities/task.rs
#[derive(Debug, Clone)]
pub struct Task {
    id: TaskId,
    project_id: ProjectId,
    title: String,
    description: Option<String>,
    status: TaskStatus,
    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,
}

impl Task {
    pub fn new(project_id: ProjectId, title: String) -> Self {
        Self {
            id: TaskId::new(),
            project_id,
            title,
            description: None,
            status: TaskStatus::Backlog,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        }
    }
    
    pub fn update_status(&mut self, status: TaskStatus) -> Result<(), DomainError> {
        // Business logic for status transitions
        match (&self.status, &status) {
            (TaskStatus::Done, _) => Err(DomainError::InvalidTransition),
            _ => {
                self.status = status;
                self.updated_at = Utc::now();
                Ok(())
            }
        }
    }
}
```

#### Repository Pattern

```rust
// domain/repositories/task_repository.rs
#[async_trait]
pub trait TaskRepository: Send + Sync {
    async fn find_by_id(&self, id: &TaskId) -> Result<Option<Task>, RepositoryError>;
    async fn find_by_project(&self, project_id: &ProjectId) -> Result<Vec<Task>, RepositoryError>;
    async fn save(&self, task: &Task) -> Result<(), RepositoryError>;
    async fn delete(&self, id: &TaskId) -> Result<(), RepositoryError>;
}

// infrastructure/persistence/repositories/task_repository_impl.rs
pub struct SqliteTaskRepository {
    pool: Arc<SqlitePool>,
}

#[async_trait]
impl TaskRepository for SqliteTaskRepository {
    async fn find_by_id(&self, id: &TaskId) -> Result<Option<Task>, RepositoryError> {
        // Implementation with proper mapping
    }
}
```

#### Application Services

```rust
// application/task_app_service.rs
pub struct TaskApplicationService {
    task_repo: Arc<dyn TaskRepository>,
    event_bus: Arc<EventBus>,
    tx_manager: Arc<TransactionManager>,
}

impl TaskApplicationService {
    pub async fn create_task(
        &self,
        command: CreateTaskCommand,
    ) -> Result<TaskDto, ApplicationError> {
        self.tx_manager.transaction(async |tx| {
            // Create domain entity
            let task = Task::new(command.project_id, command.title);
            
            // Save through repository
            self.task_repo.save(&task).await?;
            
            // Emit domain event
            self.event_bus.publish(TaskCreated {
                task_id: task.id.clone(),
                project_id: task.project_id.clone(),
            }).await?;
            
            Ok(TaskDto::from(task))
        }).await
    }
}
```

### 3. Integration Layer

#### Type Generation

```typescript
// lib/api/types.ts (generated from Rust types)
export interface Task {
  id: string;
  projectId: string;
  title: string;
  description?: string;
  status: TaskStatus;
  createdAt: string;
  updatedAt: string;
}

export enum TaskStatus {
  Backlog = 'backlog',
  Working = 'working',
  Reviewing = 'reviewing',
  Done = 'done',
}
```

#### API Client with Retry Logic

```typescript
// services/api/ApiClient.ts
export class ApiClient {
  private retryCount = 3;
  private retryDelay = 1000;
  
  async invoke<T>(command: string, args?: any): Promise<T> {
    return this.withRetry(async () => {
      try {
        return await invoke<T>(command, args);
      } catch (error) {
        throw new ApiError(command, error);
      }
    });
  }
  
  private async withRetry<T>(fn: () => Promise<T>): Promise<T> {
    let lastError: Error;
    
    for (let i = 0; i < this.retryCount; i++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error as Error;
        if (i < this.retryCount - 1) {
          await this.delay(this.retryDelay * Math.pow(2, i));
        }
      }
    }
    
    throw lastError!;
  }
}
```

#### Centralized Event Bus

```typescript
// lib/events/EventBus.ts
export class EventBus {
  private listeners = new Map<string, Set<EventHandler>>();
  private tauriUnlisteners = new Map<string, UnlistenFn>();
  
  async subscribe<T extends keyof AppEvents>(
    event: T,
    handler: (payload: AppEvents[T]) => void
  ): Promise<UnsubscribeFn> {
    // Subscribe to Tauri events
    const unlisten = await listen<AppEvents[T]>(event, (e) => {
      handler(e.payload);
    });
    
    this.tauriUnlisteners.set(event, unlisten);
    
    return () => {
      unlisten();
      this.tauriUnlisteners.delete(event);
    };
  }
  
  async emit<T extends keyof AppEvents>(
    event: T,
    payload: AppEvents[T]
  ): Promise<void> {
    await emit(event, payload);
  }
}
```

### 4. Hook Layer Architecture

Simplified hooks that delegate to services:

```typescript
// hooks/domain/useProjectManagement.ts
export function useProjectManagement() {
  const { currentProject, setCurrentProject } = useProjectStore();
  const { resetTasks } = useTaskStore();
  const { showToast } = useToastNotifications();
  
  const selectProject = useCallback(async (project: Project) => {
    setCurrentProject(project);
    await resetTasks();
    await loadTasksForProject(project.id);
    showToast({ title: 'Project selected', description: project.name });
  }, []);
  
  return { currentProject, selectProject };
}
```

### 5. Error Handling Strategy

```typescript
// lib/errors/ErrorBoundary.tsx
export class ErrorBoundary extends React.Component<Props, State> {
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Log to backend
    logger.error('React error boundary', { error, errorInfo });
    
    // Show user-friendly error
    this.setState({
      hasError: true,
      error: normalizeError(error)
    });
  }
}

// lib/errors/handlers.ts
export function handleApiError(error: unknown): UserFacingError {
  if (error instanceof ApiError) {
    return {
      title: 'Operation failed',
      message: getErrorMessage(error),
      retryable: isRetryable(error)
    };
  }
  
  return {
    title: 'Unexpected error',
    message: 'Please try again later',
    retryable: true
  };
}
```

```typescript
// stores/infrastructure/useEventBus.ts
class EventBus {
  private listeners = new Map<string, Set<Function>>();
  private tauriUnlisteners = new Map<string, Function>();
  
  async subscribe(event: string, handler: Function) {
    // Subscribe to Tauri event
    const unlisten = await listen(event, (e) => handler(e.payload));
    this.tauriUnlisteners.set(event, unlisten);
  }
  
  cleanup() {
    this.tauriUnlisteners.forEach(unlisten => unlisten());
  }
}
```

### 6. Performance Optimizations

#### Virtual Scrolling for Messages

```typescript
// components/conversation/VirtualMessageList.tsx
import { VirtualList } from '@tanstack/react-virtual';

export function VirtualMessageList({ messages }: Props) {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const virtualizer = useVirtualizer({
    count: messages.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 100,
    overscan: 5,
  });
  
  return (
    <div ref={parentRef} className="h-full overflow-auto">
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <Message message={messages[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}
```

#### Optimistic Updates

```typescript
// hooks/domain/useTaskOperations.ts
export function useTaskOperations() {
  const { updateTaskStatus, revertTaskStatus } = useTaskStore();
  
  const updateStatus = async (taskId: string, newStatus: TaskStatus) => {
    const previousStatus = getTaskStatus(taskId);
    
    // Optimistic update
    updateTaskStatus(taskId, newStatus);
    
    try {
      await taskApi.updateStatus(taskId, newStatus);
    } catch (error) {
      // Revert on failure
      revertTaskStatus(taskId, previousStatus);
      throw error;
    }
  };
}
```

```typescript
// app/App.tsx
function App() {
  return (
    <AppProviders>
      <AppShell />
    </AppProviders>
  );
}

// app/AppProviders.tsx
function AppProviders({ children }: PropsWithChildren) {
  return (
    <QueryClientProvider client={queryClient}>
      <TauriEventProvider>
        <ToastProvider>
          {children}
        </ToastProvider>
      </TauriEventProvider>
    </QueryClientProvider>
  );
}

// app/AppShell.tsx
function AppShell() {
  const { currentProject } = useProjectStore();
  
  if (!currentProject) {
    return <ProjectsView />;
  }
  
  return <TasksView />;
}
```

## Migration Strategy

### Phase 1: Foundation (Week 1-2)
1. **Event System & API Layer**
   - Implement centralized EventBus
   - Create ApiClient with retry logic
   - Generate TypeScript types from Rust
   - Set up error handling infrastructure

2. **State Management Refactoring**
   - Implement state machines for execution flow
   - Create domain stores (Project, Task)
   - Refactor ExecutionStore with simplified structure
   - Add UI stores (Layout, Dialog)

### Phase 2: Frontend Refactoring (Week 3-4)
1. **Component Decomposition**
   - Extract business logic to hooks
   - Simplify TaskConversation component
   - Create feature modules structure
   - Refactor App.tsx to minimal shell

2. **Service Layer**
   - Implement ExecutionService
   - Create MessageProcessor for unified messages
   - Add conversation management service

### Phase 3: Backend Refactoring (Week 5-6)
1. **Domain Layer**
   - Extract domain entities from models
   - Implement domain services
   - Add business rule validation

2. **Repository Pattern**
   - Create repository interfaces
   - Implement SQLite repositories
   - Add transaction management

3. **Application Services**
   - Refactor fat services into focused services
   - Implement command/query separation
   - Add proper error handling

### Phase 4: Integration & Testing (Week 7-8)
1. **Integration Layer**
   - Connect new backend services to commands
   - Update event emission to use EventBus
   - Implement end-to-end type safety

2. **Testing & Performance**
   - Add unit tests for all layers
   - Integration tests for critical paths
   - Performance testing and optimization
   - Memory leak detection and fixes

### Phase 5: Rollout (Week 9)
1. **Gradual Migration**
   - Feature flag new architecture
   - Migrate one feature at a time
   - Monitor performance and errors
   - Complete migration and cleanup

## Benefits

### Technical Benefits

1. **Maintainability**
   - Clear separation of concerns across all layers
   - Single responsibility principle enforced
   - Easier debugging with state machines
   - Centralized error handling

2. **Scalability**
   - Domain-driven design allows business growth
   - Modular architecture supports team scaling
   - Clear boundaries for microservice extraction
   - Event-driven architecture enables loose coupling

3. **Performance**
   - Virtual scrolling for large message lists
   - Optimistic updates for better UX
   - Efficient state updates with Zustand
   - Reduced re-renders with proper memoization
   - Background processing with Web Workers (future)

4. **Reliability**
   - Retry logic for network failures
   - State machines prevent invalid states
   - Proper error boundaries
   - Transaction support for data integrity

### Developer Experience

1. **Code Organization**
   - Intuitive directory structure
   - Clear naming conventions
   - Consistent patterns across codebase

2. **Type Safety**
   - End-to-end type safety
   - Generated types from backend
   - Compile-time error detection

3. **Testing**
   - Testable architecture at all layers
   - Mockable dependencies
   - Integration test support

4. **Documentation**
   - Self-documenting code structure
   - Clear architectural boundaries
   - Reduced onboarding time

## Risks and Mitigations

### Risk 1: Large-Scale Refactoring Disruption
- **Impact**: Development velocity slowdown, potential bugs
- **Mitigation**:
  - Feature flags for gradual rollout
  - Parallel development tracks
  - Comprehensive test suite before migration
  - Incremental migration by feature

### Risk 2: State Management Complexity
- **Impact**: State machines might be overkill for simple flows
- **Mitigation**:
  - Use state machines only for complex flows (execution)
  - Simple Zustand stores for basic state
  - Clear guidelines on when to use each approach

### Risk 3: Backend Breaking Changes
- **Impact**: Frontend-backend contract violations
- **Mitigation**:
  - Implement versioned APIs
  - Generate types from Rust
  - Integration tests for all endpoints
  - Gradual deprecation of old endpoints

### Risk 4: Performance Degradation
- **Impact**: Slower app performance during migration
- **Mitigation**:
  - Performance benchmarks before/after
  - Profiling at each phase
  - Optimization budget per feature
  - Rollback plan if metrics degrade

### Risk 5: Team Resistance
- **Impact**: Slow adoption, inconsistent implementation
- **Mitigation**:
  - Team workshops on new patterns
  - Pair programming during migration
  - Clear documentation and examples
  - Champions for each architectural layer

## Alternatives Considered

### 1. Redux Toolkit
- **Pros**: Mature, time-travel debugging
- **Cons**: More boilerplate, steeper learning curve
- **Decision**: Zustand is simpler and sufficient for our needs

### 2. MobX
- **Pros**: Reactive, less boilerplate
- **Cons**: Magic behavior, harder to debug
- **Decision**: Zustand is more explicit and predictable

### 3. Minimal Refactoring
- **Pros**: Less risk, faster
- **Cons**: Doesn't solve core issues
- **Decision**: Full refactoring needed for long-term maintainability

## Implementation Checklist

### Phase 1: Foundation
- [ ] **Event System**
  - [ ] Implement EventBus class
  - [ ] Define AppEvents type interface
  - [ ] Create event handlers structure
  - [ ] Add cleanup mechanisms

- [ ] **API Layer**
  - [ ] Create ApiClient with retry logic
  - [ ] Set up type generation from Rust
  - [ ] Implement error handling
  - [ ] Add request/response logging

- [ ] **State Management**
  - [ ] Install XState for state machines
  - [ ] Create execution state machine
  - [ ] Refactor ExecutionStore
  - [ ] Implement domain stores

### Phase 2: Frontend
- [ ] **Services**
  - [ ] ExecutionService implementation
  - [ ] MessageProcessor service
  - [ ] ConversationService

- [ ] **Components**
  - [ ] Simplify TaskConversation
  - [ ] Extract business logic to hooks
  - [ ] Create AppShell
  - [ ] Refactor App.tsx

### Phase 3: Backend
- [ ] **Domain Layer**
  - [ ] Extract domain entities
  - [ ] Define value objects
  - [ ] Implement domain services
  - [ ] Add domain events

- [ ] **Infrastructure**
  - [ ] Implement repositories
  - [ ] Add transaction management
  - [ ] Create EventBus for backend
  - [ ] Set up proper migrations

- [ ] **Application Layer**
  - [ ] Refactor services
  - [ ] Implement CQRS pattern
  - [ ] Add proper validation

### Phase 4: Integration
- [ ] **Connect Layers**
  - [ ] Update Tauri commands
  - [ ] Connect to new services
  - [ ] Update event flow
  - [ ] Test end-to-end

- [ ] **Testing**
  - [ ] Unit tests (80% coverage)
  - [ ] Integration tests
  - [ ] E2E tests for critical paths
  - [ ] Performance benchmarks

### Phase 5: Rollout
- [ ] **Migration**
  - [ ] Set up feature flags
  - [ ] Create rollback plan
  - [ ] Monitor metrics
  - [ ] Complete migration

- [ ] **Documentation**
  - [ ] Architecture documentation
  - [ ] Migration guide
  - [ ] Best practices guide
  - [ ] Team training

## References

- [Zustand Documentation](https://github.com/pmndrs/zustand)
- [Tauri Architecture Guide](https://tauri.app/v1/guides/architecture/)
- [React Architecture Best Practices](https://react.dev/learn/thinking-in-react)
- [Feature-Sliced Design](https://feature-sliced.design/)

## /docs/rfcs/RFC-20250124-001-message-processing-architecture.md

# RFC-20250124-001: 消息处理架构设计

## 1. 概述

### 1.1 背景

当前 Pivo 系统中,AI Agent(Claude、Gemini 等)的工具调用消息是分离的:
- "Using tool" 开始消息
- "Tool Result" 结束消息

这种设计导致:
- 前端需要解析原始工具输出
- 消息展示不够简洁
- 难以支持多种 AI Agent 的不同输出格式

### 1.2 目标

基于现有架构,以最小改动实现:
1. 后端合并工具开始和结束消息
2. 保持与现有系统的兼容性
3. 提供结构化的工具执行信息(如 diff)
4. 易于扩展新的工具处理逻辑

### 1.3 设计原则

- **最小化改动**:基于现有架构,只在必要处添加新功能
- **向后兼容**:保留现有消息格式和处理流程
- **渐进式迁移**:可以逐步迁移,新旧系统可以共存
- **保持简单**:不过度设计,解决实际问题

## 2. 现有架构分析

### 2.1 当前消息流

```
AI Agent → Converter → AgentOutput → ConversationMessage → Channel → Frontend
```

### 2.2 核心组件

```
/src-tauri/src/services/coding_agent_executor/
├── agent.rs           # CodingAgent trait 和 ChannelMessage 定义
├── claude_agent.rs    # Claude AI Agent 实现
├── gemini_agent.rs    # Gemini AI Agent 实现
├── message.rs         # AgentOutput 消息格式定义
├── metadata.rs        # 消息元数据定义
├── service.rs         # 核心服务实现,包含消息处理器
└── types.rs           # ConversationMessage 等类型定义
```

### 2.3 现有数据结构

```rust
// message.rs - AI Agent 输出的统一格式
pub enum AgentOutput {
    ToolUse {
        id: String,
        tool_name: String,
        tool_input: serde_json::Value,
        timestamp: DateTime<Utc>,
    },
    ToolResult {
        tool_use_id: String,
        tool_name: String,
        result: String,
        is_error: bool,
        timestamp: DateTime<Utc>,
    },
    // ... 其他变体
}

// types.rs - 前后端通信的统一格式
pub struct ConversationMessage {
    pub id: String,
    pub role: MessageRole,
    pub message_type: String,      // "tool_use", "tool_result" 等
    pub content: String,
    pub timestamp: DateTime<Utc>,
    pub metadata: Option<serde_json::Value>,
}

// agent.rs - Channel 消息
pub struct ChannelMessage {
    pub attempt_id: String,
    pub task_id: String,
    pub message: ConversationMessage,
}
```

## 3. 架构设计

### 3.1 核心思路

在现有的 `start_message_processor` 函数中添加消息合并逻辑,不改变其他组件。

### 3.2 改动位置

主要改动集中在 `service.rs` 的消息处理器中:

```
┌─────────────┐     ┌──────────────┐     ┌─────────────────┐     ┌──────────────┐
│   AI Agent  │────>│  Converter   │────>│ Message Channel │────>│   Frontend   │
└─────────────┘     └──────────────┘     └─────────────────┘     └──────────────┘
                                                   │
                                                   ▼
                                          ┌─────────────────┐
                                          │Message Processor│ ← 在这里添加合并逻辑
                                          │ (合并工具消息)  │
                                          └─────────────────┘
```

### 3.3 消息合并流程

```mermaid
stateDiagram-v2
    [*] --> Receiving: 接收消息
    
    Receiving --> CheckType: 检查消息类型
    
    CheckType --> StoreToolUse: tool_use 类型
    CheckType --> ProcessToolResult: tool_result 类型
    CheckType --> EmitDirectly: 其他类型
    
    StoreToolUse --> StorePending: 存储到 pending_tools
    StorePending --> [*]: 不发送
    
    ProcessToolResult --> CheckPending: 查找对应的 tool_use
    CheckPending --> Merge: 找到匹配
    CheckPending --> EmitDirectly: 未找到匹配
    
    Merge --> CreateMerged: 创建合并消息
    CreateMerged --> EmitMerged: 发送合并消息
    
    EmitDirectly --> [*]: 直接发送
    EmitMerged --> [*]: 发送
```

## 4. 实现方案

### 4.1 修改消息处理器

```rust
// service.rs
fn start_message_processor(
    receiver: Receiver<ChannelMessage>,
    app_handle: AppHandle,
    // ... 其他参数
) {
    // 新增:待处理的工具消息
    let mut pending_tools: HashMap<String, PendingToolExecution> = HashMap::new();
    
    // 新增:配置
    let merge_tool_messages = true; // 可以从配置读取
    
    while let Ok(agent_msg) = receiver.recv() {
        let conversation_msg = agent_msg.message;
        
        // 新增:消息合并逻辑
        let should_emit = if merge_tool_messages {
            process_message_with_merge(&mut pending_tools, &conversation_msg)
        } else {
            true // 不合并,直接发送
        };
        
        if should_emit {
            // 保存到内存和数据库
            let _ = messages.lock().unwrap().push(conversation_msg.clone());
            if let Err(e) = state.db.save_agent_message(...) {
                log::error!("Failed to save message: {}", e);
            }
            
            // 发送到前端
            let _ = app_handle.emit("coding-agent-message", serde_json::json!({
                "task_id": task_id,
                "attempt_id": attempt_id,
                "message": conversation_msg,
            }));
        }
    }
}

// 新增:消息处理逻辑
fn process_message_with_merge(
    pending_tools: &mut HashMap<String, PendingToolExecution>,
    message: &ConversationMessage,
) -> bool {
    match message.message_type.as_str() {
        "tool_use" => {
            if let Some(tool_use_id) = extract_tool_use_id(message) {
                pending_tools.insert(tool_use_id, PendingToolExecution {
                    message: message.clone(),
                    timestamp: Utc::now(),
                });
                false // 暂不发送
            } else {
                true // 无法提取 ID,直接发送
            }
        }
        "tool_result" => {
            if let Some(tool_use_id) = extract_tool_result_id(message) {
                if let Some(pending) = pending_tools.remove(&tool_use_id) {
                    // 创建合并消息并发送
                    let merged = create_merged_tool_message(&pending.message, message);
                    emit_merged_message(app_handle, merged);
                    false // 已处理
                } else {
                    true // 找不到对应的开始,直接发送
                }
            } else {
                true
            }
        }
        _ => true // 其他消息直接发送
    }
}
```

### 4.2 新增数据结构

```rust
// 在 types.rs 中添加
#[derive(Clone)]
struct PendingToolExecution {
    message: ConversationMessage,
    timestamp: DateTime<Utc>,
}

// 工具执行元数据
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ToolExecutionMetadata {
    pub tool_name: String,
    pub tool_use_id: String,
    pub input: serde_json::Value,
    pub output: String,
    pub is_error: bool,
    pub duration_ms: u64,
    pub summary: String,
    pub details: Option<ToolDetails>,
}

// 工具详情
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(tag = "type", content = "data")]
pub enum ToolDetails {
    Diff { 
        old_content: String,
        new_content: String,
        file_path: String,
    },
    FileList(Vec<String>),
    CommandOutput { 
        stdout: String,
        stderr: String,
        exit_code: i32,
    },
}
```

### 4.3 工具 ID 提取函数

```rust
// 提取工具使用 ID
fn extract_tool_use_id(message: &ConversationMessage) -> Option<String> {
    if message.message_type == "tool_use" {
        message.metadata
            .as_ref()
            .and_then(|m| m.get("tool_use_id"))
            .and_then(|id| id.as_str())
            .map(|s| s.to_string())
    } else {
        None
    }
}

// 提取工具结果 ID
fn extract_tool_result_id(message: &ConversationMessage) -> Option<String> {
    if message.message_type == "tool_result" {
        message.metadata
            .as_ref()
            .and_then(|m| m.get("tool_use_id"))
            .and_then(|id| id.as_str())
            .map(|s| s.to_string())
    } else {
        None
    }
}
```

### 4.4 消息合并函数

```rust
fn create_merged_tool_message(
    tool_use: &ConversationMessage,
    tool_result: &ConversationMessage,
) -> ConversationMessage {
    let tool_name = extract_tool_name(tool_use);
    let duration_ms = tool_result.timestamp
        .signed_duration_since(tool_use.timestamp)
        .num_milliseconds() as u64;
    
    // 创建简洁的摘要
    let summary = create_tool_summary(&tool_name, tool_use, tool_result);
    
    // 创建合并后的元数据
    let metadata = ToolExecutionMetadata {
        tool_name: tool_name.clone(),
        tool_use_id: extract_tool_use_id(tool_use).unwrap_or_default(),
        input: tool_use.metadata
            .as_ref()
            .and_then(|m| m.get("structured"))
            .cloned()
            .unwrap_or(serde_json::Value::Null),
        output: tool_result.content.clone(),
        is_error: tool_result.metadata
            .as_ref()
            .and_then(|m| m.get("error"))
            .and_then(|e| e.as_bool())
            .unwrap_or(false),
        duration_ms,
        summary: summary.clone(),
        details: create_tool_details(&tool_name, tool_use, tool_result),
    };
    
    ConversationMessage {
        id: format!("{}-merged", tool_use.id),
        role: MessageRole::Assistant,
        message_type: "tool_execution".to_string(),
        content: summary,
        timestamp: tool_use.timestamp,
        metadata: Some(serde_json::to_value(metadata).unwrap()),
    }
}

// 创建工具摘要
fn create_tool_summary(
    tool_name: &str,
    tool_use: &ConversationMessage,
    tool_result: &ConversationMessage,
) -> String {
    match tool_name {
        "Edit" => {
            if let Some(file_path) = extract_file_path(tool_use) {
                format!("Updated {}", file_path)
            } else {
                "Updated file".to_string()
            }
        }
        "Write" => {
            if let Some(file_path) = extract_file_path(tool_use) {
                format!("Created {}", file_path)
            } else {
                "Created file".to_string()
            }
        }
        "Read" => {
            if let Some(file_path) = extract_file_path(tool_use) {
                format!("Read {}", file_path)
            } else {
                "Read file".to_string()
            }
        }
        _ => format!("{} completed", tool_name),
    }
}
```

### 4.5 可选的工具处理器扩展

如果需要更复杂的处理逻辑(如计算 diff),可以创建工具处理器:

```rust
// tool_processors/mod.rs
pub trait ToolProcessor: Send + Sync {
    fn tool_name(&self) -> &'static str;
    fn create_details(
        &self,
        tool_use: &ConversationMessage,
        tool_result: &ConversationMessage,
    ) -> Option<ToolDetails>;
}

// tool_processors/edit_processor.rs
pub struct EditProcessor;

impl ToolProcessor for EditProcessor {
    fn tool_name(&self) -> &'static str {
        "Edit"
    }
    
    fn create_details(
        &self,
        tool_use: &ConversationMessage,
        tool_result: &ConversationMessage,
    ) -> Option<ToolDetails> {
        // 从 tool_use 提取 old_string 和 new_string
        // 返回 Diff 详情
        // 这是可选的增强功能
        None
    }
}
```

## 5. 前端适配

### 5.1 新增消息类型

```typescript
// 新增工具执行消息类型
export interface ToolExecutionMessage extends BaseMessage {
  role: 'assistant';
  messageType: 'tool_execution';
  metadata: {
    toolName: string;
    toolUseId: string;
    input: any;
    output: string;
    isError: boolean;
    duration: number;
    summary: string;
    details?: ToolDetails;
  };
}

export type ToolDetails = 
  | { type: 'diff'; data: { oldContent: string; newContent: string; filePath: string } }
  | { type: 'fileList'; data: string[] }
  | { type: 'commandOutput'; data: { stdout: string; stderr: string; exitCode: number } };
```

### 5.2 消息渲染器更新

```typescript
// MessageRenderer.tsx
function MessageRenderer({ message }: { message: ConversationMessage }) {
  switch (message.messageType) {
    case 'tool_execution':
      return <ToolExecutionMessage message={message} />;
    case 'tool_use':
      // 向后兼容:如果收到未合并的消息
      return <ToolUseMessage message={message} />;
    case 'tool_result':
      // 向后兼容:如果收到未合并的消息
      return <ToolResultMessage message={message} />;
    // ... 其他类型
  }
}
```

## 6. 实施计划

### 6.1 第一阶段:基础实现
1. 在 `service.rs` 中添加消息合并逻辑
2. 添加必要的辅助函数
3. 通过配置开关控制是否启用

### 6.2 第二阶段:前端支持
1. 添加新的消息类型定义
2. 创建 `ToolExecutionMessage` 组件
3. 保留对旧消息格式的支持

### 6.3 第三阶段:增强功能
1. 添加工具处理器机制(可选)
2. 实现 diff 计算等高级功能
3. 优化消息展示

## 7. 优势

1. **最小改动**:只在消息处理器中添加逻辑,不影响其他组件
2. **向后兼容**:可以通过配置开关控制,新旧系统可以共存
3. **渐进式迁移**:前端可以同时支持新旧消息格式
4. **易于扩展**:可以逐步添加更多工具的特殊处理逻辑
5. **保持简单**:不改变现有的架构和数据流

## 8. 风险与缓解

### 8.1 消息顺序问题
- 风险:工具结果可能先于工具使用到达
- 缓解:保留未匹配的结果消息直接发送

### 8.2 内存泄漏
- 风险:未匹配的工具使用消息一直保存在内存中
- 缓解:定期清理超时的待处理消息

### 8.3 兼容性问题
- 风险:前端可能不支持新的消息格式
- 缓解:通过配置开关控制,保留对旧格式的支持

## 9. 总结

这个方案基于现有架构,以最小的改动实现了工具消息的合并功能。通过在消息处理器中添加合并逻辑,我们可以:

1. 提供更好的用户体验(合并的工具消息)
2. 保持系统的简单性和可维护性
3. 支持渐进式迁移和向后兼容
4. 为未来的增强功能预留扩展点

整个方案专注于解决实际问题,避免过度设计,符合 Pivo 项目的实际需求。

## /docs/rfcs/RFC-20250131-architecture-cleanup.md

# RFC-20250131 架构清理报告

## 概述

根据用户要求,深入检查了代码中所有潜在的与 RFC 架构不符的地方,并进行了彻底的清理。

## 发现的问题及修复

### 1. 不符合架构的 API 方法

#### 问题
- `ExecutionApi` 中存在 `executePrompt`、`executeClaudePrompt`、`executeGeminiPrompt` 方法
- 这些方法允许前端直接执行 prompt,绕过了 TaskCommand 系统
- `TaskAttemptApi` 中存在 `create` 方法,允许前端创建 Attempt

#### 修复
- ✅ 彻底删除了 `ExecutionApi` 中的所有 execute 相关方法
- ✅ 删除了 `TaskAttemptApi.create` 方法
- ✅ 删除了 `lib/api.ts` 中对应的底层方法

### 2. 事件命名不一致

#### 问题
- 存在破折号分隔的旧事件:`task-created`、`task-updated`、`task-deleted`、`project-deleted`
- `useTaskExecutionStatus` 中使用了 `execution-stopped` 而不是 `execution:stopped`

#### 修复
- ✅ 删除了所有破折号分隔的旧事件定义
- ✅ 修正了 `execution-stopped` 为 `execution:stopped`

### 3. 遗留的事件定义

#### 问题
- `start-task-execution` 事件仍在类型定义中

#### 修复
- ✅ 已在之前的修改中删除

## 架构一致性验证

### 现在的架构完全符合 RFC 设计:

1. **Task 创建流程**
   ```
   前端: taskApi.create() 
     ↓
   后端: 自动创建 Attempt
     ↓
   前端: 直接发送消息 (SendMessage)
   ```

2. **命令系统**
   - 只有两个命令:`SEND_MESSAGE` 和 `STOP_EXECUTION`
   - 没有 `START_EXECUTION`
   - 前端不能直接执行 prompt

3. **事件系统**
   - 所有事件使用冒号分隔格式:`domain:action`
   - 删除了所有破折号分隔的旧事件

4. **API 设计**
   - 前端只能通过 `useTaskCommand` 发送消息
   - 不能直接创建 Attempt
   - 不能直接执行 prompt

## 防止未来问题的建议

1. **代码审查**:确保新代码遵循 RFC 架构
2. **API 设计原则**:前端 API 应该是高层抽象,不暴露底层实现细节
3. **事件命名规范**:统一使用冒号分隔的格式
4. **文档更新**:保持架构文档与代码同步

## 总结

通过这次彻底的清理,代码库现在完全符合 RFC-20250131 的架构设计。所有不符合架构的 API 方法都已被删除,事件系统已统一,前端只能通过正确的方式与后端交互。

## /docs/rfcs/RFC-20250131-architecture-validation.md

# RFC-20250131 架构验证报告

## 问题分析

用户指出在重构过程中出现了 `await taskApi.execute(newTask.id)` 这样的错误代码,这表明重构不完整。经过深入检查,我发现了以下问题:

### 重构过程中的问题

1. **第一次修改时的错误**:在尝试修复 TypeScript 编译错误时,我使用了错误的字符串匹配模式,导致某些 `taskApi.execute` 调用没有被正确替换。

2. **根本原因**:我在进行字符串替换时,没有考虑到代码中可能存在的格式差异(如空格、换行等),导致某些模式没有匹配成功。

## 当前架构验证

### 1. 后端架构 ✅

**Task 创建时自动创建 Attempt**:
```rust
// task_service.rs - create_task 方法
// Always create an initial attempt with worktree for the task
let attempt_req = CreateTaskAttemptRequest {
    task_id: id,
    executor: None,
    base_branch: None,
};

match self.create_task_attempt(attempt_req).await {
    Ok(_) => log::info!("Created initial attempt for task {}", id),
    Err(e) => log::error!("Failed to create initial attempt for task {}: {}", id, e),
}
```

**SendMessage 使用保存的 session ID**:
```rust
// task_commands.rs - handle_send_message
let resume_session_id = match attempt.executor.as_deref() {
    Some("claude") | Some("claude_code") => attempt.claude_session_id.clone(),
    _ => None,
};
```

### 2. 前端架构 ✅

**正确使用 SendMessage 命令**:
```typescript
// TasksView.tsx
await executeTaskCommand({
  type: 'SEND_MESSAGE',
  taskId: task.id,
  message: initialMessage,
  images
});
```

**监听并保存 Session ID**:
```typescript
// TaskConversation.tsx
useEffect(() => {
  const unsubscribe = listen('session:received', async (event: any) => {
    const { attemptId, sessionId } = event.payload;
    
    if (conversationState.currentAttemptId === attemptId) {
      await taskAttemptApi.updateClaudeSessionId(attemptId, sessionId);
    }
  });
  
  return () => {
    unsubscribe.then(fn => fn());
  };
}, [conversationState.currentAttemptId]);
```

### 3. 事件系统 ✅

- 使用冒号分隔的事件名称格式
- 删除了所有遗留的事件定义
- 清理了兼容性代码

## 架构流程图

```
用户创建任务
    ↓
后端自动创建 Attempt
    ↓
用户发送消息 (SendMessage)
    ↓
后端使用 Attempt 的 claude_session_id (如果有)
    ↓
Claude 返回新的 session ID
    ↓
前端监听并保存到数据库
    ↓
下次发送消息时使用保存的 session ID(会话恢复)
```

## 结论

当前代码完全符合 RFC-20250131 的架构设计:

1. ✅ Task 创建时自动创建 Attempt
2. ✅ 前端只使用 SendMessage,不再有 StartExecution
3. ✅ Session ID 被正确保存和使用
4. ✅ 所有兼容性代码已清理
5. ✅ 事件系统已更新为新格式

## 教训

在进行大规模重构时,应该:
1. 使用更精确的搜索和替换工具
2. 在每次修改后立即验证编译
3. 保持对 RFC 设计原则的清晰理解
4. 进行全面的代码审查,而不是依赖简单的字符串搜索

## /docs/rfcs/RFC-20250131-complete-architecture-review.md

# RFC-20250131 完整架构审查报告

## 第二次深入检查发现的问题

### 1. 后端暴露了不应该存在的 Tauri 命令

虽然前端没有调用这些命令,但它们的存在违反了架构设计,提供了绕过 TaskCommand 系统的潜在路径:

**已移除的 Tauri 命令**:
- `create_task_attempt` - 前端不应创建 Attempt
- `execute_prompt` - 绕过 TaskCommand 系统
- `execute_claude_prompt` - 绕过 TaskCommand 系统
- `execute_gemini_prompt` - 绕过 TaskCommand 系统
- `get_attempt_execution_state` - 前端不应直接访问执行状态
- `get_task_execution_summary` - 前端不应直接访问执行摘要
- `is_attempt_active` - 前端不应直接查询状态

### 2. lib/api.ts 中的遗留方法

发现并删除了以下不符合架构的 API 方法:
- `getAttemptExecutionState()`
- `getTaskExecutionSummary()`
- `addMessage()` - 前端不应直接添加消息
- `isAttemptActive()`

### 3. 架构合规性验证

#### ✅ Task-Attempt-Execution 三层架构
- Task 创建时自动创建 Attempt(后端处理)
- 前端只能通过 SendMessage 触发执行
- 执行状态通过事件系统同步

#### ✅ 命令系统
```rust
pub enum TaskCommand {
    SendMessage { task_id, message, images },
    StopExecution { task_id }
}
```
- 没有 StartExecution
- 没有 CreateAttempt
- 没有 ExecutePrompt

#### ✅ API 设计原则
- 前端 API 是高层抽象,不暴露底层实现
- 所有执行必须通过 TaskCommand
- 状态同步通过事件系统

### 4. 构建验证

构建成功,但有一些未使用函数的警告:
- 后端保留了这些函数的实现(如 `execute_claude_prompt`)
- 这些函数虽然没有暴露给前端,但可以在后续清理中删除

## 总结

经过第二次深入检查:
1. 发现并移除了所有可能绕过架构的 Tauri 命令
2. 清理了前端 API 中的遗留方法
3. 确保前端只能通过 TaskCommand 系统与后端交互
4. 没有任何后门或绕过机制

现在的代码严格遵循 RFC-20250131 的设计,实现了真正的关注点分离和安全的架构边界。

## /docs/rfcs/RFC-20250131-final-assessment.md

# RFC-20250131 Implementation Final Assessment

## Summary

Successfully completed the refactoring of the task-attempt-execution architecture according to RFC-20250131-task-attempt-execution-architecture.md. All compatibility code has been removed and the system now uses the target architecture.

## Changes Made

### Backend Changes

1. **Task Commands Refactoring** (`task_commands.rs`)
   - Removed `StartExecution` command
   - Updated `SendMessage` to use saved `claude_session_id` for session resume
   - Simplified command structure to only `SendMessage` and `StopExecution`

2. **Event System Updates**
   - Changed event names from dash-separated to colon-separated format:
     - `task-attempt-created` → `task:attempt-created`
     - `task-status-updated` → `task:status-changed`
   - Removed legacy events:
     - `coding-agent-output` (debug output)
     - `coding-agent-process-completed`

3. **Service Layer Updates** (`service.rs`)
   - Updated all event emissions to new format
   - Removed references to non-existent methods

### Frontend Changes

1. **Task Conversation Component**
   - Added `session:received` listener to save Claude session ID
   - Updated event listeners to new format

2. **State Management Hooks**
   - `useTaskConversationState`: Updated to listen to new events
   - `useTaskCommand`: Simplified to match new command structure
   - Removed unused parameters

3. **API Layer Cleanup**
   - Removed deprecated `execute` method from TaskApi
   - Removed unused compatibility aliases
   - Cleaned up unused exports

4. **Event Types**
   - Removed legacy event definitions
   - Updated imports to remove unused types

### Files Removed

1. `/src/features/projects/hooks/useProjectManagement.ts` - Temporary compatibility stub
2. `/src/features/tasks/hooks/useTaskOperations.ts` - Temporary compatibility stub
3. `/src/hooks/domain/useVcs.ts` - Replaced with single comment

## Key Achievement

The most important fix was ensuring that the `claude_session_id` stored in the database is actually used for session resume. Previously, the system was passing `None` to the Claude agent, which meant sessions couldn't be resumed. Now the system correctly:

1. Captures the session ID when Claude starts
2. Stores it in the database
3. Uses it for subsequent messages to resume the session

## Verification

All compatibility code marked with TODOs, @deprecated annotations, or temporary comments has been removed. The system now fully implements the three-layer architecture (Task → Attempt → Execution) as designed in the RFC.

## /docs/rfcs/RFC-20250131-final-cleanup-summary.md

# RFC-20250131 最终清理总结

## 执行的清理工作

### 1. 删除了不符合架构的 API

**删除的文件**:
- `/src/services/api/ExecutionApi.ts` - 整个文件已删除

**删除的方法**:
- `ExecutionApi.executePrompt()` - 绕过 TaskCommand 系统
- `ExecutionApi.executeClaudePrompt()` - 绕过 TaskCommand 系统  
- `ExecutionApi.executeGeminiPrompt()` - 绕过 TaskCommand 系统
- `TaskAttemptApi.create()` - 前端不应创建 Attempt
- `lib/api.ts` 中对应的底层方法

### 2. 修复了事件命名不一致

**删除的事件定义**:
- `task-created`
- `task-updated`
- `task-deleted`
- `project-deleted`
- `start-task-execution`

**修复的事件使用**:
- `execution-stopped` → `execution:stopped`

### 3. 架构合规性验证

现在的代码完全符合 RFC 设计:

#### Task-Attempt-Execution 三层架构
```
Task (任务)
  └─> Attempt (尝试) - 后端自动创建
        └─> Execution (执行) - 通过 SendMessage 触发
```

#### 前端只能通过 TaskCommand 系统交互
```typescript
// 正确的方式
const { sendMessage, stopExecution } = useTaskCommand();
await sendMessage(taskId, message, images);

// 删除的错误方式
// await taskApi.execute(taskId);  ❌
// await executionApi.executePrompt(...);  ❌
// await taskAttemptApi.create(...);  ❌
```

#### 统一的事件命名
```
domain:action 格式
- task:status-changed
- task:attempt-created
- execution:started
- message:added
- state:conversation-sync
```

## 构建验证

✅ TypeScript 编译通过
✅ Tauri 构建成功
✅ 生成了可执行文件

## 结论

所有潜在的架构不一致问题都已被发现并彻底删除。代码库现在严格遵循 RFC-20250131 的设计,前端只能通过规定的方式与后端交互,不存在任何绕过架构的后门。

## /docs/rfcs/RFC-20250131-implementation-gaps.md

# Task-Attempt-Execution 架构实施差距分析

## 检查日期
2025-01-31

## 已完成的改造 ✅

### 后端
1. **TaskCommand 简化**
   - ✅ 移除了 `StartExecution`
   - ✅ 只保留 `SendMessage` 和 `StopExecution`

2. **Session Resume 实现**
   - ✅ `handle_send_message` 正确使用 `attempt.claude_session_id`
   - ✅ `execute_prompt` 正确传递 `resume_session_id`

3. **事件重新设计**
   - ✅ 使用新的事件名称:`execution:started`, `execution:completed`, `message:added`, `task:status-changed`
   - ✅ 移除了冗余的事件发送方法

### 前端
1. **Session ID 监听**
   - ✅ TaskConversation 监听 `session:received` 事件
   - ✅ 正确调用 `updateClaudeSessionId` 保存 session ID

2. **事件监听更新**
   - ✅ `useTaskConversationState` 监听新事件:`state:conversation-sync`, `execution:completed`, `message:added`
   - ✅ `TasksView` 监听 `task:status-changed`

3. **命令简化**
   - ✅ `useTaskCommand` 只有 `sendMessage` 和 `stopExecution`

## 未完成的改造 ❌

### 1. TaskApi.execute 方法问题
**文件**: `/src/services/api/TaskApi.ts`
```typescript
// 仍在使用已被移除的 START_EXECUTION
async execute(id: string, initialMessage?: string, images?: string[]): Promise<void> {
  if (initialMessage) {
    return invoke<void>('execute_task_command', {
      command: {
        type: 'START_EXECUTION', // ❌ 这个已被移除
        taskId: id,
        payload: { initialMessage, images }
      }
    });
  } else {
    return invoke<void>('execute_task', { id }); // ❌ execute_task 已被移除
  }
}
```

**修复方案**:
- 移除 `execute` 方法,或改为使用 `SEND_MESSAGE`
- 确保调用方先创建 Attempt

### 2. Attempt 创建流程缺失
**问题**:
- 前端没有任何地方调用 `taskAttemptApi.create`
- 根据 RFC,Attempt 必须预先存在,不能自动创建
- 但目前没有明确的 UI 流程来创建 Attempt

**建议方案**:
- 在 Task 创建时自动创建第一个 Attempt
- 或在 Task 详情页提供"开始新对话"按钮

### 3. 旧事件监听器未移除
**文件**: `/src/features/tasks/hooks/useTaskExecutionStatus.ts`
```typescript
// 仍在监听旧事件
const unsubscribeStatus = eventBus.subscribe('task-execution-summary', (summary) => {
  // ...
});
```

**修复**:需要更新或移除这个 hook

### 4. 后端调用未实现的方法
**文件**: `/src-tauri/src/services/coding_agent_executor/service.rs`
```rust
// 这些方法被调用但未实现(已被注释掉)
self.emit_attempt_execution_state(attempt_id);
self.emit_task_execution_summary(task_id);
```

**修复**:移除这些调用

### 5. Legacy 事件定义未清理
**文件**: `/src/lib/events/EventTypes.ts`
- 仍保留 legacy events 定义
- 可能导致混淆

## 建议的修复优先级

1. **高优先级**(影响功能)
   - 修复 TaskApi.execute 方法
   - 实现 Attempt 创建流程

2. **中优先级**(代码清洁度)
   - 移除后端未实现方法的调用
   - 更新 useTaskExecutionStatus hook

3. **低优先级**(技术债务)
   - 清理 legacy 事件定义

## 总结

核心的 Session Resume 功能已经实现,但还有一些边缘问题需要处理:
- 主要问题是 TaskApi 还在使用旧的命令
- Attempt 创建流程需要明确
- 一些旧的事件监听器需要清理

## /docs/rfcs/RFC-20250131-implementation-summary.md

# Task-Attempt-Execution 架构重构实施总结

## 实施日期
2025-01-31

## 主要改动

### 1. 后端重构

#### task_commands.rs
- **简化命令系统**:移除 `StartExecution`,只保留 `SendMessage` 和 `StopExecution`
- **强制 Attempt 存在**:`SendMessage` 必须有已存在的 Attempt,否则报错
- **使用 Session Resume**:正确使用保存的 `claude_session_id` 进行会话恢复

```rust
// 之前:每次都传 None
resume_session_id: None

// 之后:使用保存的 session ID
let resume_session_id = match attempt.executor.as_deref() {
    Some("claude") | Some("claude_code") => attempt.claude_session_id.clone(),
    _ => None,
};
```

#### 事件系统重构
- 移除重复事件:`task-execution-summary`、`attempt-execution-update`
- 统一事件命名规范:
  - `task-status-updated` → `task:status-changed`
  - `coding-agent-message` → `message:added`
  - `conversation-state-update` → `state:conversation-sync`
  - `execution-completed` → `execution:completed` (包含 status)

### 2. 前端重构

#### Session ID 监听
```typescript
// TaskConversation.tsx
useEffect(() => {
  const unsubscribe = listen('session:received', async (event: any) => {
    const { attemptId, sessionId } = event.payload;
    
    if (conversationState.currentAttemptId === attemptId) {
      await taskAttemptApi.updateClaudeSessionId(attemptId, sessionId);
    }
  });
  
  return () => {
    unsubscribe.then(fn => fn());
  };
}, [conversationState.currentAttemptId]);
```

#### 命令简化
```typescript
// 移除了 startExecution,只保留:
- sendMessage(taskId, message, images?)
- stopExecution(taskId)
```

### 3. 移除的功能
- `execute_task` API - 功能合并到 `SendMessage`
- 自动创建 Attempt - 必须预先创建

## 核心价值

1. **Session 连续性**:通过正确使用 resume session,保持 Claude 对话上下文
2. **架构清晰**:Task → Attempt → Execution 三层职责明确
3. **事件简化**:减少重复事件,提高系统效率
4. **错误防护**:强制 Attempt 存在,避免意外行为

## 注意事项

1. 前端需要确保 Task 有 Attempt 才能发送消息
2. Session ID 在首次执行后自动保存
3. 旧的事件名称保留为 legacy,后续可以移除

## /docs/rfcs/RFC-20250131-task-attempt-execution-architecture.md

# RFC: Task-Attempt-Execution 架构重构

**RFC 编号**: RFC-20250131-001  
**标题**: 基于 Task-Attempt-Execution 三层模型的任务执行架构  
**作者**: Assistant  
**日期**: 2025-01-31  
**状态**: 草案  

## 概要

重新设计任务执行架构,明确 Task、Attempt 和 Execution 的关系和职责,实现连续的 Agent 会话管理。

## 动机

当前架构存在以下问题:
1. 虽然每次发送消息都创建新的 execution,但未能清晰体现 execution 与 session 的关系
2. 没有明确的 active attempt 管理机制
3. Session resume 功能未被正确使用 - 每次新消息都传递 `None` 作为 resume_session_id
4. Claude session ID 虽然被存储在数据库中,但未在后续执行中使用
5. 前端缺少监听和保存 claude-session-id-received 事件的逻辑
6. 状态管理分散,缺乏统一的生命周期管理

## 核心概念

```
┌─────────────────────────────────────────────────────┐
│                      Task                           │
│  - 一个任务实体                                      │
│  - 可以有多个 Attempt                               │
│  - 同时只有一个 Active Attempt                      │
└─────────────────────────────────────────────────────┘
                         │
                         │ 1:N (Active: 1)
                         ▼
┌─────────────────────────────────────────────────────┐
│                    Attempt                          │
│  - 一次任务尝试                                      │
│  - 绑定特定的 Agent 类型                            │
│  - 维护一个 Agent Session                          │
│  - 可以有多个 Execution                            │
│  - 同时只有一个 Active Execution                   │
└─────────────────────────────────────────────────────┘
                         │
                         │ 1:N (Active: 0-1)
                         ▼
┌─────────────────────────────────────────────────────┐
│                   Execution                         │
│  - 一次具体的执行(对应一条用户消息)                 │
│  - 通过 resume session 在同一个 Agent Session 中执行 │
│  - 记录执行状态和结果                              │
└─────────────────────────────────────────────────────┘
```

## 设计原则

1. **利用现有结构**:不新增服务层,使用已有的 TaskService 和 CodingAgentExecutorService
2. **最小化改动**:只修复 session resume 未使用的问题
3. **保持简单**:前端只需要发送消息,后端处理复杂性

## 简化的解决方案

### 1. 简化的命令系统

基于三层关系,前端只需要简单的接口:

```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TaskCommand {
    /// 发送消息(需要已存在 Attempt)
    #[serde(rename = "SEND_MESSAGE")]
    SendMessage { 
        #[serde(rename = "taskId")]
        task_id: String, 
        message: String,
        images: Option<Vec<String>>,
    },
    
    /// 停止当前执行
    #[serde(rename = "STOP_EXECUTION")]
    StopExecution { 
        #[serde(rename = "taskId")]
        task_id: String,
    },
}
```

#### 核心处理逻辑
```rust
async fn handle_send_message(
    task_id: &str,
    message: String,
    images: Option<Vec<String>>,
) -> Result<(), String> {
    // 1. 获取最新的 Attempt,如果没有则报错
    let attempts = task_service.list_task_attempts(task_id).await?;
    let attempt = attempts.last()
        .ok_or("No attempt found for this task. Please create an attempt first.")?
        .clone();
    
    // 2. 获取 resume session ID(如果有)
    let resume_session_id = match attempt.executor.as_deref() {
        Some("claude") | Some("claude_code") => attempt.claude_session_id.clone(),
        _ => None,
    };
    
    // 3. 执行
    let execution = cli::execute_prompt(
        cli_state.clone(),
        message,
        task_id.to_string(),
        attempt.id.clone(),
        working_directory,
        agent_type,
        resume_session_id,  // 使用保存的 session ID
    ).await?;
    
    Ok(())
}
```

### 2. 前端使用

```typescript
// 极简的 API
const { sendMessage, stopExecution } = useTaskConversation(taskId);

// 发送消息(需要已存在 Attempt)
await sendMessage("请帮我实现这个功能", images);
```

### 3. 关键修复点

基于现有代码分析,需要修复以下问题:

#### 3.1 后端修复 - 使用保存的 Session ID

```rust
// task_commands.rs - 修改 start_execution 和 send_message
async fn start_execution(...) -> Result<(), String> {
    // ... 现有代码 ...
    
    // 使用保存的 session ID(如果有)
    let resume_session_id = match attempt.executor.as_deref() {
        Some("claude") | Some("claude_code") => attempt.claude_session_id.clone(),
        _ => None,
    };
    
    let _execution = crate::commands::cli::execute_prompt(
        cli_state.clone(),
        prompt,
        task_id.to_string(),
        attempt.id.clone(),
        working_directory,
        agent_type,
        resume_session_id,  // 不再传 None
    ).await?;
}
```

#### 3.2 前端修复 - 监听并保存 Session ID

```typescript
// TaskConversation.tsx
useEffect(() => {
  const unsubscribe = listen('claude-session-id-received', async (event) => {
    const { task_id, attempt_id, claude_session_id } = event.payload;
    
    if (task_id === task.id) {
      // 保存到后端
      await taskAttemptApi.updateClaudeSessionId(attempt_id, claude_session_id);
    }
  });
  
  return () => unsubscribe();
}, [task.id]);
```

## 状态同步机制

### 事件驱动的状态管理

当前系统通过事件机制实现前后端状态同步:

#### 1. 核心事件流

```
后端执行流程                     发出事件                      前端响应
─────────────                   ────────                     ────────
start_execution()               
  ├─> task-status-updated       ────────────────────>       TasksView 更新任务状态
  ├─> execute_prompt()
  │     ├─> claude-session-id-received ──────────────>      保存 session ID
  │     ├─> coding-agent-message ────────────────────>       实时显示消息
  │     └─> task-execution-summary ──────────────────>       更新执行状态
  └─> conversation-state-update ──────────────────────>      刷新整体会话状态

执行过程中
  ├─> coding-agent-message (多次) ────────────────────>      增量更新消息
  └─> attempt-execution-update ────────────────────────>     更新 attempt 状态

执行结束
  ├─> execution-completed ──────────────────────────>       标记执行完成
  └─> task-status-updated ──────────────────────────>       更新任务状态
```

#### 2. 前端状态管理

```typescript
// useTaskConversationState - 统一管理会话状态
interface ConversationState {
  messages: ConversationMessage[];      // 消息列表
  isExecuting: boolean;                 // 执行状态
  currentExecution?: CodingAgentExecution; // 当前执行
  canSendMessage: boolean;              // 是否可发送消息
}

// 监听两个关键事件
- conversation-state-update: 完整状态刷新(批量更新)
- coding-agent-message: 增量消息更新(实时性)
```

#### 3. 状态一致性保证

1. **双重更新机制**:
   - 增量更新:通过 `coding-agent-message` 实时显示新消息
   - 全量刷新:通过 `conversation-state-update` 确保状态最终一致

2. **防止状态冲突**:
   ```typescript
   // 加载初始状态时暂停增量更新
   if (isLoadingInitialState) return;
   
   // 检查消息是否已存在,避免重复
   const messageExists = prev.messages.some(m => m.id === newMessage.id);
   ```

3. **状态联动**:
   - 任务状态变更 → 触发会话状态更新
   - 执行状态变更 → 更新 UI 交互能力

## 事件设计问题分析

当前执行过程中事件存在以下问题:

### 1. 事件重复和冗余
- `coding-agent-output` vs `coding-agent-message`:前者是原始输出,后者是结构化消息,存在转换关系
- `task-execution-summary` vs `attempt-execution-update`:两者都描述执行状态,职责重叠
- `execution-completed` vs `task-status-updated`:执行完成时都会触发,造成重复

### 2. 事件粒度不一致
- 有些事件太细粒度(如 `coding-agent-output` 每行输出都发送)
- 有些事件太粗粒度(如 `conversation-state-update` 包含所有状态)

### 3. 建议的事件重新设计

基于分析,Claude Code 输出的是独立完整的消息(按行的 JSON),而非流式更新单个消息。每个消息类型包括:
- `thinking`: 思考过程
- `assistant`: AI 回复(包含 text 或 tool_use)  
- `tool_result`: 工具执行结果
- `result`: 执行总结

因此不需要 `message:updated` 这样的流式更新事件。

```typescript
// 执行生命周期事件(粗粒度)
interface ExecutionLifecycleEvents {
  'execution:started': {
    taskId: string;
    attemptId: string;
    executionId: string;
  };
  
  'execution:completed': {
    taskId: string;
    attemptId: string;
    executionId: string;
    status: 'success' | 'failed' | 'cancelled';
  };
}

// 消息事件(每个消息都是完整的,不需要流式更新)
interface MessageEvents {
  'message:added': {
    taskId: string;
    attemptId: string;
    message: ConversationMessage;
  };
}

// 状态同步事件(按需触发)
interface StateSyncEvents {
  'state:conversation-sync': {
    taskId: string;
    state: ConversationState;
  };
}

// Task 状态事件(独立处理,因为 Task 状态影响范围更广)
interface TaskStateEvents {
  'task:status-changed': {
    taskId: string;
    previousStatus: TaskStatus;
    newStatus: TaskStatus;
    task: Task;
  };
  
  'task:attempt-created': {
    taskId: string;
    attempt: TaskAttempt;
  };
  
  'task:attempt-updated': {
    taskId: string;
    attemptId: string;
    updates: Partial<TaskAttempt>;
  };
}
```

## 实施计划

1. **立即修复**(1小时)
   - 修改 `start_execution` 和 `send_message` 使用保存的 session ID
   - 添加前端监听器保存 session ID

2. **后续优化**(可选)
   - 简化命令系统,让前端更简单
   - 统一事件设计,减少重复
   - 改进状态管理

## 总结

这是一个典型的"最后一英里"问题:
- 所有组件都已存在(数据库字段、事件机制、API)
- 只需要正确连接它们
- 不需要复杂的架构改动
- 事件驱动架构已经提供了良好的状态同步基础

## /docs/rfcs/RFC-20250131-third-review-findings.md

# RFC-20250131 第三次审查发现

## 发现的问题

第三次审查再次发现了不符合架构的内容:

### 1. 前端 API 中的不当方法

**TaskAttemptApi** 中存在的问题方法:
- `updateStatus()` - 前端不应该直接更新 Attempt 状态
- `saveConversation()` - 前端不应该直接保存会话
- `getConversation()` - 前端不应该直接获取会话

这些方法允许前端绕过正常的消息流程,直接操作后端数据。

### 2. 后端未使用的命令函数

虽然没有在 `lib.rs` 中注册,但这些函数的存在仍是隐患:
- `create_task_attempt()` - 创建 Attempt 的函数
- `update_attempt_status()` - 更新状态的函数
- `save_attempt_conversation()` - 保存会话的函数
- `get_attempt_conversation()` - 获取会话的函数
- `execute_claude_prompt()` - 直接执行的函数
- `execute_gemini_prompt()` - 直接执行的函数
- `get_attempt_execution_state()` - 获取执行状态
- `get_task_execution_summary()` - 获取执行摘要
- `is_attempt_active()` - 查询活跃状态

### 3. lib/api.ts 中的遗留方法

发现并删除了:
- `updateStatus()`
- `saveConversation()`
- `getConversation()`
- `getAttemptExecutionState()`
- `getTaskExecutionSummary()`
- `addMessage()`
- `isAttemptActive()`

## 修复措施

1. **删除了所有不符合架构的前端 API 方法**
2. **删除了所有未使用的后端命令函数**
3. **从 lib.rs 中移除了这些命令的注册**
4. **创建了必要的模型类型** (`AttemptConversation`, `ConversationMessage`)

## 当前架构状态

### 前端只能:
- 通过 `TaskCommand` 发送消息 (`SEND_MESSAGE`)
- 通过 `TaskCommand` 停止执行 (`STOP_EXECUTION`)
- 获取任务列表和详情(只读)
- 更新 Claude Session ID(这是唯一允许的写操作,用于会话恢复)

### 后端负责:
- 自动创建 Attempt
- 管理执行状态
- 保存会话记录
- 发送状态更新事件

## 结论

这次审查揭示了一个重要教训:**即使函数没有被暴露给前端,它们的存在本身就是架构漏洞**。通过彻底删除这些函数,我们确保了:

1. 没有人能够意外地重新启用这些功能
2. 代码库保持清晰的架构边界
3. 新开发者不会被误导使用错误的模式

构建已成功通过,架构现在严格遵循 RFC-20250131 的设计。

## /docs/rfcs/concepts.md

几个核心概念的关系是:

1. 程序可以同时打开多个 project,每个project 是一个独立的窗口。
2. project 与 task 是 1:n, 每个project 里面可以有多个 task 。
3. task 持有 attempt ,且关系是 1:n,每个task在创建的时候,会同步创建对应的 attempt。但是当前只能维持一个活跃的 attempt。
4. attempt 与 coding agent excution 是 1:n ,也就是每个 attempt 里面可以进行多轮 coding agent 的交互。在运行时状态下,每个attempt 里面只有一个 execution 会在执行状态。 Task Conversation 里面是展示的事一个 attempt 里面所有 execution 的历史消息,和最新一个 execution 的实时消息。

```plantuml
@startuml
@enduml
```

## /index.html

```html path="/index.html" 
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tauri + React + Typescript</title>
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

## /log-viewer.html

```html path="/log-viewer.html" 
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Pivo - Log Viewer</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/log-viewer.tsx"></script>
  </body>
</html>
```

## /package.json

```json path="/package.json" 
{
  "name": "pivo",
  "private": true,
  "version": "0.1.2",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "tauri": "tauri"
  },
  "dependencies": {
    "@hello-pangea/dnd": "^18.0.1",
    "@radix-ui/react-checkbox": "^1.3.2",
    "@radix-ui/react-dialog": "^1.1.14",
    "@radix-ui/react-dropdown-menu": "^2.1.15",
    "@radix-ui/react-label": "^2.1.7",
    "@radix-ui/react-scroll-area": "^1.2.9",
    "@radix-ui/react-select": "^2.2.5",
    "@radix-ui/react-separator": "^1.1.7",
    "@radix-ui/react-switch": "^1.2.5",
    "@radix-ui/react-tabs": "^1.1.12",
    "@radix-ui/react-toast": "^1.2.14",
    "@radix-ui/react-tooltip": "^1.2.7",
    "@tanstack/react-query": "^5.83.0",
    "@tauri-apps/api": "^2",
    "@tauri-apps/plugin-clipboard-manager": "^2.3.0",
    "@tauri-apps/plugin-dialog": "^2.3.0",
    "@tauri-apps/plugin-fs": "^2.4.0",
    "@tauri-apps/plugin-opener": "^2",
    "@tauri-apps/plugin-shell": "^2.3.0",
    "@xstate/react": "^6.0.0",
    "@xterm/addon-fit": "^0.10.0",
    "@xterm/addon-web-links": "^0.11.0",
    "@xterm/xterm": "^5.5.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "i18next": "^25.3.2",
    "lucide-react": "^0.525.0",
    "react": "^18.3.1",
    "react-diff-viewer-continued": "^3.4.0",
    "react-dom": "^18.3.1",
    "react-i18next": "^15.6.0",
    "react-markdown": "^10.1.0",
    "react-resizable-panels": "^3.0.3",
    "remark-gfm": "^4.0.1",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.3.1",
    "tailwindcss-animate": "^1.0.7",
    "xstate": "^5.20.1",
    "zustand": "^5.0.6"
  },
  "devDependencies": {
    "@tauri-apps/cli": "^2",
    "@types/react": "^18.3.1",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "autoprefixer": "^10.4.21",
    "postcss": "^8.5.6",
    "tailwindcss": "^3.4.17",
    "typescript": "~5.6.2",
    "vite": "^6.0.3"
  }
}

```

## /postcss.config.js

```js path="/postcss.config.js" 
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
```

## /public/tauri.svg

```svg path="/public/tauri.svg" 
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

```

## /public/vite.svg

```svg path="/public/vite.svg" 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
```

## /scripts/notarize.sh

```sh path="/scripts/notarize.sh" 
#!/bin/bash

# 公证脚本
set -e

# 配置
APP_PATH="/Users/stone/Works/12Particles/Pivo/src-tauri/target/release/bundle/macos/Pivo.app"
DMG_PATH="/Users/stone/Works/12Particles/Pivo/src-tauri/target/release/bundle/dmg/Pivo_0.1.2_aarch64.dmg"
TEAM_ID="9WZQSGSX3A"

# 颜色
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m'

echo -e "${GREEN}开始公证流程...${NC}"

# 检查环境变量
if [ -z "$APPLE_ID" ]; then
    echo -e "${RED}错误: 请设置 APPLE_ID 环境变量${NC}"
    echo -e "${YELLOW}export APPLE_ID='your-apple-id@example.com'${NC}"
    exit 1
fi

if [ -z "$APPLE_PASSWORD" ]; then
    echo -e "${RED}错误: 请设置 APPLE_PASSWORD 环境变量${NC}"
    echo -e "${YELLOW}1. 访问 https://appleid.apple.com${NC}"
    echo -e "${YELLOW}2. 登录后,在安全性部分生成 App 专用密码${NC}"
    echo -e "${YELLOW}3. export APPLE_PASSWORD='xxxx-xxxx-xxxx-xxxx'${NC}"
    exit 1
fi

# 检查文件
if [ ! -d "$APP_PATH" ]; then
    echo -e "${RED}错误: 找不到应用 $APP_PATH${NC}"
    echo -e "${YELLOW}请先运行: pnpm tauri build${NC}"
    exit 1
fi

# 创建用于公证的 ZIP
echo -e "${YELLOW}创建 ZIP 文件...${NC}"
ZIP_PATH="${APP_PATH%.*}.zip"
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"

# 提交公证
echo -e "${YELLOW}提交公证 (这可能需要几分钟)...${NC}"
xcrun notarytool submit "$ZIP_PATH" \
    --apple-id "$APPLE_ID" \
    --password "$APPLE_PASSWORD" \
    --team-id "$TEAM_ID" \
    --wait \
    --verbose

# 如果公证成功,装订票据
if [ $? -eq 0 ]; then
    echo -e "${GREEN}公证成功!${NC}"
    
    echo -e "${YELLOW}装订公证票据到应用...${NC}"
    xcrun stapler staple "$APP_PATH"
    
    # 如果有 DMG,也装订到 DMG
    if [ -f "$DMG_PATH" ]; then
        echo -e "${YELLOW}装订公证票据到 DMG...${NC}"
        xcrun stapler staple "$DMG_PATH"
    fi
    
    # 清理 ZIP
    rm -f "$ZIP_PATH"
    
    echo -e "${GREEN}✅ 公证完成!${NC}"
    echo -e "${GREEN}应用现在可以在任何 Mac 上运行了。${NC}"
    
    # 验证
    echo -e "${YELLOW}验证公证状态...${NC}"
    spctl -a -vvv -t open --context context:primary-open "$APP_PATH"
    
else
    echo -e "${RED}公证失败!${NC}"
    echo -e "${YELLOW}请检查上面的错误信息${NC}"
    
    # 清理 ZIP
    rm -f "$ZIP_PATH"
    
    exit 1
fi
```

## /scripts/update-execution-types.mjs

```mjs path="/scripts/update-execution-types.mjs" 
import { readdir, readFile, writeFile } from 'fs/promises';
import { join } from 'path';

const importMappings = [
  // Replace imports from useExecutionStore to types/execution
  {
    from: /import\s*{\s*([^}]*UnifiedMessage[^}]*)\s*}\s*from\s*['"]@\/stores\/useExecutionStore['"]/g,
    to: (match, imports) => `import { ${imports} } from '@/types/execution'`
  },
  {
    from: /import\s*{\s*([^}]*UnifiedMessageType[^}]*)\s*}\s*from\s*['"]@\/stores\/useExecutionStore['"]/g,
    to: (match, imports) => `import { ${imports} } from '@/types/execution'`
  },
  {
    from: /import\s*{\s*([^}]*SystemMessageLevel[^}]*)\s*}\s*from\s*['"]@\/stores\/useExecutionStore['"]/g,
    to: (match, imports) => `import { ${imports} } from '@/types/execution'`
  },
  {
    from: /import\s*{\s*([^}]*AttemptExecutionState[^}]*)\s*}\s*from\s*['"]@\/stores\/useExecutionStore['"]/g,
    to: (match, imports) => `import { ${imports} } from '@/types/execution'`
  },
  {
    from: /import\s*{\s*([^}]*TaskExecutionSummary[^}]*)\s*}\s*from\s*['"]@\/stores\/useExecutionStore['"]/g,
    to: (match, imports) => `import { ${imports} } from '@/types/execution'`
  }
];

async function* getFiles(dir) {
  const entries = await readdir(dir, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      if (!['node_modules', '.git', 'dist', 'target'].includes(entry.name)) {
        yield* getFiles(fullPath);
      }
    } else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
      yield fullPath;
    }
  }
}

async function updateImports() {
  const srcDir = './src';
  let updatedFiles = 0;
  const updatedFilesList = [];
  
  for await (const file of getFiles(srcDir)) {
    // Skip the useExecutionStore.ts file itself
    if (file.includes('useExecutionStore.ts')) continue;
    
    let content = await readFile(file, 'utf-8');
    let modified = false;
    
    for (const mapping of importMappings) {
      const newContent = content.replace(mapping.from, mapping.to);
      if (newContent !== content) {
        content = newContent;
        modified = true;
      }
    }
    
    if (modified) {
      await writeFile(file, content);
      updatedFilesList.push(file);
      updatedFiles++;
    }
  }
  
  console.log('Updated files:');
  updatedFilesList.forEach(file => console.log(`  ${file}`));
  console.log(`\nTotal files updated: ${updatedFiles}`);
}

updateImports().catch(console.error);
```

## /src-tauri/.gitignore

```gitignore path="/src-tauri/.gitignore" 
# Generated by Cargo
# will have compiled files and executables
/target/

# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

```

## /src-tauri/Cargo.toml

```toml path="/src-tauri/Cargo.toml" 
[package]
name = "pivo"
version = "0.1.2"
description = "A Tauri App"
authors = ["you"]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "pivo_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
tauri-plugin-clipboard-manager = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "sqlite", "migrate"] }
tokio = { version = "1.46.1", features = ["full"] }
uuid = { version = "1.17.0", features = ["v4", "serde"] }
chrono = { version = "0.4.41", features = ["serde"] }
tauri-plugin-dialog = "2.3.0"
tauri-plugin-fs = "2.4.0"
slug = "0.1.6"
deunicode = "1.6"
async-trait = "0.1.88"
reqwest = { version = "0.12.22", features = ["json", "native-tls-vendored"] }
lazy_static = "1.5.0"
git2 = { version = "0.19.0", features = ["vendored-openssl"] }
log = "0.4"
log4rs = { version = "1.3", features = ["file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] }
directories = "5.0"
regex = "1.11.1"
urlencoding = "2.1.3"
base64 = "0.22"
notify = "7.0"
rand = "0.8"
dirs = "5.0"
glob = "0.3"
walkdir = "2.5"
shell-escape = "0.1"
shell-words = "1.1"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29", features = ["signal"] }
libc = "0.2"


```

## /src-tauri/FIX_GIT_LFS_ENVIRONMENT.md

# Git LFS 环境变量问题修复

## 问题描述
在发布版本中(非通过 `pnpm tauri dev` 运行时),git-lfs 命令找不到。这是因为 macOS 应用程序启动时不会加载用户的 shell 环境变量,所以通过 Homebrew 安装的工具(如 git-lfs)不在 PATH 中。

## 解决方案
创建了一个通用的命令执行封装,确保在 macOS 上通过 login shell 执行命令,从而加载用户的完整环境变量。

## 修改内容

### 1. 创建命令执行工具模块
- 文件:`src/utils/command.rs`
- 功能:
  - `execute_command()` - 通用命令执行函数
  - `execute_git()` - 专门用于执行 git 命令
  - 在 macOS 上通过 `/bin/bash -l -c` 执行命令以加载用户环境

### 2. 更新的文件
- `src/services/git_service.rs` - 所有 git 命令改用 `execute_git()`
- `src/services/github_service.rs` - GitHub 相关的 git 操作
- `src/services/gitlab_service.rs` - GitLab 相关的 git 操作
- `src/commands/projects.rs` - 项目扫描中的 git 命令
- `src/commands/git.rs` - git 相关的 Tauri 命令

### 3. 添加的依赖
- `shell-escape = "0.1"` - 用于安全地转义 shell 命令参数

## 技术细节

### macOS 特定处理
```rust
#[cfg(target_os = "macos")]
{
    // 通过 login shell 执行命令
    let mut cmd = Command::new("/bin/bash");
    cmd.arg("-l")  // Login shell,加载 ~/.bash_profile 等
       .arg("-c")
       .arg(&shell_command);
}
```

### 其他平台
在非 macOS 平台上,命令直接执行,无需特殊处理。

## 测试
- 编译通过:`cargo check`
- 所有 git 相关功能应该在发布版本中正常工作
- git-lfs 等通过 Homebrew 安装的工具现在可以被正确找到

## 未来改进
如果其他服务(如 MCP 服务、编码代理等)也需要访问用户环境变量,可以使用相同的 `execute_command()` 函数。

## /src-tauri/build.rs

```rs path="/src-tauri/build.rs" 
fn main() {
    tauri_build::build()
}

```

## /src-tauri/capabilities/default.json

```json path="/src-tauri/capabilities/default.json" 
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "opener:default",
    "dialog:default",
    "dialog:allow-open",
    "dialog:allow-save",
    "dialog:allow-message",
    "dialog:allow-ask",
    "dialog:allow-confirm",
    "shell:allow-open",
    "fs:default",
    "fs:allow-read-text-file",
    "fs:allow-write-text-file",
    "fs:allow-read-file",
    "fs:allow-write-file",
    "fs:allow-exists",
    "fs:allow-mkdir",
    "fs:allow-create",
    "fs:allow-appdata-read",
    "fs:allow-appdata-write",
    "fs:allow-appdata-read-recursive",
    "fs:allow-appdata-write-recursive",
    "clipboard-manager:default",
    "clipboard-manager:allow-read-text",
    "clipboard-manager:allow-write-text",
    "core:webview:allow-create-webview-window"
  ]
}

```

## /src-tauri/capabilities/project-window.json

```json path="/src-tauri/capabilities/project-window.json" 
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "project-window",
  "description": "Capability for project windows",
  "windows": ["project-*"],
  "permissions": [
    "core:default",
    "opener:default",
    "dialog:default",
    "dialog:allow-open",
    "dialog:allow-save",
    "dialog:allow-message",
    "dialog:allow-ask",
    "dialog:allow-confirm",
    "shell:allow-open",
    "fs:default",
    "fs:allow-read-text-file",
    "fs:allow-write-text-file",
    "fs:allow-read-file",
    "fs:allow-write-file",
    "fs:allow-exists",
    "fs:allow-mkdir",
    "fs:allow-create",
    "fs:allow-appdata-read",
    "fs:allow-appdata-write",
    "fs:allow-appdata-read-recursive",
    "fs:allow-appdata-write-recursive",
    "clipboard-manager:default",
    "clipboard-manager:allow-read-text",
    "clipboard-manager:allow-write-text"
  ]
}
```

## /src-tauri/entitlements.plist

```plist path="/src-tauri/entitlements.plist" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- App Sandbox disabled - required for developer tools functionality -->
    <key>com.apple.security.app-sandbox</key>
    <false/>
    
    <!-- Network permissions -->
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
    
    <!-- File permissions for user-selected files -->
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.files.downloads.read-write</key>
    <true/>
    
    <!-- Required for Hardened Runtime -->
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    
    <!-- Process spawning -->
    <key>com.apple.security.inherit</key>
    <true/>
    
</dict>
</plist>
```

## /src-tauri/icons/128x128.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/128x128.png

## /src-tauri/icons/128x128@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/128x128@2x.png

## /src-tauri/icons/32x32.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/32x32.png

## /src-tauri/icons/64x64.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/64x64.png

## /src-tauri/icons/Square107x107Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square107x107Logo.png

## /src-tauri/icons/Square142x142Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square142x142Logo.png

## /src-tauri/icons/Square150x150Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square150x150Logo.png

## /src-tauri/icons/Square284x284Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square284x284Logo.png

## /src-tauri/icons/Square30x30Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square30x30Logo.png

## /src-tauri/icons/Square310x310Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square310x310Logo.png

## /src-tauri/icons/Square44x44Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square44x44Logo.png

## /src-tauri/icons/Square71x71Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square71x71Logo.png

## /src-tauri/icons/Square89x89Logo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/Square89x89Logo.png

## /src-tauri/icons/StoreLogo.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/StoreLogo.png

## /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png

## /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png

## /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png

## /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png

## /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png

## /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png

## /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png

## /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png

## /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png

## /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png

## /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png

## /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png

## /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png

## /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png

## /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png

## /src-tauri/icons/icon.icns

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/icon.icns

## /src-tauri/icons/icon.ico

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/icon.ico

## /src-tauri/icons/icon.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/icon.png

## /src-tauri/icons/icon_backup.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/icon_backup.png

## /src-tauri/icons/ios/AppIcon-20x20@1x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-20x20@1x.png

## /src-tauri/icons/ios/AppIcon-20x20@2x-1.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-20x20@2x-1.png

## /src-tauri/icons/ios/AppIcon-20x20@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-20x20@2x.png

## /src-tauri/icons/ios/AppIcon-20x20@3x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-20x20@3x.png

## /src-tauri/icons/ios/AppIcon-29x29@1x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-29x29@1x.png

## /src-tauri/icons/ios/AppIcon-29x29@2x-1.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-29x29@2x-1.png

## /src-tauri/icons/ios/AppIcon-29x29@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-29x29@2x.png

## /src-tauri/icons/ios/AppIcon-29x29@3x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-29x29@3x.png

## /src-tauri/icons/ios/AppIcon-40x40@1x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-40x40@1x.png

## /src-tauri/icons/ios/AppIcon-40x40@2x-1.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-40x40@2x-1.png

## /src-tauri/icons/ios/AppIcon-40x40@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-40x40@2x.png

## /src-tauri/icons/ios/AppIcon-40x40@3x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-40x40@3x.png

## /src-tauri/icons/ios/AppIcon-512@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-512@2x.png

## /src-tauri/icons/ios/AppIcon-60x60@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-60x60@2x.png

## /src-tauri/icons/ios/AppIcon-60x60@3x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-60x60@3x.png

## /src-tauri/icons/ios/AppIcon-76x76@1x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-76x76@1x.png

## /src-tauri/icons/ios/AppIcon-76x76@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-76x76@2x.png

## /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png

Binary file available at https://raw.githubusercontent.com/12Particles/Pivo/refs/heads/main/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png

## /src-tauri/migrations/001_init.sql

```sql path="/src-tauri/migrations/001_init.sql" 
-- Enable foreign key support
PRAGMA foreign_keys = ON;

-- Projects table
CREATE TABLE IF NOT EXISTS projects (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    path TEXT NOT NULL,
    git_repo TEXT,
    setup_script TEXT,
    dev_script TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Tasks table
CREATE TABLE IF NOT EXISTS tasks (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL,
    title TEXT NOT NULL,
    description TEXT,
    status TEXT NOT NULL DEFAULT 'backlog',
    priority TEXT NOT NULL DEFAULT 'medium',
    parent_task_id TEXT,
    assignee TEXT,
    tags TEXT, -- JSON array
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
    FOREIGN KEY (parent_task_id) REFERENCES tasks(id)
);

-- Task attempts table
CREATE TABLE IF NOT EXISTS task_attempts (
    id TEXT PRIMARY KEY,
    task_id TEXT NOT NULL,
    worktree_path TEXT NOT NULL,
    branch TEXT NOT NULL,
    base_branch TEXT NOT NULL DEFAULT 'main',
    executor TEXT,
    status TEXT NOT NULL DEFAULT 'running',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    completed_at DATETIME,
    FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
);

-- Execution processes table
CREATE TABLE IF NOT EXISTS execution_processes (
    id TEXT PRIMARY KEY,
    task_attempt_id TEXT NOT NULL,
    process_type TEXT NOT NULL,
    executor_type TEXT,
    status TEXT NOT NULL DEFAULT 'running',
    command TEXT NOT NULL,
    args TEXT,
    working_directory TEXT NOT NULL,
    stdout TEXT,
    stderr TEXT,
    exit_code INTEGER,
    started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    completed_at DATETIME,
    FOREIGN KEY (task_attempt_id) REFERENCES task_attempts(id) ON DELETE CASCADE
);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_task_attempts_task_id ON task_attempts(task_id);
CREATE INDEX IF NOT EXISTS idx_execution_processes_task_attempt_id ON execution_processes(task_attempt_id);

-- Triggers for updated_at
CREATE TRIGGER update_projects_updated_at AFTER UPDATE ON projects
BEGIN
    UPDATE projects SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

CREATE TRIGGER update_tasks_updated_at AFTER UPDATE ON tasks
BEGIN
    UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
```

## /src-tauri/migrations/002_fix_enum_values.sql

```sql path="/src-tauri/migrations/002_fix_enum_values.sql" 
-- Update existing tasks with lowercase status values to capitalized values
UPDATE tasks SET status = 'Backlog' WHERE status = 'backlog';
UPDATE tasks SET status = 'Working' WHERE status = 'working';
UPDATE tasks SET status = 'Reviewing' WHERE status = 'reviewing';
UPDATE tasks SET status = 'Done' WHERE status = 'done';
UPDATE tasks SET status = 'Cancelled' WHERE status = 'cancelled';

-- Update existing tasks with lowercase priority values to capitalized values
UPDATE tasks SET priority = 'Low' WHERE priority = 'low';
UPDATE tasks SET priority = 'Medium' WHERE priority = 'medium';
UPDATE tasks SET priority = 'High' WHERE priority = 'high';
UPDATE tasks SET priority = 'Urgent' WHERE priority = 'urgent';

-- Update default values in the schema
-- SQLite doesn't support ALTER COLUMN, so we need to recreate the table

-- Create temporary table with new defaults
CREATE TABLE tasks_new (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL,
    title TEXT NOT NULL,
    description TEXT,
    status TEXT NOT NULL DEFAULT 'Backlog',
    priority TEXT NOT NULL DEFAULT 'Medium',
    parent_task_id TEXT,
    assignee TEXT,
    tags TEXT, -- JSON array
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
    FOREIGN KEY (parent_task_id) REFERENCES tasks(id)
);

-- Copy data from old table
INSERT INTO tasks_new SELECT * FROM tasks;

-- Drop old table
DROP TABLE tasks;

-- Rename new table
ALTER TABLE tasks_new RENAME TO tasks;

-- Recreate indexes
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);

-- Recreate trigger
CREATE TRIGGER update_tasks_updated_at AFTER UPDATE ON tasks
BEGIN
    UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
```

## /src-tauri/migrations/003_add_attempt_conversations.sql

```sql path="/src-tauri/migrations/003_add_attempt_conversations.sql" 
-- Attempt conversations table
CREATE TABLE IF NOT EXISTS attempt_conversations (
    id TEXT PRIMARY KEY,
    task_attempt_id TEXT NOT NULL UNIQUE,
    messages TEXT NOT NULL, -- JSON array of conversation messages
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (task_attempt_id) REFERENCES task_attempts(id) ON DELETE CASCADE
);

-- Index for quick lookup by task_attempt_id
CREATE INDEX idx_attempt_conversations_task_attempt_id ON attempt_conversations(task_attempt_id);

-- Trigger for updated_at
CREATE TRIGGER update_attempt_conversations_updated_at AFTER UPDATE ON attempt_conversations
BEGIN
    UPDATE attempt_conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
```

## /src-tauri/migrations/20250121_add_git_tracking_fields.sql

```sql path="/src-tauri/migrations/20250121_add_git_tracking_fields.sql" 
-- Add Git tracking fields to task_attempts table
ALTER TABLE task_attempts ADD COLUMN base_commit TEXT;
ALTER TABLE task_attempts ADD COLUMN last_sync_commit TEXT;
ALTER TABLE task_attempts ADD COLUMN last_sync_at DATETIME;
```

## /src-tauri/migrations/20250122_add_merge_requests_table.sql

```sql path="/src-tauri/migrations/20250122_add_merge_requests_table.sql" 
-- Create merge_requests table
CREATE TABLE IF NOT EXISTS merge_requests (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    task_attempt_id TEXT NOT NULL,
    provider TEXT NOT NULL, -- 'gitlab' or 'github'
    mr_id INTEGER NOT NULL, -- GitLab/GitHub MR/PR ID
    mr_iid INTEGER NOT NULL, -- GitLab IID (internal ID)
    mr_number INTEGER NOT NULL, -- MR/PR number
    title TEXT NOT NULL,
    description TEXT,
    state TEXT NOT NULL, -- opened, closed, merged, locked
    source_branch TEXT NOT NULL,
    target_branch TEXT NOT NULL,
    web_url TEXT NOT NULL,
    merge_status TEXT, -- can_be_merged, cannot_be_merged, etc.
    has_conflicts BOOLEAN DEFAULT 0,
    pipeline_status TEXT, -- success, failed, running, etc.
    pipeline_url TEXT,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    merged_at DATETIME,
    synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (task_attempt_id) REFERENCES task_attempts(id) ON DELETE CASCADE,
    UNIQUE(provider, mr_id)
);

-- Create index for faster lookups
CREATE INDEX idx_merge_requests_task_attempt ON merge_requests(task_attempt_id);
CREATE INDEX idx_merge_requests_state ON merge_requests(state);
CREATE INDEX idx_merge_requests_provider_number ON merge_requests(provider, mr_number);

-- Create app_config table for storing configuration
CREATE TABLE IF NOT EXISTS app_config (
    id INTEGER PRIMARY KEY,
    key TEXT NOT NULL UNIQUE,
    value TEXT NOT NULL,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```

## /src-tauri/migrations/20250123_add_git_provider_to_projects.sql

```sql path="/src-tauri/migrations/20250123_add_git_provider_to_projects.sql" 
-- Add git_provider column to projects table
ALTER TABLE projects ADD COLUMN git_provider TEXT DEFAULT NULL;

-- Update existing projects based on git_repo URL
UPDATE projects 
SET git_provider = CASE 
    WHEN git_repo LIKE '%github.com%' THEN 'github'
    WHEN git_repo IS NOT NULL THEN 'gitlab'
    ELSE NULL
END
WHERE git_repo IS NOT NULL;
```

## /src-tauri/migrations/20250124_add_claude_session_id.sql

```sql path="/src-tauri/migrations/20250124_add_claude_session_id.sql" 
-- Add claude_session_id to task_attempts table
ALTER TABLE task_attempts
ADD COLUMN claude_session_id TEXT;
```

## /src-tauri/migrations/20250128_add_last_opened_to_projects.sql

```sql path="/src-tauri/migrations/20250128_add_last_opened_to_projects.sql" 
-- Add last_opened field to projects table for tracking recently opened projects
ALTER TABLE projects ADD COLUMN last_opened DATETIME;

-- Create index for better performance when querying recent projects
CREATE INDEX idx_projects_last_opened ON projects(last_opened DESC);
```

## /src-tauri/migrations/20250201_add_main_branch_to_projects.sql

```sql path="/src-tauri/migrations/20250201_add_main_branch_to_projects.sql" 
-- Add main_branch column to projects table
ALTER TABLE projects ADD COLUMN main_branch TEXT NOT NULL DEFAULT 'main';
```

## /src-tauri/src/commands/cli.rs

```rs path="/src-tauri/src/commands/cli.rs" 
use crate::services::coding_agent_executor::{
    CodingAgentExecutorService, CodingAgentExecution, CodingAgentType
};
use std::sync::Arc;
use tauri::State;
use std::fs;
use base64::{Engine as _, engine::general_purpose};

pub struct CliState {
    pub service: Arc<CodingAgentExecutorService>,
}

#[tauri::command]
pub async fn execute_prompt(
    state: State<'_, CliState>,
    prompt: String,
    task_id: String,
    attempt_id: String,
    working_directory: String,
    agent_type: CodingAgentType,
    resume_session_id: Option<String>,
) -> Result<CodingAgentExecution, String> {
    state.service.execute_prompt(
        &prompt,
        &task_id,
        &attempt_id,
        &working_directory,
        agent_type,
        resume_session_id,
    ).await
}


#[tauri::command]
pub async fn configure_claude_api_key(
    state: State<'_, CliState>,
    api_key: String,
) -> Result<(), String> {
    state.service.configure_claude_api_key(&api_key)
}

#[tauri::command]
pub async fn configure_gemini_api_key(
    state: State<'_, CliState>,
    api_key: String,
) -> Result<(), String> {
    state.service.configure_gemini_api_key(&api_key)
}

#[tauri::command]
pub async fn save_images_to_temp(
    base64_images: Vec<String>,
) -> Result<Vec<String>, String> {
    let mut paths = Vec::new();
    let temp_dir = std::env::temp_dir();
    
    for (index, base64_image) in base64_images.iter().enumerate() {
        // Extract the data part after "data:image/png;base64," or similar
        let data_part = if let Some(comma_pos) = base64_image.find(',') {
            &base64_image[comma_pos + 1..]
        } else {
            base64_image
        };
        
        // Decode base64
        let image_data = general_purpose::STANDARD
            .decode(data_part)
            .map_err(|e| format!("Failed to decode base64: {}", e))?;
        
        // Generate filename
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_err(|e| format!("Time error: {}", e))?
            .as_millis();
        let filename = format!("pivo_image_{}_{}.png", timestamp, index);
        let file_path = temp_dir.join(&filename);
        
        // Write to file
        fs::write(&file_path, image_data)
            .map_err(|e| format!("Failed to write image file: {}", e))?;
        
        // Add path to results
        paths.push(file_path.to_string_lossy().to_string());
    }
    
    Ok(paths)
}

#[tauri::command]
pub async fn get_running_tasks(
    state: State<'_, CliState>,
) -> Result<Vec<String>, String> {
    Ok(state.service.get_running_tasks())
}
```

## /src-tauri/src/commands/command.rs

```rs path="/src-tauri/src/commands/command.rs" 
use crate::services::CommandService;
use crate::models::CommandSearchResult;

#[tauri::command]
pub async fn search_commands(
    project_path: String,
    query: Option<String>,
    limit: Option<usize>,
) -> Result<CommandSearchResult, String> {
    let service = CommandService::new();
    let limit = limit.unwrap_or(5);
    
    service.search_commands(
        &project_path,
        query.as_deref(),
        limit
    )
}

#[tauri::command]
pub async fn get_command_content(
    command_path: String,
) -> Result<String, String> {
    let service = CommandService::new();
    service.get_command_content(&command_path)
}
```

## /src-tauri/src/commands/dev_server.rs

```rs path="/src-tauri/src/commands/dev_server.rs" 
use std::collections::HashMap;
use std::process::Stdio;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, State};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
use uuid::Uuid;

pub struct DevServerManager {
    processes: Arc<Mutex<HashMap<String, Child>>>,
}

impl DevServerManager {
    pub fn new() -> Self {
        Self {
            processes: Arc::new(Mutex::new(HashMap::new())),
        }
    }
}

#[tauri::command]
pub async fn start_dev_server(
    app_handle: AppHandle,
    dev_manager: State<'_, DevServerManager>,
    project_path: String,
    command: String,
) -> Result<serde_json::Value, String> {
    // Generate a unique process ID
    let process_id = Uuid::new_v4().to_string();
    
    // For complex commands like 'pnpm tauri dev', we need to run them through a shell
    // This ensures that npm/pnpm/yarn scripts work correctly
    let mut cmd;
    
    #[cfg(target_os = "windows")]
    {
        cmd = Command::new("cmd");
        cmd.args(&["/C", &command]);
    }
    
    #[cfg(not(target_os = "windows"))]
    {
        cmd = Command::new("sh");
        cmd.args(&["-c", &command]);
        
        // On macOS, ensure we have access to user's PATH
        #[cfg(target_os = "macos")]
        {
            // Get the user's shell PATH for better compatibility
            if let Ok(output) = std::process::Command::new("sh")
                .args(&["-l", "-c", "echo $PATH"])
                .output()
            {
                if let Ok(path) = String::from_utf8(output.stdout) {
                    cmd.env("PATH", path.trim());
                }
            } else if let Ok(path) = std::env::var("PATH") {
                let homebrew_path = "/opt/homebrew/bin:/usr/local/bin";
                let full_path = format!("{}:{}", homebrew_path, path);
                cmd.env("PATH", full_path);
            }
        }
    }
    
    cmd.current_dir(&project_path)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .stdin(Stdio::null())
        .kill_on_drop(true);
    
    // Spawn the process
    eprintln!("[DEV_SERVER] About to spawn command: {}", command);
    let mut child = cmd.spawn().map_err(|e| format!("Failed to start dev server: {}", e))?;
    
    let pid = child.id().unwrap_or(0);
    eprintln!("[DEV_SERVER] Process spawned successfully with PID: {}", pid);
    let proc_id = process_id.clone();
    let app = app_handle.clone();
    
    // Handle stdout
    if let Some(stdout) = child.stdout.take() {
        let reader = BufReader::new(stdout);
        let proc_id = proc_id.clone();
        let app = app.clone();
        
        tokio::spawn(async move {
            let mut lines = reader.lines();
            eprintln!("[DEV_SERVER] Started stdout reader for process {}", proc_id);
            while let Ok(Some(line)) = lines.next_line().await {
                eprintln!("[DEV_SERVER] STDOUT: {}", line);
                let emit_result = app.emit("dev-server-output", serde_json::json!({
                    "process_id": proc_id,
                    "type": "stdout",
                    "data": line
                }));
                if let Err(e) = emit_result {
                    eprintln!("[DEV_SERVER] Failed to emit stdout: {}", e);
                }
            }
            eprintln!("[DEV_SERVER] STDOUT reader ended for process {}", proc_id);
        });
    }
    
    // Handle stderr
    if let Some(stderr) = child.stderr.take() {
        let reader = BufReader::new(stderr);
        let proc_id = proc_id.clone();
        let app = app.clone();
        
        tokio::spawn(async move {
            let mut lines = reader.lines();
            eprintln!("[DEV_SERVER] Started stderr reader for process {}", proc_id);
            while let Ok(Some(line)) = lines.next_line().await {
                eprintln!("[DEV_SERVER] STDERR: {}", line);
                let emit_result = app.emit("dev-server-output", serde_json::json!({
                    "process_id": proc_id,
                    "type": "stderr",
                    "data": line
                }));
                if let Err(e) = emit_result {
                    eprintln!("[DEV_SERVER] Failed to emit stderr: {}", e);
                }
            }
            eprintln!("[DEV_SERVER] STDERR reader ended for process {}", proc_id);
        });
    }
    
    // Store the child process first
    let mut processes = dev_manager.processes.lock().await;
    processes.insert(process_id.clone(), child);
    drop(processes); // Release the lock
    
    // Monitor process completion
    let proc_id_monitor = process_id.clone();
    let app_monitor = app_handle.clone();
    let manager = dev_manager.processes.clone();
    
    tokio::spawn(async move {
        // Wait a bit to ensure the process is properly started
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        
        loop {
            // Check if process still exists
            let should_wait = {
                let processes = manager.lock().await;
                processes.contains_key(&proc_id_monitor)
            };
            
            if !should_wait {
                break;
            }
            
            // Check process status
            let mut processes = manager.lock().await;
            if let Some(child) = processes.get_mut(&proc_id_monitor) {
                match child.try_wait() {
                    Ok(Some(status)) => {
                        // Process has exited
                        let exit_code = status.code();
                        processes.remove(&proc_id_monitor);
                        let _ = app_monitor.emit("dev-server-stopped", serde_json::json!({
                            "process_id": proc_id_monitor,
                            "exit_code": exit_code
                        }));
                        break;
                    }
                    Ok(None) => {
                        // Process is still running
                        drop(processes);
                        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
                    }
                    Err(e) => {
                        eprintln!("Error checking process status: {}", e);
                        processes.remove(&proc_id_monitor);
                        let _ = app_monitor.emit("dev-server-stopped", serde_json::json!({
                            "process_id": proc_id_monitor
                        }));
                        break;
                    }
                }
            } else {
                break;
            }
        }
    });
    
    Ok(serde_json::json!({
        "process_id": process_id,
        "pid": pid
    }))
}

#[tauri::command]
pub async fn stop_dev_server(
    app_handle: AppHandle,
    dev_manager: State<'_, DevServerManager>,
    process_id: String,
) -> Result<(), String> {
    let mut processes = dev_manager.processes.lock().await;
    
    if let Some(mut child) = processes.remove(&process_id) {
        // Try to kill the process and all its children
        // For shell-spawned processes, we need to be more aggressive
        
        #[cfg(unix)]
        {
            if let Some(pid) = child.id() {
                unsafe {
                    // First, try to kill the process group
                    // The shell typically creates a new process group
                    let pgid = pid as i32;
                    
                    // Send SIGTERM to the process itself
                    libc::kill(pgid, libc::SIGTERM);
                    
                    // Also try to kill as a process group (negative PID)
                    libc::kill(-pgid, libc::SIGTERM);
                    
                    // Give processes time to clean up
                    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
                    
                    // Check if process is still running
                    match child.try_wait() {
                        Ok(None) => {
                            // Still running, force kill
                            libc::kill(pgid, libc::SIGKILL);
                            libc::kill(-pgid, libc::SIGKILL);
                            let _ = child.kill().await;
                        }
                        _ => {}
                    }
                }
            } else {
                // Fallback to normal kill
                let _ = child.kill().await;
            }
        }
        
        #[cfg(not(unix))]
        {
            // On Windows, kill the process tree
            if let Some(pid) = child.id() {
                // Use taskkill to kill the process tree
                let _ = std::process::Command::new("taskkill")
                    .args(&["/F", "/T", "/PID", &pid.to_string()])
                    .output();
            }
            // Also try normal kill as fallback
            let _ = child.kill().await;
        }
        
        // Emit stopped event
        let _ = app_handle.emit("dev-server-stopped", serde_json::json!({
            "process_id": process_id,
            "exit_code": -1
        }));
        
        Ok(())
    } else {
        // Process not found, might have already stopped
        // Still emit the stopped event to update UI
        let _ = app_handle.emit("dev-server-stopped", serde_json::json!({
            "process_id": process_id
        }));
        Ok(())
    }
}

#[tauri::command]
pub async fn get_dev_server_status(
    dev_manager: State<'_, DevServerManager>,
    process_id: String,
) -> Result<String, String> {
    let processes = dev_manager.processes.lock().await;
    
    if processes.contains_key(&process_id) {
        Ok("running".to_string())
    } else {
        Ok("stopped".to_string())
    }
}
```

## /src-tauri/src/commands/filesystem.rs

```rs path="/src-tauri/src/commands/filesystem.rs" 
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::SystemTime;
use walkdir::WalkDir;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileSearchResult {
    pub path: String,
    pub name: String,
    pub relative_path: String,
    pub modified_time: u64,
    pub is_directory: bool,
}

#[tauri::command]
pub async fn search_project_files(
    project_path: String,
    query: String,
    max_results: Option<usize>,
) -> Result<Vec<FileSearchResult>, String> {
    let max_results = max_results.unwrap_or(5);
    let project_path = PathBuf::from(&project_path);
    
    if !project_path.exists() || !project_path.is_dir() {
        return Err("Invalid project path".to_string());
    }
    
    let query_lower = query.to_lowercase();
    let mut results = Vec::new();
    
    // Common directories to ignore
    let ignore_dirs = vec![
        ".git", "node_modules", "target", "dist", "build", 
        ".next", ".vscode", ".idea", "__pycache__", ".cache",
        "coverage", ".nyc_output", "vendor"
    ];
    
    // Walk through the directory tree
    for entry in WalkDir::new(&project_path)
        .follow_links(true)
        .into_iter()
        .filter_entry(|e| {
            // Filter out ignored directories
            let file_name = e.file_name().to_string_lossy();
            !ignore_dirs.iter().any(|ignored| file_name == *ignored)
        })
        .filter_map(|e| e.ok())
    {
        let path = entry.path();
        let file_name = path.file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("")
            .to_string();
        
        // Check if the file name contains the query (case-insensitive)
        if file_name.to_lowercase().contains(&query_lower) {
            // Get relative path
            let relative_path = path.strip_prefix(&project_path)
                .unwrap_or(path)
                .to_string_lossy()
                .to_string();
            
            // Get modified time
            let modified_time = entry.metadata()
                .ok()
                .and_then(|m| m.modified().ok())
                .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
                .map(|d| d.as_secs())
                .unwrap_or(0);
            
            results.push(FileSearchResult {
                path: path.to_string_lossy().to_string(),
                name: file_name,
                relative_path,
                modified_time,
                is_directory: path.is_dir(),
            });
        }
    }
    
    // Sort by modified time (most recent first)
    results.sort_by(|a, b| b.modified_time.cmp(&a.modified_time));
    
    // Limit results
    results.truncate(max_results);
    
    Ok(results)
}

#[tauri::command]
pub async fn search_files_from_current_dir(
    current_path: String,
    query: String,
    max_results: Option<usize>,
) -> Result<Vec<FileSearchResult>, String> {
    let max_results = max_results.unwrap_or(5);
    let current_path = PathBuf::from(&current_path);
    
    if !current_path.exists() || !current_path.is_dir() {
        return Err("Invalid current path".to_string());
    }
    
    // Find the project root by looking for .git directory
    let mut project_root = current_path.clone();
    let mut found_git = false;
    
    while let Some(parent) = project_root.parent() {
        if project_root.join(".git").exists() {
            found_git = true;
            break;
        }
        project_root = parent.to_path_buf();
    }
    
    // If no .git found, use the current directory
    if !found_git {
        project_root = current_path;
    }
    
    // Use the search_project_files function with the determined project root
    search_project_files(
        project_root.to_string_lossy().to_string(),
        query,
        Some(max_results),
    ).await
}
```

## /src-tauri/src/commands/git.rs

```rs path="/src-tauri/src/commands/git.rs" 
use crate::models::{DiffMode, DiffResult, RebaseStatus};
use crate::services::GitService;
use std::path::Path;

// Original git commands
#[tauri::command]
pub async fn create_worktree(
    repo_path: String,
    branch_name: String,
    base_branch: String,
) -> Result<String, String> {
    let git_service = GitService::new();
    let worktree_path = git_service.create_worktree(
        Path::new(&repo_path),
        &branch_name,
        &base_branch,
    )?;
    Ok(worktree_path.to_string_lossy().to_string())
}

#[tauri::command]
pub async fn remove_worktree(
    repo_path: String,
    worktree_path: String,
) -> Result<(), String> {
    let git_service = GitService::new();
    git_service.remove_worktree(Path::new(&repo_path), Path::new(&worktree_path))
}

#[tauri::command]
pub async fn get_current_branch(repo_path: String) -> Result<String, String> {
    GitService::get_current_branch(Path::new(&repo_path))
}

#[tauri::command]
pub async fn list_branches(repo_path: String) -> Result<Vec<String>, String> {
    GitService::list_branches(Path::new(&repo_path))
}

#[tauri::command]
pub async fn get_git_status(repo_path: String) -> Result<crate::services::GitStatus, String> {
    let git_service = GitService::new();
    git_service.get_status(Path::new(&repo_path))
}

#[tauri::command]
pub async fn stage_files(repo_path: String, files: Vec<String>) -> Result<(), String> {
    let file_refs: Vec<&str> = files.iter().map(|s| s.as_str()).collect();
    GitService::stage_files(Path::new(&repo_path), &file_refs)
}

#[tauri::command]
pub async fn commit_changes(repo_path: String, message: String) -> Result<String, String> {
    GitService::commit(Path::new(&repo_path), &message)
}

#[tauri::command]
pub async fn push_branch(repo_path: String, branch: String, force: bool) -> Result<(), String> {
    GitService::push(Path::new(&repo_path), &branch, force)
}

#[tauri::command]
pub async fn get_diff(repo_path: String, staged: bool) -> Result<String, String> {
    GitService::get_diff(Path::new(&repo_path), staged)
}

#[tauri::command]
pub async fn list_all_files(repo_path: String) -> Result<Vec<String>, String> {
    use std::fs;
    use std::path::PathBuf;
    
    let repo_path_buf = PathBuf::from(&repo_path);
    let mut all_files = Vec::new();
    
    // Function to recursively collect files
    fn collect_files(dir: &Path, base_path: &Path, files: &mut Vec<String>) -> Result<(), String> {
        if let Ok(entries) = fs::read_dir(dir) {
            for entry in entries {
                if let Ok(entry) = entry {
                    let path = entry.path();
                    let file_name = path.file_name()
                        .and_then(|n| n.to_str())
                        .unwrap_or("");
                    
                    // Skip hidden files, .git directory, and common build/dependency directories
                    if file_name.starts_with('.') 
                        || file_name == "node_modules" 
                        || file_name == "target"
                        || file_name == "build"
                        || file_name == "dist" {
                        continue;
                    }
                    
                    if path.is_dir() {
                        collect_files(&path, base_path, files)?;
                    } else if path.is_file() {
                        // Get relative path from repo root
                        if let Ok(relative_path) = path.strip_prefix(base_path) {
                            if let Some(path_str) = relative_path.to_str() {
                                files.push(path_str.to_string());
                            }
                        }
                    }
                }
            }
        }
        Ok(())
    }
    
    // Collect all files recursively
    collect_files(&repo_path_buf, &repo_path_buf, &mut all_files)
        .map_err(|e| format!("Failed to list files: {}", e))?;
    
    // Sort files for consistent ordering
    all_files.sort();
    
    log::info!("[list_all_files] Found {} files in {}", all_files.len(), repo_path);
    if all_files.len() <= 10 {
        log::info!("[list_all_files] Files: {:?}", all_files);
    } else {
        log::info!("[list_all_files] First 10 files: {:?}", &all_files[..10]);
    }
    
    Ok(all_files)
}

#[tauri::command]
pub async fn read_file_content(repo_path: String, file_path: String) -> Result<String, String> {
    let full_path = Path::new(&repo_path).join(&file_path);
    std::fs::read_to_string(&full_path)
        .map_err(|e| format!("Failed to read file: {}", e))
}

#[tauri::command]
pub async fn get_file_from_ref(repo_path: String, file_ref: String) -> Result<String, String> {
    GitService::get_file_from_ref(Path::new(&repo_path), &file_ref)
}

// New enhanced diff commands
#[tauri::command]
pub async fn get_git_diff(
    worktree_path: String,
    mode: DiffMode,
) -> Result<DiffResult, String> {
    let git_service = GitService::new();
    git_service.get_comprehensive_diff(Path::new(&worktree_path), mode)
}

#[tauri::command]
pub async fn check_rebase_status(
    worktree_path: String,
    base_branch: String,
) -> Result<RebaseStatus, String> {
    let git_service = GitService::new();
    git_service.check_rebase_status(Path::new(&worktree_path), &base_branch)
}

#[tauri::command]
pub async fn get_branch_commit(
    repo_path: String,
    branch: String,
) -> Result<String, String> {
    let git_service = GitService::new();
    git_service.get_branch_commit(Path::new(&repo_path), &branch)
}
```

## /src-tauri/src/commands/git_info.rs

```rs path="/src-tauri/src/commands/git_info.rs" 
use crate::services::git_info::{extract_git_info, GitInfo};
use tauri::command;

#[command]
pub async fn extract_git_info_from_path(path: String) -> Result<GitInfo, String> {
    Ok(extract_git_info(&path))
}
```

## /src-tauri/src/commands/github.rs

```rs path="/src-tauri/src/commands/github.rs" 
use crate::models::{GitHubConfig, MergeRequestInfo, GitRemoteInfo, CreateMergeRequestData, MergeRequestState};
use crate::services::{ConfigService, GitHubService, GitPlatformService};
use crate::AppState;
use std::sync::Arc;
use tauri::State;
use tokio::sync::Mutex;
use chrono::Utc;
use std::str::FromStr;

#[tauri::command]
pub async fn get_github_config(
    state: State<'_, Arc<Mutex<ConfigService>>>,
) -> Result<Option<GitHubConfig>, String> {
    let config_service = state.lock().await;
    Ok(config_service.get_github_config().cloned())
}

#[tauri::command]
pub async fn update_github_config(
    state: State<'_, Arc<Mutex<ConfigService>>>,
    config: GitHubConfig,
) -> Result<(), String> {
    let mut config_service = state.lock().await;
    config_service.update_github_config(config).await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn create_github_pr(
    config_state: State<'_, Arc<Mutex<ConfigService>>>,
    app_state: State<'_, AppState>,
    task_attempt_id: String,
    remote_url: String,
    title: String,
    description: String,
    source_branch: String,
    target_branch: String,
) -> Result<MergeRequestInfo, String> {
    let config_service = config_state.lock().await;
    let github_config = config_service.get_github_config()
        .ok_or("GitHub not configured")?
        .clone();
    
    drop(config_service); // Release lock
    
    let remote_info = GitRemoteInfo::from_remote_url(&remote_url)
        .ok_or("Invalid remote URL")?;
    
    let github_service = GitHubService::new(github_config);
    let pr_info = github_service.create_merge_request(
        &remote_info,
        &title,
        &description,
        &source_branch,
        &target_branch,
    ).await?;
    
    // Store PR in database
    let pr_data = CreateMergeRequestData {
        task_attempt_id,
        provider: "github".to_string(),
        mr_id: pr_info.id,
        mr_iid: pr_info.iid,
        mr_number: pr_info.number,
        title: pr_info.title.clone(),
        description: pr_info.description.clone(),
        state: format!("{:?}", pr_info.state).to_lowercase(),
        source_branch: pr_info.source_branch.clone(),
        target_branch: pr_info.target_branch.clone(),
        web_url: pr_info.web_url.clone(),
        merge_status: pr_info.merge_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        has_conflicts: pr_info.has_conflicts,
        pipeline_status: pr_info.pipeline_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        pipeline_url: None,
        created_at: Utc::now(),
        updated_at: Utc::now(),
        merged_at: None,
    };
    
    app_state.merge_request_service.create_merge_request(pr_data)
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(pr_info)
}

#[tauri::command]
pub async fn get_github_pr_status(
    config_state: State<'_, Arc<Mutex<ConfigService>>>,
    app_state: State<'_, AppState>,
    task_attempt_id: String,
    remote_url: String,
    pr_number: i64,
) -> Result<MergeRequestInfo, String> {
    let config_service = config_state.lock().await;
    let github_config = config_service.get_github_config()
        .ok_or("GitHub not configured")?
        .clone();
    
    drop(config_service); // Release lock
    
    let remote_info = GitRemoteInfo::from_remote_url(&remote_url)
        .ok_or("Invalid remote URL")?;
    
    let github_service = GitHubService::new(github_config);
    let pr_info = github_service.update_merge_request_status(&remote_info, pr_number).await?;
    
    // Sync PR to database
    let pr_data = CreateMergeRequestData {
        task_attempt_id,
        provider: "github".to_string(),
        mr_id: pr_info.id,
        mr_iid: pr_info.iid,
        mr_number: pr_info.number,
        title: pr_info.title.clone(),
        description: pr_info.description.clone(),
        state: format!("{:?}", pr_info.state).to_lowercase(),
        source_branch: pr_info.source_branch.clone(),
        target_branch: pr_info.target_branch.clone(),
        web_url: pr_info.web_url.clone(),
        merge_status: pr_info.merge_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        has_conflicts: pr_info.has_conflicts,
        pipeline_status: pr_info.pipeline_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        pipeline_url: None,
        created_at: Utc::now(),
        updated_at: Utc::now(),
        merged_at: None,
    };
    
    app_state.merge_request_service.sync_merge_request_from_api("github", pr_info.id, pr_data)
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(pr_info)
}

#[tauri::command]
pub async fn push_to_github(
    config_state: State<'_, Arc<Mutex<ConfigService>>>,
    repo_path: String,
    branch: String,
    force: bool,
) -> Result<(), String> {
    let config_service = config_state.lock().await;
    let github_config = config_service.get_github_config()
        .ok_or("GitHub not configured")?
        .clone();
    
    drop(config_service); // Release lock
    
    let github_service = GitHubService::new(github_config);
    
    // Verify token before attempting to push
    match github_service.verify_token().await {
        Ok(user_info) => {
            log::info!("GitHub token verified for user: {}", 
                user_info.get("login").and_then(|v| v.as_str()).unwrap_or("unknown"));
        },
        Err(e) => {
            log::error!("Failed to verify GitHub token: {}", e);
            return Err(format!("GitHub token verification failed: {}", e));
        }
    }
    
    // List organizations the user has access to
    match github_service.list_user_orgs().await {
        Ok(orgs) => {
            log::info!("User has access to organizations: {:?}", orgs);
            
            // Check specific access to 12Particles org
            if !orgs.contains(&"12Particles".to_string()) {
                log::warn!("User does not have access to 12Particles organization");
                log::warn!("Please grant OAuth App access to the organization:");
                log::warn!("1. Go to: https://github.com/settings/connections/applications/{}", "Ov23limL5nB8uf0tDrQX");
                log::warn!("2. Find '12Particles' in the organization access section");
                log::warn!("3. Click 'Grant' or 'Request' access");
            }
        },
        Err(e) => {
            log::error!("Failed to list user organizations: {}", e);
        }
    }
    
    // Check specific org access
    match github_service.check_org_access("12Particles").await {
        Ok(has_access) => {
            if !has_access {
                log::error!("No access to 12Particles organization");
                return Err("OAuth App does not have access to 12Particles organization. Please grant access at: https://github.com/settings/connections/applications/Ov23limL5nB8uf0tDrQX".to_string());
            }
        },
        Err(e) => {
            log::error!("Failed to check org access: {}", e);
        }
    }
    
    github_service.push_branch(&repo_path, &branch, force).await
}

#[tauri::command]
pub async fn get_pull_requests_by_attempt(
    app_state: State<'_, AppState>,
    task_attempt_id: String,
) -> Result<Vec<MergeRequestInfo>, String> {
    app_state.merge_request_service
        .get_merge_requests_by_attempt(&task_attempt_id)
        .await
        .map(|mrs| mrs.into_iter().map(|mr| {
            MergeRequestInfo {
                id: mr.mr_id,
                iid: mr.mr_iid,
                number: mr.mr_number,
                title: mr.title,
                description: mr.description,
                state: MergeRequestState::from_str(&mr.state).unwrap_or(MergeRequestState::Opened),
                source_branch: mr.source_branch,
                target_branch: mr.target_branch,
                web_url: mr.web_url,
                merge_status: mr.merge_status.and_then(|s| s.parse().ok()),
                has_conflicts: mr.has_conflicts,
                pipeline_status: mr.pipeline_status.and_then(|s| s.parse().ok()),
                created_at: mr.created_at.to_rfc3339(),
                updated_at: mr.updated_at.to_rfc3339(),
            }
        }).collect())
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_pull_requests_by_task(
    app_state: State<'_, AppState>,
    task_id: String,
) -> Result<Vec<MergeRequestInfo>, String> {
    app_state.merge_request_service
        .get_merge_requests_by_task(&task_id)
        .await
        .map(|mrs| mrs.into_iter().map(|mr| {
            MergeRequestInfo {
                id: mr.mr_id,
                iid: mr.mr_iid,
                number: mr.mr_number,
                title: mr.title,
                description: mr.description,
                state: MergeRequestState::from_str(&mr.state).unwrap_or(MergeRequestState::Opened),
                source_branch: mr.source_branch,
                target_branch: mr.target_branch,
                web_url: mr.web_url,
                merge_status: mr.merge_status.and_then(|s| s.parse().ok()),
                has_conflicts: mr.has_conflicts,
                pipeline_status: mr.pipeline_status.and_then(|s| s.parse().ok()),
                created_at: mr.created_at.to_rfc3339(),
                updated_at: mr.updated_at.to_rfc3339(),
            }
        }).collect())
        .map_err(|e| e.to_string())
}

use serde::{Serialize, Deserialize};
use serde_json::json;

#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceCodeResponse {
    device_code: String,
    user_code: String,
    verification_uri: String,
    expires_in: i32,
    interval: i32,
}


#[tauri::command]
pub async fn github_start_device_flow() -> Result<DeviceCodeResponse, String> {
    let client_id = "Ov23limL5nB8uf0tDrQX"; // Your GitHub OAuth App Client ID - Note: First character is letter O, not zero
    
    log::info!("Starting GitHub device flow with client_id: {}", client_id);
    
    let client = reqwest::Client::new();
    
    // Build the request
    let url = "https://github.com/login/device/code";
    log::info!("Sending POST request to: {}", url);
    
    // Build form body WITHOUT client_secret - Device Flow doesn't need it
    // Add 'read:org' scope to request organization access
    let body = format!("client_id={}&scope=repo%20user%20read:org%20write:org", client_id);
    log::info!("Request body: {}", body);
    
    let response = client
        .post(url)
        .header("Accept", "application/json")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .header("User-Agent", "pivo-app")
        .body(body)
        .send()
        .await
        .map_err(|e| format!("Failed to start device flow: {}", e))?;
    
    log::info!("Response status: {}", response.status());
    
    if !response.status().is_success() {
        let status = response.status();
        let error_text = response.text().await.unwrap_or_default();
        
        if status == 404 {
            return Err(format!(
                "GitHub Device Flow API not found (404). Please ensure:\n\
                1. Device Flow is enabled in your GitHub OAuth App settings\n\
                2. Go to GitHub Settings -> Developer settings -> OAuth Apps\n\
                3. Edit your app and enable 'Device Flow'\n\
                Error details: {}", 
                error_text
            ));
        }
        
        return Err(format!("GitHub API error: {} - {}", status, error_text));
    }
    
    let device_code_response = response
        .json::<DeviceCodeResponse>()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;
    
    Ok(device_code_response)
}

#[tauri::command]
pub async fn github_poll_device_auth(
    config_state: State<'_, Arc<Mutex<ConfigService>>>,
    device_code: String,
) -> Result<serde_json::Value, String> {
    let client_id = "Ov23limL5nB8uf0tDrQX"; // Your GitHub OAuth App Client ID - Note: First character is letter O, not zero
    
    log::debug!("Polling device auth for device_code: {}", device_code);
    
    let client = reqwest::Client::new();
    let response = client
        .post("https://github.com/login/oauth/access_token")
        .header("Accept", "application/json")
        .form(&[
            ("client_id", client_id),
            ("device_code", &device_code),
            ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
        ])
        .send()
        .await
        .map_err(|e| format!("Failed to poll auth: {}", e))?;
    
    let status = response.status();
    log::debug!("Poll response status: {}", status);
    
    let json_response = response
        .json::<serde_json::Value>()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;
    
    log::debug!("Poll response: {:?}", json_response);
    
    // Check if we got an access token
    if let Some(access_token) = json_response.get("access_token").and_then(|v| v.as_str()) {
        log::info!("Got access token, length: {}", access_token.len());
        
        // Also get the token type if available
        let token_type = json_response.get("token_type").and_then(|v| v.as_str()).unwrap_or("bearer");
        log::info!("Token type: {}", token_type);
        
        // Get scope if available
        if let Some(scope) = json_response.get("scope").and_then(|v| v.as_str()) {
            log::info!("Token scope: {}", scope);
        }
        
        // Save the access token to config
        let mut config_service = config_state.lock().await;
        let mut github_config = config_service.get_github_config()
            .cloned()
            .unwrap_or_default();
        github_config.access_token = Some(access_token.to_string());
        
        // Fetch user info to get the username
        let github_service = GitHubService::new(github_config.clone());
        match github_service.verify_token().await {
            Ok(user_info) => {
                if let Some(login) = user_info.get("login").and_then(|v| v.as_str()) {
                    github_config.username = Some(login.to_string());
                    log::info!("GitHub user authenticated: {}", login);
                }
            },
            Err(e) => {
                log::error!("Failed to fetch GitHub user info: {}", e);
            }
        }
        
        config_service.update_github_config(github_config).await
            .map_err(|e| format!("Failed to save GitHub config: {}", e))?;
        
        Ok(json!({ "status": "success" }))
    } else if let Some(error) = json_response.get("error").and_then(|v| v.as_str()) {
        log::debug!("Poll error: {}", error);
        if error == "authorization_pending" {
            Ok(json!({ "status": "pending" }))
        } else if error == "slow_down" {
            // GitHub is asking us to slow down
            Ok(json!({ "status": "pending", "slow_down": true }))
        } else {
            Ok(json!({ 
                "status": "error", 
                "error": error,
                "error_description": json_response.get("error_description").and_then(|v| v.as_str()).unwrap_or("")
            }))
        }
    } else {
        log::error!("Unexpected response format: {:?}", json_response);
        Err("Unexpected response format".to_string())
    }
}
```

## /src-tauri/src/commands/gitlab.rs

```rs path="/src-tauri/src/commands/gitlab.rs" 
use crate::models::{GitLabConfig, MergeRequestInfo, GitRemoteInfo, CreateMergeRequestData};
use crate::services::{ConfigService, GitLabService, GitPlatformService};
use crate::AppState;
use std::sync::Arc;
use tauri::State;
use tokio::sync::Mutex;
use chrono::Utc;

#[tauri::command]
pub async fn get_gitlab_config(
    state: State<'_, Arc<Mutex<ConfigService>>>,
) -> Result<Option<GitLabConfig>, String> {
    let config_service = state.lock().await;
    Ok(config_service.get_gitlab_config().cloned())
}

#[tauri::command]
pub async fn update_gitlab_config(
    state: State<'_, Arc<Mutex<ConfigService>>>,
    config: GitLabConfig,
) -> Result<(), String> {
    let mut config_service = state.lock().await;
    config_service.update_gitlab_config(config).await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn create_gitlab_mr(
    config_state: State<'_, Arc<Mutex<ConfigService>>>,
    app_state: State<'_, AppState>,
    task_attempt_id: String,
    remote_url: String,
    title: String,
    description: String,
    source_branch: String,
    target_branch: String,
) -> Result<MergeRequestInfo, String> {
    let config_service = config_state.lock().await;
    let gitlab_config = config_service.get_gitlab_config()
        .ok_or("GitLab not configured")?
        .clone();
    
    drop(config_service); // Release lock
    
    let remote_info = GitRemoteInfo::from_remote_url(&remote_url)
        .ok_or("Invalid remote URL")?;
    
    let gitlab_service = GitLabService::new(gitlab_config);
    let mr_info = gitlab_service.create_merge_request(
        &remote_info,
        &title,
        &description,
        &source_branch,
        &target_branch,
    ).await?;
    
    // Store MR in database
    let mr_data = CreateMergeRequestData {
        task_attempt_id,
        provider: "gitlab".to_string(),
        mr_id: mr_info.id,
        mr_iid: mr_info.iid,
        mr_number: mr_info.number,
        title: mr_info.title.clone(),
        description: mr_info.description.clone(),
        state: format!("{:?}", mr_info.state).to_lowercase(),
        source_branch: mr_info.source_branch.clone(),
        target_branch: mr_info.target_branch.clone(),
        web_url: mr_info.web_url.clone(),
        merge_status: mr_info.merge_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        has_conflicts: mr_info.has_conflicts,
        pipeline_status: mr_info.pipeline_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        pipeline_url: None, // TODO: Get from API if available
        created_at: Utc::now(),
        updated_at: Utc::now(),
        merged_at: None,
    };
    
    app_state.merge_request_service.create_merge_request(mr_data)
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(mr_info)
}

#[tauri::command]
pub async fn get_gitlab_mr_status(
    config_state: State<'_, Arc<Mutex<ConfigService>>>,
    app_state: State<'_, AppState>,
    task_attempt_id: String,
    remote_url: String,
    mr_number: i64,
) -> Result<MergeRequestInfo, String> {
    let config_service = config_state.lock().await;
    let gitlab_config = config_service.get_gitlab_config()
        .ok_or("GitLab not configured")?
        .clone();
    
    drop(config_service); // Release lock
    
    let remote_info = GitRemoteInfo::from_remote_url(&remote_url)
        .ok_or("Invalid remote URL")?;
    
    let gitlab_service = GitLabService::new(gitlab_config);
    let mr_info = gitlab_service.update_merge_request_status(&remote_info, mr_number).await?;
    
    // Sync MR to database
    let mr_data = CreateMergeRequestData {
        task_attempt_id,
        provider: "gitlab".to_string(),
        mr_id: mr_info.id,
        mr_iid: mr_info.iid,
        mr_number: mr_info.number,
        title: mr_info.title.clone(),
        description: mr_info.description.clone(),
        state: format!("{:?}", mr_info.state).to_lowercase(),
        source_branch: mr_info.source_branch.clone(),
        target_branch: mr_info.target_branch.clone(),
        web_url: mr_info.web_url.clone(),
        merge_status: mr_info.merge_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        has_conflicts: mr_info.has_conflicts,
        pipeline_status: mr_info.pipeline_status.as_ref().map(|s| format!("{:?}", s).to_lowercase()),
        pipeline_url: None, // TODO: Get from API if available
        created_at: Utc::now(),
        updated_at: Utc::now(),
        merged_at: None,
    };
    
    app_state.merge_request_service.sync_merge_request_from_api("gitlab", mr_info.id, mr_data)
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(mr_info)
}

#[tauri::command]
pub async fn push_to_gitlab(
    config_state: State<'_, Arc<Mutex<ConfigService>>>,
    repo_path: String,
    branch: String,
    force: bool,
) -> Result<(), String> {
    let config_service = config_state.lock().await;
    let gitlab_config = config_service.get_gitlab_config()
        .ok_or("GitLab not configured")?
        .clone();
    
    drop(config_service); // Release lock
    
    let gitlab_service = GitLabService::new(gitlab_config);
    gitlab_service.push_branch(&repo_path, &branch, force).await
}

#[tauri::command]
pub async fn detect_git_provider(remote_url: String) -> Result<String, String> {
    let remote_info = GitRemoteInfo::from_remote_url(&remote_url)
        .ok_or("Invalid remote URL")?;
    
    Ok(remote_info.provider.display_name().to_string())
}

#[tauri::command]
pub async fn get_merge_requests_by_attempt(
    app_state: State<'_, AppState>,
    task_attempt_id: String,
) -> Result<Vec<crate::models::MergeRequest>, String> {
    app_state.merge_request_service
        .get_merge_requests_by_attempt(&task_attempt_id)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_merge_requests_by_task(
    app_state: State<'_, AppState>,
    task_id: String,
) -> Result<Vec<crate::models::MergeRequest>, String> {
    app_state.merge_request_service
        .get_merge_requests_by_task(&task_id)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_active_merge_requests(
    app_state: State<'_, AppState>,
    provider: Option<String>,
) -> Result<Vec<crate::models::MergeRequest>, String> {
    app_state.merge_request_service
        .get_active_merge_requests(provider.as_deref())
        .await
        .map_err(|e| e.to_string())
}
```

## /src-tauri/src/commands/logging.rs

```rs path="/src-tauri/src/commands/logging.rs" 
use crate::logging::get_log_file_path;
use std::fs;

#[tauri::command]
pub async fn get_log_content(
    lines: Option<usize>,
) -> Result<String, String> {
    let log_path = get_log_file_path();
    
    if !log_path.exists() {
        return Ok("No log file found".to_string());
    }
    
    let content = fs::read_to_string(&log_path)
        .map_err(|e| format!("Failed to read log file: {}", e))?;
    
    // If lines is specified, return only the last N lines
    if let Some(n) = lines {
        let lines: Vec<&str> = content.lines().collect();
        let start = lines.len().saturating_sub(n);
        Ok(lines[start..].join("\n"))
    } else {
        Ok(content)
    }
}

#[tauri::command]
pub async fn get_log_path() -> Result<String, String> {
    Ok(get_log_file_path().to_string_lossy().to_string())
}

#[tauri::command]
pub async fn open_log_file(_app_handle: tauri::AppHandle) -> Result<(), String> {
    let log_path = get_log_file_path();
    
    // Open the log file in the default text editor
    #[cfg(target_os = "macos")]
    {
        std::process::Command::new("open")
            .arg("-t") // Open with default text editor
            .arg(&log_path)
            .spawn()
            .map_err(|e| format!("Failed to open log file: {}", e))?;
    }
    
    #[cfg(target_os = "windows")]
    {
        std::process::Command::new("notepad")
            .arg(&log_path)
            .spawn()
            .map_err(|e| format!("Failed to open log file: {}", e))?;
    }
    
    #[cfg(target_os = "linux")]
    {
        std::process::Command::new("xdg-open")
            .arg(&log_path)
            .spawn()
            .map_err(|e| format!("Failed to open log file: {}", e))?;
    }
    
    Ok(())
}

#[tauri::command]
pub async fn clear_logs() -> Result<(), String> {
    let log_path = get_log_file_path();
    
    if log_path.exists() {
        fs::write(&log_path, "")
            .map_err(|e| format!("Failed to clear log file: {}", e))?;
    }
    
    log::info!("Logs cleared");
    Ok(())
}
```

## /src-tauri/src/commands/mcp.rs

```rs path="/src-tauri/src/commands/mcp.rs" 
use crate::services::mcp_server::{
    McpServer, McpServerManager, McpCapabilities, McpServerStatus
};
use std::sync::Arc;
use tauri::State;
use serde_json::Value;

pub struct McpState {
    pub manager: Arc<McpServerManager>,
}

#[tauri::command]
pub async fn register_mcp_server(
    state: State<'_, McpState>,
    name: String,
    command: String,
    args: Vec<String>,
    env: std::collections::HashMap<String, String>,
) -> Result<String, String> {
    let server = McpServer {
        id: uuid::Uuid::new_v4().to_string(),
        name,
        command,
        args,
        env,
        capabilities: McpCapabilities {
            tools: true,
            resources: true,
            prompts: true,
        },
        status: McpServerStatus::Stopped,
    };

    state.manager.register_server(server)
}

#[tauri::command]
pub async fn start_mcp_server(
    state: State<'_, McpState>,
    server_id: String,
) -> Result<(), String> {
    state.manager.start_server(&server_id)
}

#[tauri::command]
pub async fn stop_mcp_server(
    state: State<'_, McpState>,
    server_id: String,
) -> Result<(), String> {
    state.manager.stop_server(&server_id)
}

#[tauri::command]
pub async fn list_mcp_servers(
    state: State<'_, McpState>,
) -> Result<Vec<McpServer>, String> {
    Ok(state.manager.list_servers())
}

#[tauri::command]
pub async fn get_mcp_server(
    state: State<'_, McpState>,
    server_id: String,
) -> Result<Option<McpServer>, String> {
    Ok(state.manager.get_server(&server_id))
}

#[tauri::command]
pub async fn send_mcp_request(
    state: State<'_, McpState>,
    server_id: String,
    method: String,
    params: Option<Value>,
) -> Result<String, String> {
    state.manager.send_request(&server_id, &method, params)
}

#[tauri::command]
pub async fn list_mcp_tools(
    state: State<'_, McpState>,
    server_id: String,
) -> Result<String, String> {
    state.manager.send_request(&server_id, "tools/list", None)
}


#[tauri::command]
pub async fn list_mcp_resources(
    state: State<'_, McpState>,
    server_id: String,
) -> Result<String, String> {
    state.manager.send_request(&server_id, "resources/list", None)
}

#[tauri::command]
pub async fn read_mcp_resource(
    state: State<'_, McpState>,
    server_id: String,
    uri: String,
) -> Result<String, String> {
    state.manager.send_request(
        &server_id,
        "resources/read",
        Some(serde_json::json!({ "uri": uri })),
    )
}

#[tauri::command]
pub async fn list_mcp_prompts(
    state: State<'_, McpState>,
    server_id: String,
) -> Result<String, String> {
    state.manager.send_request(&server_id, "prompts/list", None)
}

#[tauri::command]
pub async fn get_mcp_prompt(
    state: State<'_, McpState>,
    server_id: String,
    name: String,
    arguments: Value,
) -> Result<String, String> {
    state.manager.send_request(
        &server_id,
        "prompts/get",
        Some(serde_json::json!({
            "name": name,
            "arguments": arguments,
        })),
    )
}
```

## /src-tauri/src/commands/mod.rs

```rs path="/src-tauri/src/commands/mod.rs" 
pub mod tasks;
pub mod task_attempts;
pub mod task_commands;
pub mod projects;
pub mod process;
pub mod git;
pub mod mcp;
pub mod cli;
pub mod git_info;
pub mod logging;
pub mod window;
pub mod gitlab;
pub mod github;
pub mod system;
pub mod filesystem;
pub mod command;
pub mod dev_server;
```

## /src-tauri/src/commands/process.rs

```rs path="/src-tauri/src/commands/process.rs" 
use crate::models::ExecutionProcess;
use crate::AppState;
use tauri::State;
use uuid::Uuid;

#[tauri::command]
pub async fn get_process(
    state: State<'_, AppState>,
    id: String,
) -> Result<Option<ExecutionProcess>, String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .process_service
        .get_process(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn list_processes_for_attempt(
    state: State<'_, AppState>,
    task_attempt_id: String,
) -> Result<Vec<ExecutionProcess>, String> {
    let uuid = Uuid::parse_str(&task_attempt_id).map_err(|e| e.to_string())?;
    state
        .process_service
        .list_processes_for_attempt(uuid)
        .await
        .map_err(|e| e.to_string())
}
```

## /src-tauri/src/commands/projects.rs

```rs path="/src-tauri/src/commands/projects.rs" 
use crate::models::{CreateProjectRequest, Project, UpdateProjectRequest};
use crate::AppState;
use crate::utils::command::execute_git;
use tauri::State;
use uuid::Uuid;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::fs;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectInfo {
    pub path: String,
    pub name: String,
    pub description: Option<String>,
    pub git_repo: Option<String>,
    pub main_branch: Option<String>,
    pub setup_script: Option<String>,
    pub dev_script: Option<String>,
    pub has_git: bool,
    pub has_package_json: bool,
}

#[tauri::command]
pub async fn create_project(
    state: State<'_, AppState>,
    request: CreateProjectRequest,
) -> Result<Project, String> {
    state
        .project_service
        .create_project(request)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_project(
    state: State<'_, AppState>,
    id: String,
) -> Result<Option<Project>, String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .project_service
        .get_project(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn list_projects(
    state: State<'_, AppState>,
) -> Result<Vec<Project>, String> {
    state
        .project_service
        .list_projects()
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn update_project(
    state: State<'_, AppState>,
    id: String,
    request: UpdateProjectRequest,
) -> Result<Project, String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .project_service
        .update_project(uuid, request)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn delete_project(
    state: State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .project_service
        .delete_project(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn refresh_all_git_providers(
    state: State<'_, AppState>,
) -> Result<Vec<Project>, String> {
    // Get all projects
    let projects = state
        .project_service
        .list_projects()
        .await
        .map_err(|e| e.to_string())?;
    
    let mut updated_projects = Vec::new();
    
    // Update each project that has a git_repo but no git_provider
    for project in projects {
        if project.git_repo.is_some() && project.git_provider.is_none() {
            // Create an update request with just the git_repo to trigger provider detection
            let update_req = UpdateProjectRequest {
                name: None,
                description: None,
                path: None,
                git_repo: project.git_repo.clone(),
                main_branch: None,
                setup_script: None,
                dev_script: None,
            };
            
            match state
                .project_service
                .update_project(Uuid::parse_str(&project.id).unwrap(), update_req)
                .await
            {
                Ok(updated_project) => {
                    updated_projects.push(updated_project);
                }
                Err(e) => {
                    log::error!("Failed to update project {}: {}", project.id, e);
                }
            }
        }
    }
    
    Ok(updated_projects)
}

#[tauri::command]
pub async fn update_project_last_opened(
    state: State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .project_service
        .update_last_opened(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_recent_projects(
    state: State<'_, AppState>,
    limit: i32,
) -> Result<Vec<Project>, String> {
    state
        .project_service
        .get_recent_projects(limit)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn select_project_directory(app_handle: tauri::AppHandle) -> Result<Option<String>, String> {
    use tauri_plugin_dialog::DialogExt;
    use tokio::sync::oneshot;
    
    let (tx, rx) = oneshot::channel();
    
    app_handle
        .dialog()
        .file()
        .set_title("Select Project Directory")
        .pick_folder(move |folder_path| {
            let _ = tx.send(folder_path.map(|path| path.to_string()));
        });
    
    match rx.await {
        Ok(result) => Ok(result),
        Err(_) => Ok(None),
    }
}

#[tauri::command]
pub async fn read_project_info(path: String) -> Result<ProjectInfo, String> {
    let project_path = PathBuf::from(&path);
    
    if !project_path.exists() || !project_path.is_dir() {
        return Err("Invalid directory path".to_string());
    }
    
    // Extract project name from directory name
    let name = project_path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("Untitled Project")
        .to_string();
    
    // Check for git
    let git_path = project_path.join(".git");
    let has_git = git_path.exists() && git_path.is_dir();
    
    // Validate git repository
    if !has_git {
        return Err("Selected directory is not a valid Git repository. Please select a directory with an initialized Git repository.".to_string());
    }
    
    // Get git remote URL if available
    let mut git_repo = None;
    let mut main_branch = None;
    if has_git {
        log::info!("Checking git remotes for path: {}", project_path.display());
        
        // Get current branch
        if let Ok(output) = execute_git(&["symbolic-ref", "--short", "HEAD"], &project_path)
        {
            if output.status.success() {
                let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
                if !branch.is_empty() {
                    main_branch = Some(branch);
                    log::info!("Found current branch: {:?}", main_branch);
                }
            }
        }
        
        // If we couldn't get the current branch, try to get the default branch from remote
        if main_branch.is_none() {
            if let Ok(output) = execute_git(&["symbolic-ref", "refs/remotes/origin/HEAD"], &project_path)
            {
                if output.status.success() {
                    let remote_head = String::from_utf8_lossy(&output.stdout).trim().to_string();
                    // Extract branch name from refs/remotes/origin/main
                    if let Some(branch) = remote_head.split('/').last() {
                        main_branch = Some(branch.to_string());
                        log::info!("Found default branch from remote: {:?}", main_branch);
                    }
                }
            }
        }
        
        // First try to get origin remote
        if let Ok(output) = execute_git(&["remote", "get-url", "origin"], &project_path)
        {
            if output.status.success() {
                let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
                log::info!("Found origin remote URL: {}", url);
                if !url.is_empty() {
                    git_repo = Some(url);
                }
            } else {
                let error = String::from_utf8_lossy(&output.stderr);
                log::warn!("Failed to get origin remote: {}", error);
            }
        } else {
            log::error!("Failed to execute git remote get-url origin command");
        }
        
        // If origin doesn't exist, try to get the first available remote
        if git_repo.is_none() {
            log::info!("Origin not found, checking for other remotes");
            if let Ok(output) = execute_git(&["remote"], &project_path)
            {
                if output.status.success() {
                    let remotes = String::from_utf8_lossy(&output.stdout);
                    log::info!("Available remotes: {}", remotes.trim());
                    if let Some(first_remote) = remotes.lines().next() {
                        if !first_remote.is_empty() {
                            log::info!("Trying to get URL for remote: {}", first_remote);
                            // Get URL for the first remote
                            if let Ok(url_output) = execute_git(&["remote", "get-url", first_remote], &project_path)
                            {
                                if url_output.status.success() {
                                    let url = String::from_utf8_lossy(&url_output.stdout).trim().to_string();
                                    log::info!("Found remote URL: {}", url);
                                    if !url.is_empty() {
                                        git_repo = Some(url);
                                    }
                                } else {
                                    let error = String::from_utf8_lossy(&url_output.stderr);
                                    log::warn!("Failed to get URL for remote {}: {}", first_remote, error);
                                }
                            }
                        }
                    } else {
                        log::info!("No remotes found");
                    }
                } else {
                    let error = String::from_utf8_lossy(&output.stderr);
                    log::warn!("Failed to list remotes: {}", error);
                }
            } else {
                log::error!("Failed to execute git remote command");
            }
        }
    }
    
    // Check for package.json
    let package_json_path = project_path.join("package.json");
    let has_package_json = package_json_path.exists();
    
    let mut description = None;
    let mut setup_script = None;
    let mut dev_script = None;
    
    // Read package.json if it exists
    if has_package_json {
        if let Ok(content) = fs::read_to_string(&package_json_path) {
            if let Ok(package_json) = serde_json::from_str::<serde_json::Value>(&content) {
                // Get description
                if let Some(desc) = package_json.get("description").and_then(|d| d.as_str()) {
                    description = Some(desc.to_string());
                }
                
                // Get scripts
                if let Some(scripts) = package_json.get("scripts").and_then(|s| s.as_object()) {
                    // Look for install/setup scripts
                    if scripts.contains_key("install") {
                        setup_script = Some("npm install".to_string());
                    } else if scripts.contains_key("setup") {
                        setup_script = Some("npm run setup".to_string());
                    } else {
                        setup_script = Some("npm install".to_string());
                    }
                    
                    // Look for dev scripts
                    if scripts.contains_key("dev") {
                        dev_script = Some("npm run dev".to_string());
                    } else if scripts.contains_key("start") {
                        dev_script = Some("npm start".to_string());
                    } else if scripts.contains_key("serve") {
                        dev_script = Some("npm run serve".to_string());
                    }
                }
            }
        }
    }
    
    // Check for other common project files
    let composer_json = project_path.join("composer.json").exists();
    let cargo_toml = project_path.join("Cargo.toml").exists();
    let pom_xml = project_path.join("pom.xml").exists();
    let build_gradle = project_path.join("build.gradle").exists();
    let requirements_txt = project_path.join("requirements.txt").exists();
    let pipfile = project_path.join("Pipfile").exists();
    let gemfile = project_path.join("Gemfile").exists();
    let go_mod = project_path.join("go.mod").exists();
    
    // Set default scripts based on project type
    if setup_script.is_none() {
        if composer_json {
            setup_script = Some("composer install".to_string());
        } else if cargo_toml {
            setup_script = Some("cargo build".to_string());
        } else if pom_xml {
            setup_script = Some("mvn install".to_string());
        } else if build_gradle {
            setup_script = Some("gradle build".to_string());
        } else if requirements_txt {
            setup_script = Some("pip install -r requirements.txt".to_string());
        } else if pipfile {
            setup_script = Some("pipenv install".to_string());
        } else if gemfile {
            setup_script = Some("bundle install".to_string());
        } else if go_mod {
            setup_script = Some("go mod download".to_string());
        }
    }
    
    if dev_script.is_none() {
        if cargo_toml {
            dev_script = Some("cargo run".to_string());
        } else if pom_xml {
            dev_script = Some("mvn spring-boot:run".to_string());
        } else if build_gradle {
            dev_script = Some("gradle bootRun".to_string());
        } else if requirements_txt || pipfile {
            dev_script = Some("python main.py".to_string());
        } else if gemfile {
            dev_script = Some("bundle exec ruby main.rb".to_string());
        } else if go_mod {
            dev_script = Some("go run .".to_string());
        }
    }
    
    log::info!("Returning ProjectInfo: name={}, has_git={}, git_repo={:?}", name, has_git, git_repo);
    
    Ok(ProjectInfo {
        path,
        name,
        description,
        git_repo,
        main_branch,
        setup_script,
        dev_script,
        has_git,
        has_package_json,
    })
}
```

## /src-tauri/src/commands/system.rs

```rs path="/src-tauri/src/commands/system.rs" 
use std::process::Command;
use crate::utils::command::execute_command;
use std::path::Path;

#[tauri::command]
pub async fn open_in_terminal(path: String) -> Result<(), String> {
    #[cfg(target_os = "macos")]
    {
        // Try to open with iTerm2 first (if available), then fall back to Terminal.app
        // This respects the user's preference if they have iTerm2 installed
        let iterm_result = Command::new("open")
            .args(&["-a", "iTerm", &path])
            .spawn();
        
        match iterm_result {
            Ok(_) => return Ok(()),
            Err(_) => {
                // iTerm not found, use Terminal.app
                Command::new("open")
                    .args(&["-a", "Terminal", &path])
                    .spawn()
                    .map_err(|e| format!("Failed to open terminal: {}", e))?;
            }
        }
    }
    
    #[cfg(target_os = "windows")]
    {
        // Try Windows Terminal first
        if let Ok(_) = Command::new("wt")
            .args(&["-d", &path])
            .spawn()
        {
            return Ok(());
        }
        
        // Fall back to cmd
        Command::new("cmd")
            .args(&["/c", "start", "cmd", "/k", "cd", "/d", &path])
            .spawn()
            .map_err(|e| format!("Failed to open terminal: {}", e))?;
    }
    
    #[cfg(target_os = "linux")]
    {
        // Use x-terminal-emulator which is the Debian alternatives system
        // Most distros have this symlink pointing to the default terminal
        if let Ok(_) = Command::new("x-terminal-emulator")
            .current_dir(&path)
            .spawn()
        {
            return Ok(());
        }
        
        // Try gnome-terminal as fallback
        Command::new("gnome-terminal")
            .arg("--working-directory")
            .arg(&path)
            .spawn()
            .map_err(|e| format!("Failed to open terminal: {}", e))?;
    }
    
    Ok(())
}

#[tauri::command]
pub async fn show_in_file_manager(file_path: String) -> Result<(), String> {
    let path = Path::new(&file_path);
    
    // Validate that the path exists
    if !path.exists() {
        return Err(format!("File or directory does not exist: {}", file_path));
    }
    
    // Ensure the path is absolute and normalized
    let absolute_path = match path.canonicalize() {
        Ok(p) => p,
        Err(e) => return Err(format!("Failed to resolve path: {}", e)),
    };
    
    let file_path_str = absolute_path.to_str()
        .ok_or_else(|| "Invalid UTF-8 in path".to_string())?;
    
    #[cfg(target_os = "macos")]
    {
        // macOS: Use 'open' command with -R flag to reveal in Finder
        match execute_command("open", &["-R", file_path_str], None) {
            Ok(_) => Ok(()),
            Err(e) => Err(format!("Failed to show in Finder: {}", e)),
        }
    }
    
    #[cfg(target_os = "windows")]
    {
        // Windows: Use explorer with /select flag
        match execute_command("explorer", &["/select,", file_path_str], None) {
            Ok(_) => Ok(()),
            Err(e) => Err(format!("Failed to show in Explorer: {}", e)),
        }
    }
    
    #[cfg(target_os = "linux")]
    {
        // Linux: Try common file managers
        // Try nautilus first (GNOME)
        if let Ok(_) = execute_command("nautilus", &["--select", file_path_str], None) {
            return Ok(());
        }
        
        // Try dolphin (KDE)
        if let Ok(_) = execute_command("dolphin", &["--select", file_path_str], None) {
            return Ok(());
        }
        
        // Try thunar (XFCE)
        if let Ok(_) = execute_command("thunar", &[file_path_str], None) {
            return Ok(());
        }
        
        // Fallback to xdg-open on parent directory
        if let Some(parent) = path.parent() {
            match execute_command("xdg-open", &[parent.to_str().unwrap_or(".")], None) {
                Ok(_) => Ok(()),
                Err(e) => Err(format!("Failed to open file manager: {}", e)),
            }
        } else {
            Err("Failed to open file manager".to_string())
        }
    }
}
```

## /src-tauri/src/commands/task_attempts.rs

```rs path="/src-tauri/src/commands/task_attempts.rs" 
use crate::models::TaskAttempt;
use crate::AppState;
use tauri::State;
use uuid::Uuid;

#[tauri::command]
pub async fn get_task_attempt(
    state: State<'_, AppState>,
    id: String,
) -> Result<Option<TaskAttempt>, String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .task_service
        .get_task_attempt(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn list_task_attempts(
    state: State<'_, AppState>,
    task_id: String,
) -> Result<Vec<TaskAttempt>, String> {
    let uuid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
    state
        .task_service
        .list_task_attempts(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn update_attempt_claude_session(
    state: State<'_, AppState>,
    attempt_id: String,
    claude_session_id: String,
) -> Result<(), String> {
    let uuid = Uuid::parse_str(&attempt_id).map_err(|e| e.to_string())?;
    state
        .task_service
        .update_attempt_claude_session(uuid, claude_session_id)
        .await
        .map_err(|e| e.to_string())
}
```

## /src-tauri/src/commands/task_commands.rs

```rs path="/src-tauri/src/commands/task_commands.rs" 
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, State, Emitter};
use uuid::Uuid;

use crate::{
    commands::cli::CliState,
    AppState,
    models::TaskStatus,
};

// Simplified command system based on RFC
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TaskCommand {
    /// Send message (requires existing Attempt)
    #[serde(rename = "SEND_MESSAGE")]
    SendMessage { 
        #[serde(rename = "taskId")]
        task_id: String, 
        message: String,
        images: Option<Vec<String>>,
    },
    
    /// Stop current execution
    #[serde(rename = "STOP_EXECUTION")]
    StopExecution { 
        #[serde(rename = "taskId")]
        task_id: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationState {
    messages: Vec<ServiceConversationMessage>,
    #[serde(rename = "isExecuting")]
    is_executing: bool,
    #[serde(rename = "currentAttemptId")]
    current_attempt_id: Option<String>,
    #[serde(rename = "canSendMessage")]
    can_send_message: bool,
    #[serde(rename = "currentExecution")]
    current_execution: Option<crate::services::coding_agent_executor::types::CodingAgentExecution>,
    #[serde(rename = "worktreePath")]
    worktree_path: Option<String>,
}

// Use ConversationMessage from the service module
use crate::services::coding_agent_executor::types::ConversationMessage as ServiceConversationMessage;

/// Execute a task command
#[tauri::command]
pub async fn execute_task_command(
    app: AppHandle,
    state: State<'_, AppState>,
    cli_state: State<'_, CliState>,
    command: TaskCommand,
) -> Result<(), String> {
    log::info!("Executing task command: {:?}", command);
    
    match command {
        TaskCommand::SendMessage { task_id, message, images } => {
            handle_send_message(&app, &state, &cli_state, &task_id, message, images).await
        }
        TaskCommand::StopExecution { task_id } => {
            handle_stop_execution(&app, &state, &cli_state, &task_id).await
        }
    }
}

/// Get current conversation state
#[tauri::command]
pub async fn get_conversation_state(
    state: State<'_, AppState>,
    cli_state: State<'_, CliState>,
    task_id: String,
) -> Result<ConversationState, String> {
    let task_service = &state.task_service;
    let task_uuid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
    
    // Get task
    let _task = task_service.get_task(task_uuid)
        .await
        .map_err(|e| e.to_string())?
        .ok_or("Task not found")?;
    
    // Get attempts
    let attempts = task_service.list_task_attempts(task_uuid)
        .await
        .map_err(|e| e.to_string())?;
    
    // Get latest attempt
    let current_attempt = attempts.last();
    
    // Check if task is executing and get current execution
    let executions = cli_state.service.list_executions();
    log::debug!("Total executions found: {}", executions.len());
    for exec in &executions {
        log::debug!("Execution - ID: {}, Task ID: {}, Status: {:?}", exec.id, exec.task_id, exec.status);
    }
    
    let current_execution = executions.iter().find(|e| e.task_id == task_id).cloned();
    
    let is_executing = current_execution.as_ref().map(|e| 
        matches!(e.status, 
            crate::services::coding_agent_executor::types::CodingAgentExecutionStatus::Running | 
            crate::services::coding_agent_executor::types::CodingAgentExecutionStatus::Starting
        )
    ).unwrap_or(false);
    
    log::info!("get_conversation_state for task {}: is_executing = {}, has_attempt = {}", 
        task_id, is_executing, current_attempt.is_some());
    
    // Get messages from current attempt
    let messages = if let Some(attempt) = current_attempt {
        get_attempt_messages(&state, &attempt.id).await.unwrap_or_default()
    } else {
        vec![]
    };
    
    Ok(ConversationState {
        messages,
        is_executing,
        current_attempt_id: current_attempt.map(|a| a.id.clone()),
        can_send_message: !is_executing && current_attempt.is_some(),
        current_execution,
        worktree_path: current_attempt.map(|a| a.worktree_path.clone()),
    })
}

// Core logic based on RFC
async fn handle_send_message(
    app: &AppHandle,
    state: &State<'_, AppState>,
    cli_state: &State<'_, CliState>,
    task_id: &str,
    message: String,
    images: Option<Vec<String>>,
) -> Result<(), String> {
    let task_service = &state.task_service;
    let task_uuid = Uuid::parse_str(task_id).map_err(|e| e.to_string())?;
    
    // 1. Get the latest Attempt, error if none exists
    let attempts = task_service.list_task_attempts(task_uuid)
        .await
        .map_err(|e| e.to_string())?;
    
    let mut attempt = attempts.last()
        .ok_or("No attempt found for this task. Please create an attempt first.")?
        .clone();
    
    // 2. Determine agent type and update executor field if needed
    let agent_type = match attempt.executor.as_deref() {
        Some("claude") | Some("claude_code") | Some("ClaudeCode") => 
            crate::services::coding_agent_executor::CodingAgentType::ClaudeCode,
        Some("gemini") | Some("gemini_cli") | Some("GeminiCli") => 
            crate::services::coding_agent_executor::CodingAgentType::GeminiCli,
        _ => crate::services::coding_agent_executor::CodingAgentType::ClaudeCode, // Default to Claude
    };
    
    // Update executor field if not set or different
    let executor_str = match &agent_type {
        crate::services::coding_agent_executor::CodingAgentType::ClaudeCode => "claude_code",
        crate::services::coding_agent_executor::CodingAgentType::GeminiCli => "gemini_cli",
    };
    
    if attempt.executor.as_deref() != Some(executor_str) {
        log::info!("Updating attempt {} executor from {:?} to {}", attempt.id, attempt.executor, executor_str);
        let attempt_uuid = Uuid::parse_str(&attempt.id).map_err(|e| e.to_string())?;
        task_service.update_attempt_executor(attempt_uuid, executor_str.to_string())
            .await
            .map_err(|e| e.to_string())?;
        
        // Update local attempt object
        attempt.executor = Some(executor_str.to_string());
    }
    
    // 3. Get resume session ID if available (now that executor is set correctly)
    let resume_session_id = match executor_str {
        "claude_code" => {
            log::info!("Attempt {} Claude session ID: {:?}", attempt.id, attempt.claude_session_id);
            if attempt.claude_session_id.is_none() {
                log::info!("No Claude session ID found for attempt {}, this is expected for first execution", attempt.id);
            }
            attempt.claude_session_id.clone()
        },
        _ => None,
    };
    
    // 4. Check if there's already an active execution and stop it first
    let executions = cli_state.service.list_executions();
    if let Some(exec) = executions.iter().find(|e| 
        e.task_id == task_id && 
        matches!(e.status, 
            crate::services::coding_agent_executor::types::CodingAgentExecutionStatus::Running | 
            crate::services::coding_agent_executor::types::CodingAgentExecutionStatus::Starting
        )
    ) {
        log::info!("Stopping existing execution {} before starting new one", exec.id);
        cli_state.service.stop_execution(&exec.id).await?;
    }
    
    // 5. Get task and project info
    let task = task_service.get_task(task_uuid)
        .await
        .map_err(|e| e.to_string())?
        .ok_or("Task not found")?;
    
    let project_uuid = Uuid::parse_str(&task.project_id).map_err(|e| e.to_string())?;
    let project = state.project_service
        .get_project(project_uuid)
        .await
        .map_err(|e| e.to_string())?
        .ok_or("Project not found")?;
    
    // 6. Update task status to Working if not already
    if task.status != TaskStatus::Working {
        let updated_task = task_service.update_task_status(task_uuid, TaskStatus::Working)
            .await
            .map_err(|e| e.to_string())?;
        
        // Emit task:status-changed event
        let _ = app.emit("task:status-changed", &serde_json::json!({
            "taskId": task_id,
            "previousStatus": task.status,
            "newStatus": TaskStatus::Working,
            "task": updated_task,
        }));
    }
    
    // 7. Combine message with images if provided
    let prompt = if let Some(imgs) = &images {
        if !imgs.is_empty() {
            format!("{}\n\n[Images: {} attached]", message, imgs.len())
        } else {
            message
        }
    } else {
        message
    };
    
    // 8. Execute with resume session
    let execution = crate::commands::cli::execute_prompt(
        cli_state.clone(),
        prompt,
        task_id.to_string(),
        attempt.id.clone(),
        if attempt.worktree_path.is_empty() { project.path.clone() } else { attempt.worktree_path.clone() },
        agent_type,
        resume_session_id, // Use saved session ID
    ).await?;
    
    // 9. Emit execution:started event
    let _ = app.emit("execution:started", &serde_json::json!({
        "taskId": task_id,
        "attemptId": attempt.id,
        "executionId": execution.id,
    }));
    
    // 10. Don't emit state update immediately - let the frontend handle the state change
    // The execution:started event is enough to update the UI state
    
    Ok(())
}

async fn handle_stop_execution(
    app: &AppHandle,
    state: &State<'_, AppState>,
    cli_state: &State<'_, CliState>,
    task_id: &str,
) -> Result<(), String> {
    let task_service = &state.task_service;
    let task_uuid = Uuid::parse_str(task_id).map_err(|e| e.to_string())?;
    
    // Get current execution and attempt ID from the latest attempt
    let attempts = task_service.list_task_attempts(task_uuid)
        .await
        .map_err(|e| e.to_string())?;
    
    let attempt_id = attempts.last()
        .map(|a| a.id.clone())
        .unwrap_or_default();
    
    let executions = cli_state.service.list_executions();
    if let Some(execution) = executions.iter().find(|e| e.task_id == task_id) {
        let exec_id = execution.id.clone();
        
        cli_state.service.stop_execution(&exec_id).await?;
        
        // Emit execution:completed event
        let _ = app.emit("execution:completed", &serde_json::json!({
            "taskId": task_id,
            "attemptId": attempt_id,
            "executionId": exec_id,
            "status": "cancelled",
        }));
    }
    
    // Update task status back to Backlog when stopping
    let task = task_service.get_task(task_uuid)
        .await
        .map_err(|e| e.to_string())?
        .ok_or("Task not found")?;
    
    if task.status != TaskStatus::Backlog {
        let updated_task = task_service.update_task_status(task_uuid, TaskStatus::Backlog)
            .await
            .map_err(|e| e.to_string())?;
        
        // Emit task:status-changed event
        let _ = app.emit("task:status-changed", &serde_json::json!({
            "taskId": task_id,
            "previousStatus": task.status,
            "newStatus": TaskStatus::Backlog,
            "task": updated_task,
        }));
    }
    
    // Don't emit state update immediately - let the frontend handle the state change
    
    Ok(())
}

// Removed emit_state_update function - no longer needed
// State updates are now handled through granular events:
// - execution:started
// - execution:completed
// - message:added
// - task:status-changed

async fn get_attempt_messages(
    state: &State<'_, AppState>,
    attempt_id: &str,
) -> Result<Vec<ServiceConversationMessage>, String> {
    let attempt_uuid = Uuid::parse_str(attempt_id).map_err(|e| e.to_string())?;
    
    // Get messages from attempt conversation
    if let Ok(Some(conversation)) = state.task_service.get_attempt_conversation(attempt_uuid).await {
        let messages = conversation.messages.into_iter().map(|msg| {
            
            // Parse the new message format where content contains type, content, and metadata
            let (message_type, content, metadata) = if let Ok(json_content) = serde_json::from_str::<serde_json::Value>(&msg.content) {
                // New format: content is JSON with type, content, and metadata fields
                let msg_type = json_content.get("type")
                    .and_then(|v| v.as_str())
                    .unwrap_or(&msg.role)
                    .to_string();
                let content = json_content.get("content")
                    .and_then(|v| v.as_str())
                    .unwrap_or(&msg.content)
                    .to_string();
                let metadata = json_content.get("metadata")
                    .cloned();
                (msg_type, content, metadata)
            } else {
                // Old format: plain text content
                (msg.role.clone(), msg.content, None)
            };
            
            
            // Map role string to MessageRole enum
            let role = match msg.role.as_str() {
                "user" => crate::services::coding_agent_executor::types::MessageRole::User,
                "assistant" => crate::services::coding_agent_executor::types::MessageRole::Assistant,
                "system" => crate::services::coding_agent_executor::types::MessageRole::System,
                _ => crate::services::coding_agent_executor::types::MessageRole::Assistant,
            };
            
            ServiceConversationMessage::new(
                role,
                message_type,
                content,
                metadata,
            )
        }).collect();
        
        Ok(messages)
    } else {
        Ok(vec![])
    }
}
```

## /src-tauri/src/commands/tasks.rs

```rs path="/src-tauri/src/commands/tasks.rs" 
use crate::models::{CreateTaskRequest, Task, TaskStatus, UpdateTaskRequest};
use crate::AppState;
use tauri::{State, AppHandle, Emitter};
use uuid::Uuid;

#[tauri::command]
pub async fn create_task(
    state: State<'_, AppState>,
    request: CreateTaskRequest,
) -> Result<Task, String> {
    state
        .task_service
        .create_task(request)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_task(
    state: State<'_, AppState>,
    id: String,
) -> Result<Option<Task>, String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .task_service
        .get_task(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn list_tasks(
    state: State<'_, AppState>,
    project_id: String,
) -> Result<Vec<Task>, String> {
    let uuid = Uuid::parse_str(&project_id).map_err(|e| e.to_string())?;
    state
        .task_service
        .list_tasks(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn update_task(
    state: State<'_, AppState>,
    id: String,
    request: UpdateTaskRequest,
) -> Result<Task, String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .task_service
        .update_task(uuid, request)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn delete_task(
    state: State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    state
        .task_service
        .delete_task(uuid)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn update_task_status(
    state: State<'_, AppState>,
    app_handle: AppHandle,
    id: String,
    status: TaskStatus,
) -> Result<Task, String> {
    let uuid = Uuid::parse_str(&id).map_err(|e| e.to_string())?;
    
    // Get previous status before update
    let previous_task = state
        .task_service
        .get_task(uuid)
        .await
        .map_err(|e| e.to_string())?
        .ok_or("Task not found")?;
    let previous_status = previous_task.status.clone();
    
    let task = state
        .task_service
        .update_task_status(uuid, status.clone())
        .await
        .map_err(|e| e.to_string())?;
    
    // Emit task status update event with new format
    let _ = app_handle.emit("task:status-changed", &serde_json::json!({
        "taskId": id,
        "previousStatus": previous_status,
        "newStatus": status,
        "task": &task
    }));
    
    Ok(task)
}

// Removed execute_task - functionality moved to SendMessage in task_commands
// Tasks must have an existing attempt before sending messages
```

## /src-tauri/src/commands/window.rs

```rs path="/src-tauri/src/commands/window.rs" 
use tauri::{Manager, State, WebviewUrl, WebviewWindowBuilder};
use crate::AppState;

#[tauri::command]
pub async fn show_log_viewer(app: tauri::AppHandle) -> Result<(), String> {
    // Check if log viewer window already exists
    if let Some(window) = app.get_webview_window("log-viewer") {
        // If it exists, focus it
        window.show().map_err(|e| format!("Failed to show window: {}", e))?;
        window.set_focus().map_err(|e| format!("Failed to focus window: {}", e))?;
    } else {
        // Create new log viewer window
        WebviewWindowBuilder::new(
            &app,
            "log-viewer",
            WebviewUrl::App("log-viewer.html".into())
        )
        .title("Pivo - Log Viewer")
        .inner_size(900.0, 700.0)
        .resizable(true)
        .build()
        .map_err(|e| format!("Failed to create log viewer window: {}", e))?;
    }
    
    Ok(())
}

#[tauri::command]
pub async fn open_project_window(
    project_id: String,
    project_name: String,
    state: State<'_, AppState>
) -> Result<String, String> {
    state.window_manager.open_project_window(&project_id, &project_name).await
}

#[tauri::command]
pub async fn close_project_window(
    project_id: String,
    state: State<'_, AppState>
) -> Result<(), String> {
    state.window_manager.close_project_window(&project_id).await
}

#[tauri::command]
pub async fn get_project_window(
    project_id: String,
    state: State<'_, AppState>
) -> Result<Option<String>, String> {
    Ok(state.window_manager.get_project_window(&project_id).await)
}

#[tauri::command]
pub async fn list_open_project_windows(
    state: State<'_, AppState>
) -> Result<Vec<(String, String)>, String> {
    Ok(state.window_manager.list_open_projects().await)
}
```

## /src-tauri/src/db/mod.rs

```rs path="/src-tauri/src/db/mod.rs" 
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite, migrate::Migrator};
use tauri::{AppHandle, Manager};

pub type DbPool = Pool<Sqlite>;

// Embed migrations at compile time
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");

pub async fn init_database(app_handle: &AppHandle) -> Result<DbPool, Box<dyn std::error::Error>> {
    let app_dir = app_handle
        .path()
        .app_data_dir()?;
    
    // Create app data directory if it doesn't exist
    std::fs::create_dir_all(&app_dir)?;
    
    let db_path = app_dir.join("pivo.db");
    let db_url = format!("sqlite://{}?mode=rwc", db_path.display());
    
    // Create connection pool
    let pool = SqlitePoolOptions::new()
        .max_connections(5)
        .connect(&db_url)
        .await?;
    
    // Run embedded migrations using SQLx's standard approach
    match MIGRATOR.run(&pool).await {
        Ok(_) => {
            log::info!("Database migrations completed successfully");
            Ok(pool)
        }
        Err(e) => {
            log::error!("Database migration failed: {}", e);
            
            // Close the connection pool
            pool.close().await;
            
            // Delete the database file
            if db_path.exists() {
                log::warn!("Removing corrupted database file: {:?}", db_path);
                if let Err(remove_err) = std::fs::remove_file(&db_path) {
                    log::error!("Failed to remove database file: {}", remove_err);
                    return Err(Box::new(std::io::Error::new(
                        std::io::ErrorKind::Other,
                        format!("Migration failed and couldn't remove database: {}", e)
                    )));
                }
                
                // Try to recreate the database
                log::info!("Attempting to recreate database...");
                let new_pool = SqlitePoolOptions::new()
                    .max_connections(5)
                    .connect(&db_url)
                    .await?;
                
                // Try migrations again
                match MIGRATOR.run(&new_pool).await {
                    Ok(_) => {
                        log::info!("Database recreated and migrations completed successfully");
                        Ok(new_pool)
                    }
                    Err(retry_err) => {
                        log::error!("Failed to recreate database: {}", retry_err);
                        Err(Box::new(retry_err))
                    }
                }
            } else {
                Err(Box::new(e))
            }
        }
    }
}
```

## /src-tauri/src/main.rs

```rs path="/src-tauri/src/main.rs" 
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    pivo_lib::run()
}

```

## /src-tauri/src/utils/mod.rs

```rs path="/src-tauri/src/utils/mod.rs" 
pub mod command;
```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!