diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 25f111e037..69ba06f3b3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - # pnpm workspace for the UI (desktop, acp, text, sdk, goose-binary/*, goose2). + # pnpm workspace for the UI (desktop, acp, text, sdk, goose-binary/*). # Point at the workspace ROOT where pnpm-lock.yaml lives so Dependabot updates # both the child package.json AND ui/pnpm-lock.yaml in one PR. - package-ecosystem: "npm" diff --git a/.github/workflows/bundle-goose2-manual.yml b/.github/workflows/bundle-goose2-manual.yml deleted file mode 100644 index 8655287054..0000000000 --- a/.github/workflows/bundle-goose2-manual.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Manual Goose 2 Bundle (Unsigned)" - -on: - workflow_dispatch: - inputs: - branch: - description: "Branch name to bundle app from" - required: true - type: string - cli-run-id: - description: "Run ID of a build-cli workflow to pull the goose binary from (optional, builds from source if empty)" - required: false - type: string - default: "" - -jobs: - bundle-goose2: - uses: ./.github/workflows/bundle-goose2.yml - permissions: - id-token: write - contents: read - actions: read - with: - signing: false - ref: ${{ inputs.branch }} - cli-run-id: ${{ inputs.cli-run-id }} diff --git a/.github/workflows/bundle-goose2.yml b/.github/workflows/bundle-goose2.yml deleted file mode 100644 index 4350744d32..0000000000 --- a/.github/workflows/bundle-goose2.yml +++ /dev/null @@ -1,774 +0,0 @@ -# Reusable workflow that bundles the Goose 2 (Tauri) desktop app. -# Produces .app / .dmg on macOS and .deb / .AppImage on Linux. -# -# The justfile recipe is: `just bundle` → `pnpm tauri build` -# -# Called from release.yml, canary.yml, or manually via bundle-goose2-manual.yml. -# -# The goose CLI binary can either be built from source (default) or pulled -# from a prior build-cli.yml run by passing `cli-run-id`. This avoids -# redundant ~20min Rust builds during release pipelines. -name: "Bundle Goose 2 Desktop" - -on: - workflow_call: - inputs: - version: - description: "Version to set for the build (leave empty to use Cargo.toml default)" - required: false - default: "" - type: string - signing: - description: "Whether to perform Apple signing and notarization (macOS only)" - required: false - default: false - type: boolean - quick_test: - description: "Whether to perform the quick launch test (macOS only)" - required: false - default: true - type: boolean - ref: - description: "Git ref to checkout (branch, tag, or SHA). Defaults to the triggering ref." - required: false - default: "" - type: string - environment: - description: "GitHub Environment containing signing secrets. Leave empty to skip." - required: false - default: "" - type: string - windows-signing: - description: "Whether to perform Windows signing via Azure Trusted Signing" - required: false - default: false - type: boolean - cli-run-id: - description: > - Run ID of a prior build-cli.yml workflow run to download the goose - binary from. When empty, the goose CLI is built from source. - required: false - default: "" - type: string - -jobs: - # ─────────────────────────────────────────────── - # macOS ARM (Apple Silicon) - # ─────────────────────────────────────────────── - bundle-macos-arm: - name: "macOS ARM64" - runs-on: macos-latest - environment: ${{ inputs.environment || '' }} - timeout-minutes: 60 - env: - MACOSX_DEPLOYMENT_TARGET: "12.0" - permissions: - id-token: write - contents: read - actions: read - outputs: - artifact-url: ${{ steps.upload.outputs.artifact-url }} - steps: - - name: Debug workflow info - env: - INPUT_REF: ${{ inputs.ref }} - INPUT_VERSION: ${{ inputs.version }} - INPUT_SIGNING: ${{ inputs.signing }} - INPUT_CLI_RUN_ID: ${{ inputs.cli-run-id }} - run: | - echo "=== Goose 2 Bundle (macOS ARM64) ===" - echo "Ref: ${INPUT_REF:-}" - echo "Version: ${INPUT_VERSION:-}" - echo "Signing: ${INPUT_SIGNING}" - echo "CLI run ID: ${INPUT_CLI_RUN_ID:-}" - df -h - - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ inputs.ref != '' && inputs.ref || '' }} - - # ── Version stamps ── - - name: Update versions - if: inputs.version != '' - env: - VERSION: ${{ inputs.version }} - run: | - # Root Cargo.toml (workspace version) - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml - rm -f Cargo.toml.bak - - # Tauri Cargo.toml - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml - rm -f ui/goose2/src-tauri/Cargo.toml.bak - - # package.json - source ./bin/activate-hermit - cd ui/goose2 - npm pkg set "version=${VERSION}" - - # ── Goose CLI: download from prior run OR build from source ── - - name: Download goose CLI from build-cli run - if: inputs.cli-run-id != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: goose-aarch64-apple-darwin - run-id: ${{ inputs.cli-run-id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - path: cli-artifact - - - name: Extract downloaded goose CLI - if: inputs.cli-run-id != '' - run: | - TRIPLE="aarch64-apple-darwin" - mkdir -p target/release - # The artifact contains goose-.tar.bz2 with goose inside - tar -xjf "cli-artifact/goose-${TRIPLE}.tar.bz2" -C target/release/ - chmod +x target/release/goose - cp target/release/goose "target/release/goose-${TRIPLE}" - ls -lh "target/release/goose-${TRIPLE}" - - - name: Cache Rust dependencies - if: inputs.cli-run-id == '' - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - key: goose2-macos-arm64 - - - name: Build goose CLI (release) - if: inputs.cli-run-id == '' - run: | - source ./bin/activate-hermit - cargo build --release -p goose-cli --bin goose - - - name: Prepare goose binary with target triple - if: inputs.cli-run-id == '' - run: | - TRIPLE=$(rustc --print host-tuple) - cp target/release/goose "target/release/goose-${TRIPLE}" - ls -lh "target/release/goose-${TRIPLE}" - - # ── Frontend: pnpm install + SDK build ── - - name: Cache pnpm dependencies - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 - with: - path: | - ui/goose2/node_modules - ui/sdk/node_modules - .hermit/node/cache - key: goose2-pnpm-macos-arm64-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }} - restore-keys: | - goose2-pnpm-macos-arm64-${{ runner.os }}- - - - name: Install pnpm dependencies - run: | - source ./bin/activate-hermit - cd ui - pnpm install --frozen-lockfile - - - name: Build SDK - run: | - source ./bin/activate-hermit - cd ui/sdk - pnpm build - - # ── Apple signing ── - - name: Import Apple signing certificate - if: inputs.signing - uses: ./.github/actions/apple-codesign - with: - certificate-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - # ── Tauri bundle ── - - name: Bundle Goose 2 (pnpm tauri build) - env: - APPLE_SIGNING_IDENTITY: ${{ inputs.signing && 'Developer ID Application' || '' }} - APPLE_ID: ${{ inputs.signing && secrets.APPLE_ID || '' }} - APPLE_PASSWORD: ${{ inputs.signing && secrets.APPLE_ID_PASSWORD || '' }} - APPLE_TEAM_ID: ${{ inputs.signing && secrets.APPLE_TEAM_ID || '' }} - working-directory: ui/goose2 - run: | - source ../../bin/activate-hermit - pnpm tauri build - - - name: Clean up signing keychain - if: always() - run: | - if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then - security delete-keychain "$KEYCHAIN_PATH" || true - fi - - # ── Upload ── - - name: List bundle output - run: | - BUNDLE_DIR="ui/goose2/src-tauri/target/release/bundle" - echo "=== Bundle contents ===" - find "$BUNDLE_DIR" -type f 2>/dev/null || echo "(no bundle output found)" - - - name: Upload Goose 2 macOS ARM64 artifact - id: upload - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: Goose2-darwin-arm64 - path: | - ui/goose2/src-tauri/target/release/bundle/dmg/*.dmg - ui/goose2/src-tauri/target/release/bundle/macos/*.app - if-no-files-found: error - - # ── Smoke test ── - - name: Quick launch test - if: inputs.quick_test - run: | - APP_PATH=$(find ui/goose2/src-tauri/target/release/bundle/macos -maxdepth 1 -name "*.app" | head -1) - if [ -z "$APP_PATH" ]; then - echo "No .app found, skipping launch test" - exit 0 - fi - - xattr -cr "$APP_PATH" - echo "Opening $APP_PATH..." - open -g "$APP_PATH" - sleep 5 - - if pgrep -f "Goose.app/Contents/MacOS" > /dev/null; then - echo "✅ App is running" - else - echo "❌ App did not stay open" - exit 1 - fi - - pkill -f "Goose.app/Contents/MacOS" || true - - # ─────────────────────────────────────────────── - # macOS Intel (x86_64) - # ─────────────────────────────────────────────── - bundle-macos-intel: - name: "macOS x86_64" - runs-on: macos-15-intel - environment: ${{ inputs.environment || '' }} - timeout-minutes: 60 - env: - MACOSX_DEPLOYMENT_TARGET: "12.0" - permissions: - id-token: write - contents: read - actions: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ inputs.ref != '' && inputs.ref || '' }} - - - name: Update versions - if: inputs.version != '' - env: - VERSION: ${{ inputs.version }} - run: | - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml - rm -f Cargo.toml.bak - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml - rm -f ui/goose2/src-tauri/Cargo.toml.bak - source ./bin/activate-hermit - cd ui/goose2 - npm pkg set "version=${VERSION}" - - # ── Goose CLI: download from prior run OR build from source ── - - name: Download goose CLI from build-cli run - if: inputs.cli-run-id != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: goose-x86_64-apple-darwin - run-id: ${{ inputs.cli-run-id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - path: cli-artifact - - - name: Extract downloaded goose CLI - if: inputs.cli-run-id != '' - run: | - TARGET="x86_64-apple-darwin" - mkdir -p target/release - tar -xjf "cli-artifact/goose-${TARGET}.tar.bz2" -C target/release/ - chmod +x target/release/goose - cp target/release/goose "target/release/goose-${TARGET}" - ls -lh "target/release/goose-${TARGET}" - - - name: Cache Rust dependencies - if: inputs.cli-run-id == '' - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - key: goose2-macos-x86_64 - - - name: Install Intel target for both toolchains - if: inputs.cli-run-id == '' - run: | - source ./bin/activate-hermit - rustup target add x86_64-apple-darwin - cd ui/goose2/src-tauri - rustup target add x86_64-apple-darwin - - - name: Build goose CLI for Intel (x86_64-apple-darwin) - if: inputs.cli-run-id == '' - run: | - source ./bin/activate-hermit - cargo build --release -p goose-cli --bin goose --target x86_64-apple-darwin - - - name: Prepare goose binary with target triple - if: inputs.cli-run-id == '' - run: | - TARGET="x86_64-apple-darwin" - mkdir -p target/release - cp "target/${TARGET}/release/goose" "target/release/goose-${TARGET}" - ls -lh "target/release/goose-${TARGET}" - - # ── Intel target still needed for Tauri's own Rust build ── - - name: Install Intel target for Tauri toolchain - run: | - source ./bin/activate-hermit - rustup target add x86_64-apple-darwin - cd ui/goose2/src-tauri - rustup target add x86_64-apple-darwin - - # ── Frontend ── - - name: Cache pnpm dependencies - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 - with: - path: | - ui/goose2/node_modules - ui/sdk/node_modules - .hermit/node/cache - key: goose2-pnpm-macos-intel-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }} - restore-keys: | - goose2-pnpm-macos-intel-${{ runner.os }}- - - - name: Install pnpm dependencies - run: | - source ./bin/activate-hermit - cd ui - pnpm install --frozen-lockfile - - - name: Build SDK - run: | - source ./bin/activate-hermit - cd ui/sdk - pnpm build - - # ── Apple signing ── - - name: Import Apple signing certificate - if: inputs.signing - uses: ./.github/actions/apple-codesign - with: - certificate-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - # ── Tauri bundle ── - - name: Bundle Goose 2 for Intel - env: - APPLE_SIGNING_IDENTITY: ${{ inputs.signing && 'Developer ID Application' || '' }} - APPLE_ID: ${{ inputs.signing && secrets.APPLE_ID || '' }} - APPLE_PASSWORD: ${{ inputs.signing && secrets.APPLE_ID_PASSWORD || '' }} - APPLE_TEAM_ID: ${{ inputs.signing && secrets.APPLE_TEAM_ID || '' }} - working-directory: ui/goose2 - run: | - source ../../bin/activate-hermit - pnpm tauri build --target x86_64-apple-darwin - - - name: Clean up signing keychain - if: always() - run: | - if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then - security delete-keychain "$KEYCHAIN_PATH" || true - fi - - # ── Upload ── - - name: List bundle output - run: | - BUNDLE_DIR="ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle" - echo "=== Bundle contents ===" - find "$BUNDLE_DIR" -type f 2>/dev/null || echo "(no bundle output found)" - - - name: Upload Goose 2 macOS Intel artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: Goose2-darwin-x64 - path: | - ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg - ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/*.app - if-no-files-found: error - - - name: Quick launch test - if: inputs.quick_test - run: | - APP_PATH=$(find ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle/macos -maxdepth 1 -name "*.app" 2>/dev/null | head -1) - if [ -z "$APP_PATH" ]; then - echo "No .app found, skipping launch test" - exit 0 - fi - - xattr -cr "$APP_PATH" - echo "Opening $APP_PATH..." - open -g "$APP_PATH" - sleep 5 - - if pgrep -f "Goose.app/Contents/MacOS" > /dev/null; then - echo "✅ App is running" - else - echo "❌ App did not stay open" - exit 1 - fi - - pkill -f "Goose.app/Contents/MacOS" || true - - # ─────────────────────────────────────────────── - # Linux x86_64 - # ─────────────────────────────────────────────── - bundle-linux: - name: "Linux x86_64" - runs-on: ubuntu-24.04 - timeout-minutes: 60 - permissions: - contents: read - actions: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ inputs.ref != '' && inputs.ref || '' }} - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - libgtk-3-dev \ - libwebkit2gtk-4.1-dev \ - libappindicator3-dev \ - librsvg2-dev \ - patchelf \ - protobuf-compiler \ - libvulkan-dev \ - libvulkan1 \ - glslc - - - name: Update versions - if: inputs.version != '' - env: - VERSION: ${{ inputs.version }} - run: | - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml - rm -f Cargo.toml.bak - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml - rm -f ui/goose2/src-tauri/Cargo.toml.bak - source ./bin/activate-hermit - cd ui/goose2 - npm pkg set "version=${VERSION}" - - # ── Goose CLI: download from prior run OR build from source ── - - name: Download goose CLI from build-cli run - if: inputs.cli-run-id != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: goose-x86_64-unknown-linux-gnu - run-id: ${{ inputs.cli-run-id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - path: cli-artifact - - - name: Extract downloaded goose CLI - if: inputs.cli-run-id != '' - run: | - TRIPLE="x86_64-unknown-linux-gnu" - mkdir -p target/release - tar -xjf "cli-artifact/goose-${TRIPLE}.tar.bz2" -C target/release/ - chmod +x target/release/goose - cp target/release/goose "target/release/goose-${TRIPLE}" - ls -lh "target/release/goose-${TRIPLE}" - - - name: Cache Rust dependencies - if: inputs.cli-run-id == '' - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - key: goose2-linux-x86_64 - - - name: Build goose CLI (release) - if: inputs.cli-run-id == '' - run: | - source ./bin/activate-hermit - cargo build --release -p goose-cli --bin goose --features vulkan - - - name: Prepare goose binary with target triple - if: inputs.cli-run-id == '' - run: | - TRIPLE=$(rustc --print host-tuple) - cp target/release/goose "target/release/goose-${TRIPLE}" - ls -lh "target/release/goose-${TRIPLE}" - - # ── Frontend ── - - name: Cache pnpm dependencies - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 - with: - path: | - ui/goose2/node_modules - ui/sdk/node_modules - .hermit/node/cache - key: goose2-pnpm-linux-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }} - restore-keys: | - goose2-pnpm-linux-${{ runner.os }}- - - - name: Install pnpm dependencies - run: | - source ./bin/activate-hermit - cd ui - pnpm install --frozen-lockfile - - - name: Build SDK - run: | - source ./bin/activate-hermit - cd ui/sdk - pnpm build - - # ── Tauri bundle ── - - name: Bundle Goose 2 (pnpm tauri build) - working-directory: ui/goose2 - run: | - source ../../bin/activate-hermit - pnpm tauri build - - # ── Upload ── - - name: List bundle output - run: | - BUNDLE_DIR="ui/goose2/src-tauri/target/release/bundle" - echo "=== Bundle contents ===" - find "$BUNDLE_DIR" -type f 2>/dev/null | head -30 - echo "" - echo "=== File sizes ===" - find "$BUNDLE_DIR" -type f -exec ls -lh {} \; 2>/dev/null | head -20 - - - name: Upload .deb package - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: Goose2-linux-x64-deb - path: ui/goose2/src-tauri/target/release/bundle/deb/*.deb - if-no-files-found: warn - - - name: Upload AppImage - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: Goose2-linux-x64-appimage - path: ui/goose2/src-tauri/target/release/bundle/appimage/*.AppImage - if-no-files-found: warn - - - name: Upload RPM package - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: Goose2-linux-x64-rpm - path: ui/goose2/src-tauri/target/release/bundle/rpm/*.rpm - if-no-files-found: warn - - # ─────────────────────────────────────────────── - # Windows x86_64 - # ─────────────────────────────────────────────── - bundle-windows: - name: "Windows x86_64" - runs-on: windows-latest - timeout-minutes: 60 - permissions: - id-token: write - contents: read - actions: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ inputs.ref != '' && inputs.ref || '' }} - - # Hermit doesn't work on Windows — install node/pnpm directly - - name: Set up Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - with: - node-version: 24 - - - name: Install pnpm - run: npm install -g pnpm@10.33.0 - - - name: Update versions - if: inputs.version != '' - shell: bash - env: - VERSION: ${{ inputs.version }} - run: | - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml - rm -f Cargo.toml.bak - sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml - rm -f ui/goose2/src-tauri/Cargo.toml.bak - cd ui/goose2 - npm pkg set "version=${VERSION}" - - # ── Goose CLI: download from prior run OR build from source ── - - name: Download goose CLI from build-cli run - if: inputs.cli-run-id != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: goose-x86_64-pc-windows-msvc - run-id: ${{ inputs.cli-run-id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - path: cli-artifact - - - name: Extract downloaded goose CLI - if: inputs.cli-run-id != '' - shell: bash - run: | - TARGET="x86_64-pc-windows-msvc" - mkdir -p target/release - # The zip contains goose-package/goose.exe - cd cli-artifact - 7z x "goose-${TARGET}.zip" - cd .. - cp cli-artifact/goose-package/goose.exe target/release/ - # Tauri externalBin appends target triple + .exe on Windows - cp target/release/goose.exe "target/release/goose-${TARGET}.exe" - ls -lh "target/release/goose-${TARGET}.exe" - - - name: Cache Rust dependencies - if: inputs.cli-run-id == '' - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - key: goose2-windows-x86_64 - - - name: Setup Rust - if: inputs.cli-run-id == '' - shell: bash - run: | - rustup show - rustup target add x86_64-pc-windows-msvc - - - name: Build goose CLI (release) - if: inputs.cli-run-id == '' - shell: bash - run: | - cargo build --release --target x86_64-pc-windows-msvc -p goose-cli --bin goose - - - name: Prepare goose binary with target triple - if: inputs.cli-run-id == '' - shell: bash - run: | - TARGET="x86_64-pc-windows-msvc" - cp "target/${TARGET}/release/goose.exe" "target/release/goose-${TARGET}.exe" - ls -lh "target/release/goose-${TARGET}.exe" - - # ── Frontend ── - - name: Cache pnpm dependencies - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 - with: - path: | - ui/goose2/node_modules - ui/sdk/node_modules - key: goose2-pnpm-windows-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }} - restore-keys: | - goose2-pnpm-windows-${{ runner.os }}- - - - name: Install pnpm dependencies - shell: bash - run: | - cd ui - pnpm install --frozen-lockfile - - - name: Build SDK - shell: bash - run: | - cd ui/sdk - pnpm build - - # ── Tauri bundle ── - - name: Bundle Goose 2 (pnpm tauri build) - shell: bash - working-directory: ui/goose2 - run: | - pnpm tauri build --target x86_64-pc-windows-msvc - - # ── Upload ── - - name: List bundle output - shell: bash - run: | - BUNDLE_DIR="ui/goose2/src-tauri/target/x86_64-pc-windows-msvc/release/bundle" - echo "=== Bundle contents ===" - find "$BUNDLE_DIR" -type f 2>/dev/null || echo "(no bundle output found)" - - - name: Upload NSIS installer - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: Goose2-windows-x64-nsis - path: ui/goose2/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe - if-no-files-found: warn - - - name: Upload MSI installer - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: Goose2-windows-x64-msi - path: ui/goose2/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi - if-no-files-found: warn - - sign-windows: - name: "Sign Windows installers" - needs: bundle-windows - if: inputs.windows-signing - runs-on: windows-latest - environment: signing - permissions: - id-token: write - contents: read - actions: read - steps: - - name: Download NSIS installer - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-windows-x64-nsis - path: unsigned/nsis - - - name: Download MSI installer - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-windows-x64-msi - path: unsigned/msi - - - name: Azure login - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Sign Windows installers with Azure Trusted Signing - uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v1 - with: - endpoint: ${{ secrets.AZURE_SIGNING_ENDPOINT }} - trusted-signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} - files-folder: ${{ github.workspace }}/unsigned - files-folder-filter: exe,msi - files-folder-recurse: true - - - name: Verify signed installers - shell: pwsh - run: | - $files = Get-ChildItem -Path unsigned -Recurse -Include *.exe,*.msi - foreach ($file in $files) { - Write-Output "Verifying signature: $($file.FullName)" - $sig = Get-AuthenticodeSignature $file.FullName - if ($sig.Status -ne "Valid") { - throw "Signature invalid for $($file.Name): $($sig.Status)" - } - Write-Output "✅ Signature valid: $($file.Name)" - } - - - name: Upload signed NSIS installer - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: Goose2-windows-x64-nsis-signed - path: unsigned/nsis/*.exe - if-no-files-found: error - - - name: Upload signed MSI installer - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: Goose2-windows-x64-msi-signed - path: unsigned/msi/*.msi - if-no-files-found: error diff --git a/.github/workflows/goose2-ci.yml b/.github/workflows/goose2-ci.yml deleted file mode 100644 index 01e56fef2d..0000000000 --- a/.github/workflows/goose2-ci.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: "Goose 2 CI" - -on: - push: - branches: [main] - paths: - - "ui/goose2/**" - - "ui/sdk/**" - pull_request: - branches: [main] - paths: - - "ui/goose2/**" - - "ui/sdk/**" - merge_group: - branches: [main] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -defaults: - run: - working-directory: ui/goose2 - -jobs: - lint: - name: Lint & Format - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 - with: - version: 10.30.3 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: ui/pnpm-lock.yaml - - run: pnpm install --frozen-lockfile - - name: Build SDK - run: pnpm build - working-directory: ui/sdk - - run: pnpm check - - run: pnpm typecheck - - test: - name: Unit Tests - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 - with: - version: 10.30.3 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: ui/pnpm-lock.yaml - - run: pnpm install --frozen-lockfile - - name: Build SDK - run: pnpm build - working-directory: ui/sdk - - run: pnpm test - - desktop: - name: Desktop Build & E2E - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 - with: - version: 10.30.3 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: ui/pnpm-lock.yaml - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libgtk-3-dev \ - libwebkit2gtk-4.1-dev \ - libappindicator3-dev \ - librsvg2-dev \ - patchelf - - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 - with: - rust-src-dir: ui/goose2 - - - name: Cache Rust - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ui/goose2/src-tauri/target - key: ${{ runner.os }}-goose2-cargo-${{ hashFiles('ui/goose2/src-tauri/Cargo.lock') }} - - - run: pnpm install --frozen-lockfile - - - name: Build SDK - run: pnpm build - working-directory: ui/sdk - - - name: Build frontend - run: pnpm build - - - name: Mock goose binary - working-directory: . - run: mkdir -p target/release && touch target/release/goose-$(rustc --print host-tuple) - - - name: Check Tauri - run: cd src-tauri && cargo check - - - name: Clippy - run: cd src-tauri && cargo clippy -- -D warnings - - - name: Format check - run: cd src-tauri && cargo fmt --check - - - name: Install Playwright Chromium - run: pnpm exec playwright install --with-deps chromium - - - name: Run E2E tests - run: pnpm exec playwright test - - - name: Upload test artifacts - if: failure() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: playwright-report - path: | - ui/goose2/playwright-report/ - ui/goose2/test-results/ - retention-days: 7 - - rust-lint: - name: Rust Lint - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libgtk-3-dev \ - libwebkit2gtk-4.1-dev \ - libappindicator3-dev \ - librsvg2-dev \ - patchelf - - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 - with: - rust-src-dir: ui/goose2 - components: rustfmt, clippy - - - name: Cache Rust - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ui/goose2/src-tauri/target - key: ${{ runner.os }}-goose2-cargo-${{ hashFiles('ui/goose2/src-tauri/Cargo.lock') }} - - - name: Mock goose binary - working-directory: . - run: mkdir -p target/release && touch target/release/goose-$(rustc --print host-tuple) - - - name: Format check - run: cd src-tauri && cargo fmt --check - - - name: Clippy - run: cd src-tauri && cargo clippy -- -D warnings diff --git a/.github/workflows/release-goose2.yml b/.github/workflows/release-goose2.yml deleted file mode 100644 index 3724cb1826..0000000000 --- a/.github/workflows/release-goose2.yml +++ /dev/null @@ -1,192 +0,0 @@ -on: - push: - tags: - - "v2.*" - workflow_dispatch: - inputs: - version: - description: "Version string (e.g. 2.0.0-rc.1). Used when testing from a branch." - required: true - type: string - cli-run-id: - description: "Run ID of a build-cli workflow to pull goose binaries from (skips CLI build step)" - required: false - type: string - default: "" - -name: "Release Goose 2" - -permissions: - id-token: write # Sigstore OIDC signing + Azure OIDC (Windows signing) - contents: write # Creating releases + actions/checkout - actions: read # Downloading artifacts across workflow runs - attestations: write # SLSA build provenance attestations - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - prepare-version: - name: Prepare Version - runs-on: ubuntu-latest - outputs: - version: ${{ steps.set-version.outputs.version }} - steps: - - name: Extract version - id: set-version - run: | - if [ -n "${{ inputs.version }}" ]; then - VERSION="${{ inputs.version }}" - else - # Strip the leading "v" from the tag (e.g. v2.0.0 → 2.0.0) - VERSION="${GITHUB_REF_NAME#v}" - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "Release version: $VERSION" - - build-cli: - if: inputs.cli-run-id == '' - needs: [prepare-version] - uses: ./.github/workflows/build-cli.yml - with: - version: ${{ needs.prepare-version.outputs.version }} - - bundle-goose2: - needs: [prepare-version, build-cli] - if: ${{ !cancelled() && needs.prepare-version.result == 'success' && (needs.build-cli.result == 'success' || needs.build-cli.result == 'skipped') }} - uses: ./.github/workflows/bundle-goose2.yml - permissions: - id-token: write - contents: read - actions: read - with: - version: ${{ needs.prepare-version.outputs.version }} - signing: true - windows-signing: true - environment: signing - cli-run-id: ${{ inputs.cli-run-id != '' && inputs.cli-run-id || github.run_id }} - secrets: inherit - - install-script: - name: Upload Install Script - runs-on: ubuntu-latest - if: inputs.cli-run-id == '' - needs: [build-cli] - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: download_cli.sh - path: download_cli.sh - - release: - name: Release - runs-on: ubuntu-latest - needs: [prepare-version, build-cli, install-script, bundle-goose2] - if: ${{ !cancelled() && needs.bundle-goose2.result == 'success' }} - permissions: - contents: write - id-token: write - actions: read - attestations: write - steps: - - name: Download CLI artifacts - if: needs.build-cli.result == 'success' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: goose-* - merge-multiple: true - path: release - - - name: Download install script - if: needs.install-script.result == 'success' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: download_cli.sh - path: release - - - name: Download macOS ARM64 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-darwin-arm64 - path: release - - - name: Download macOS Intel - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-darwin-x64 - path: release - - - name: Download Linux .deb - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-linux-x64-deb - path: release - continue-on-error: true - - - name: Download Linux AppImage - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-linux-x64-appimage - path: release - continue-on-error: true - - - name: Download Linux RPM - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-linux-x64-rpm - path: release - continue-on-error: true - - - name: Download signed Windows NSIS installer - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-windows-x64-nsis-signed - path: release - - - name: Download signed Windows MSI installer - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Goose2-windows-x64-msi-signed - path: release - - - name: List downloaded artifacts - run: | - echo "=== All release artifacts ===" - find release -type f | sort - - - name: Attest build provenance - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-path: | - release/goose-*.tar.bz2 - release/goose-*.tar.gz - release/goose-*.zip - release/**/*.dmg - release/*.exe - release/*.msi - release/*.deb - release/*.rpm - release/*.AppImage - release/download_cli.sh - - # Create/update the versioned pre-release (e.g. v2.0.0) - - name: Release versioned - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - prerelease: true - artifacts: | - release/goose-*.tar.bz2 - release/goose-*.tar.gz - release/goose-*.zip - release/**/*.dmg - release/*.exe - release/*.msi - release/*.deb - release/*.rpm - release/*.AppImage - release/download_cli.sh - allowUpdates: true - omitBody: true diff --git a/Cargo.toml b/Cargo.toml index 65beb7622c..ac66c47e1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ # Mainly for cargo-machete to not error out during inspection. "vendor/v8", ] -exclude = ["ui/goose2/src-tauri"] resolver = "2" [workspace.package] diff --git a/Justfile b/Justfile index 4c7860e547..f615a25ce3 100644 --- a/Justfile +++ b/Justfile @@ -1,7 +1,5 @@ # Justfile -mod goose2 'ui/goose2' - # list all tasks default: @just --list @@ -450,8 +448,3 @@ build-test-tools: record-mcp-tests: build-test-tools GOOSE_RECORD_MCP=1 cargo test --package goose --test mcp_integration_test git add crates/goose/tests/mcp_replays/ - -bundle-goose2: - cargo build --release --package goose-cli --bin goose {{linux_vulkan_features}} - cp target/release/goose target/release/goose-$(rustc --print host-tuple) - @just goose2::bundle diff --git a/ui/desktop/spike-doc/acp-config-extensions-first-spike.md b/ui/desktop/spike-doc/acp-config-extensions-first-spike.md deleted file mode 100644 index a0cf2e082f..0000000000 --- a/ui/desktop/spike-doc/acp-config-extensions-first-spike.md +++ /dev/null @@ -1,380 +0,0 @@ -# ACP First Spike: Configured Extensions List - -## Purpose - -Use configured extensions as the first `ui/desktop` REST-to-ACP migration slice. - -This spike is intentionally smaller than chat or sessions. It proves that desktop can talk directly to `/acp` through `goosed` during migration, without depending on ACP session ID behavior or chat streaming semantics. - -## Scope - -Migrate the read-only configured extensions list behind a feature flag. - -```text -Current REST: - GET /config/extensions - -Target ACP: - _goose/config/extensions -``` - -Do not migrate add/update/delete in this first slice. Those can remain REST until the read path and ACP client plumbing are proven. - -## Why This Slice - -- No session ID dependency. -- No chat streaming or prompt lifecycle. -- No tool approval. -- No message-shape conversion. -- Exercises direct renderer WebSocket to `/acp`. -- Exercises ACP initialization and custom `_goose/*` request dispatch. -- Easy to compare REST and ACP responses. - -## Relevant Existing Code - -Desktop REST read path: - -- `ui/desktop/src/components/ConfigContext.tsx` - - imports `getExtensions as apiGetExtensions` - - `refreshExtensions()` calls `apiGetExtensions()` - - initial provider load also calls `apiGetExtensions()` - -Desktop UI consumer: - -- `ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx` - - consumes `extensionsList` from `useConfig()` - - calls `getExtensions(true)` to refresh - -REST backend: - -- `crates/goose-server/src/routes/config_management.rs` - - `GET /config/extensions` - - `POST /config/extensions` - - `DELETE /config/extensions/{name}` - -ACP backend: - -- `crates/goose/src/acp/server/extensions.rs` - - `_goose/config/extensions` - - `_goose/config/extensions/add` - - `_goose/config/extensions/remove` - - `_goose/config/extensions/toggle` - -ACP client reference: - -- `ui/goose2/src/shared/api/createWebSocketStream.ts` -- `ui/goose2/src/shared/api/acpConnection.ts` -- `ui/goose2/src/shared/api/acpApi.ts` - -Treat `ui/goose2` as a reference only. Do not share runtime code with it because `ui/goose2` is expected to move out of this repo. - -## Current Gap Found - -REST `GET /config/extensions` filters hidden extensions: - -```rust -goose::config::get_all_extensions() - .into_iter() - .filter(|ext| !goose::agents::extension_manager::is_hidden_extension(&ext.config.name())) -``` - -ACP `_goose/config/extensions` currently calls `crate::config::extensions::get_all_extensions()` and does not apply the same hidden-extension filter. - -Recommendation: fix ACP to match REST before enabling this feature flag. The goal is to prove the migration path without introducing UI-visible behavior differences. - -## Backend Plan - -## Step-By-Step Review Plan - -Work in small reviewable slices. After each step, stop and review before moving to the next one. - -### Step 1: Inspect router/auth shape - -Goal: confirm the exact `goosed` router composition and ACP transport shape before editing. - -Review points: - -- where REST `X-Secret-Key` middleware is applied -- whether ACP can be mounted as a separate branch -- whether `goose::acp::transport::create_router` causes route collisions -- where ACP token auth should live -- whether Cargo dependencies already allow `goose-server` to call ACP router code - -Expected outcome: a precise patch plan for backend mounting. - -### Step 2: Add ACP-only router helper - -Goal: avoid route collisions by exposing only `/acp` routes for embedding in `goosed`. - -Likely file: - -- `crates/goose/src/acp/transport/mod.rs` - -Reason: existing `create_router(...)` includes `/health`, `/status`, and MCP app proxy routes. `goosed` already owns some of those routes. - -Expected outcome: a helper such as `create_acp_router(...)` or equivalent that only mounts: - -```text -/acp POST -/acp GET -/acp DELETE -``` - -### Step 3: Mount `/acp` in `goosed` - -Goal: serve REST and ACP from the same `goosed` process during migration. - -Likely files: - -- `crates/goose-server/src/commands/agent.rs` -- maybe `crates/goose-server/src/routes/mod.rs` - -Expected shape: - -```text -goosed - REST routes protected by X-Secret-Key - /acp protected by ACP token auth -``` - -### Step 4: Expose ACP URL/token to renderer - -Goal: let renderer connect directly to `/acp` over WebSocket. - -Likely files: - -- `ui/desktop/src/main.ts` -- `ui/desktop/src/preload.ts` -- related Electron type declarations if present - -Expected renderer-facing value: - -```text -ws(s)://127.0.0.1:/acp?token= -``` - -### Step 5: Add minimal desktop ACP client - -Goal: create enough client plumbing to initialize ACP and call one custom method. - -Suggested files: - -```text -ui/desktop/src/acp/createWebSocketStream.ts -ui/desktop/src/acp/acpConnection.ts -ui/desktop/src/acp/acpApi.ts -``` - -Reference, not shared dependency: - -- `ui/goose2/src/shared/api/createWebSocketStream.ts` -- `ui/goose2/src/shared/api/acpConnection.ts` -- `ui/goose2/src/shared/api/acpApi.ts` - -### Step 6: Migrate configured extensions read path behind a flag - -Goal: call `_goose/config/extensions` for read-only extension listing when enabled. - -Primary integration file: - -- `ui/desktop/src/components/ConfigContext.tsx` - -Keep writes on REST in this slice: - -- add extension -- remove extension -- toggle extension -- bundled extension sync/prune - -### Step 7: Validate parity - -Goal: prove ACP and REST return equivalent visible extension data. - -Validation checklist is below. - -## Backend Details - -### Mount `/acp` in `goosed` - -Add the ACP Axum router to `goosed agent`. - -Likely files: - -- `crates/goose-server/src/commands/agent.rs` -- `crates/goose-server/src/routes/mod.rs` - -Use existing ACP pieces: - -```rust -goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig} -goose::acp::transport::create_router -``` - -Keep REST and ACP as separate route branches. - -Watch for route collisions: - -- `/status` -- `/mcp-app-proxy` -- `/mcp-app-guest` - -### Add ACP token auth - -The renderer should connect through direct WebSocket: - -```text -ws(s)://127.0.0.1:/acp?token= -``` - -Do not put `/acp` behind the REST `X-Secret-Key` middleware because browser WebSocket cannot set arbitrary headers. - -Guardrails: - -- Use a random ACP token for the `goosed` process. -- Prefer an ACP-specific token over reusing the raw REST `X-Secret-Key`. -- Never log full `/acp?token=...` URLs. -- Accept ACP token auth only for the local desktop backend. -- Keep REST `X-Secret-Key` for REST routes during migration. - -### Fix `_goose/config/extensions` parity - -Update ACP `_goose/config/extensions` to match REST behavior: - -- filter hidden extensions -- preserve warnings -- preserve the response shape needed by desktop - -The ACP response currently injects a `config_key` field. Verify the desktop shape expected by `ExtensionEntry` and normalize on the client if needed. - -## Desktop Plan - -### Add ACP client files - -Suggested files: - -```text -ui/desktop/src/acp/createWebSocketStream.ts -ui/desktop/src/acp/acpConnection.ts -ui/desktop/src/acp/acpApi.ts -``` - -Base these on `ui/goose2`, but adapt to Electron. - -Differences from `ui/goose2`: - -- URL lookup should use Electron, not Tauri. -- Build ACP URL from the existing `goosed` host. -- Append the ACP token. -- Initialize ACP once and reconnect on close. - -Example URL shape: - -```ts -const baseUrl = await window.electron.getGoosedHostPort(); -const acpUrl = baseUrl.replace(/^http/, 'ws') + '/acp?token=' + encodeURIComponent(token); -``` - -If `goosed` is running HTTPS, this becomes `wss://.../acp?...`. - -### Add ACP extensions API wrapper - -Add a wrapper such as: - -```ts -getConfigExtensionsViaAcp(): Promise -``` - -It should call: - -```text -_goose/config/extensions -``` - -Normalize the response to the current desktop `ExtensionResponse` shape: - -```ts -type ExtensionResponse = { - extensions: ExtensionEntry[]; - warnings: string[]; -}; -``` - -### Add a feature flag - -Suggested name: - -```text -acpConfigExtensions -``` - -The exact flag mechanism should follow existing desktop feature-flag conventions if present. If there is no suitable framework, use a local env/config gate for the spike. - -### Route read path through ACP only when enabled - -Primary integration file: - -- `ui/desktop/src/components/ConfigContext.tsx` - -Candidate call sites: - -- `refreshExtensions()` -- initial load effect that fetches extensions after bundled sync - -Keep these mutation paths on REST for the first spike: - -- `addExtension` -- `removeExtension` -- `toggleExtension` -- bundled extension sync/prune - -After REST mutations complete, refresh can use ACP when the flag is enabled. - -### Add fallback behavior - -For the spike, if ACP connection or `_goose/config/extensions` fails, log the error and fall back to REST. - -This fallback is only for the spike. Once a feature area is fully migrated, the matching REST endpoint should be removed. - -## Validation Checklist - -Compare REST and ACP for the same local config: - -- same visible extension count -- same names -- same `enabled` values -- same extension types (`builtin`, `stdio`, `streamable_http`, etc.) -- same warnings -- hidden extensions do not appear -- settings/extensions UI renders correctly -- add/update/delete still work through REST and refresh the list -- failed ACP connection falls back to REST while the spike flag is enabled - -## Removal Rule - -Do not remove `GET /config/extensions` after this first read-only spike if writes still use REST sync logic that depends on the endpoint. - -Remove the REST extension endpoints only after the full extension config surface has migrated: - -```text -GET /config/extensions -POST /config/extensions -DELETE /config/extensions/{name} -``` - -Corresponding ACP methods: - -```text -_goose/config/extensions -_goose/config/extensions/add -_goose/config/extensions/remove -_goose/config/extensions/toggle -``` - -## Done Criteria - -- `goosed` exposes token-authenticated `/acp`. -- `ui/desktop` can open a direct renderer WebSocket to `/acp`. -- ACP initialize succeeds. -- `_goose/config/extensions` returns REST-equivalent visible extension data. -- Feature flag can switch extension list reads between REST and ACP. -- Existing extension settings UI behaves the same under the ACP read path. diff --git a/ui/desktop/spike-doc/acp-migration-spike.md b/ui/desktop/spike-doc/acp-migration-spike.md deleted file mode 100644 index 9a02f604ef..0000000000 --- a/ui/desktop/spike-doc/acp-migration-spike.md +++ /dev/null @@ -1,707 +0,0 @@ -# Desktop ACP Migration Spike - -## Goal - -`ui/desktop` currently talks to `goosed agent` through REST/OpenAPI. We want to migrate the desktop app to talk to Goose through ACP, while keeping the migration gradual and avoiding a large bundle-size increase. - -The preferred migration direction is: - -- Keep bundling only `goosed` during migration to avoid shipping both large binaries. -- Add ACP serving at `/acp` inside `goosed agent` as a temporary bridge. -- Let `ui/desktop` talk directly to `/acp` for migrated surfaces. -- Keep existing REST routes only for unmigrated areas. -- Once all REST behavior has moved to ACP, stop bundling/running `goosed` and switch desktop to the existing `goose serve` ACP server. - -## Current Architecture - -### Desktop backend - -`goosed` is an Axum HTTP server. - -Relevant files: - -- `crates/goose-server/src/main.rs` -- `crates/goose-server/src/commands/agent.rs` -- `crates/goose-server/src/routes/mod.rs` -- `ui/desktop/src/goosed.ts` - -`ui/desktop/src/goosed.ts` finds and spawns the bundled `goosed` binary: - -```ts -const spawnCommand = goosedPath; -const spawnArgs = ['agent']; -``` - -The renderer configures the generated OpenAPI client against the `goosed` URL in `ui/desktop/src/renderer.tsx`. - -### Desktop REST usage - -`ui/desktop` imports generated API methods from `ui/desktop/src/api`. - -Important chat/session paths today: - -- `ui/desktop/src/sessions.ts` - - `startAgent` creates sessions. -- `ui/desktop/src/hooks/useChatStream.ts` - - `resumeAgent` - - `sessionReply` - - `sessionCancel` - - `getSession` - - `updateFromSession` -- `ui/desktop/src/hooks/useSessionEvents.ts` - - opens `GET /sessions/{id}/events` as SSE. - -The current REST streaming model is Goose-specific: - -- `POST /sessions/{id}/reply` -- `GET /sessions/{id}/events` -- event types such as `Message`, `Finish`, `Error`, `Notification`, `ActiveRequests` -- request routing through `request_id` / `chat_request_id` - -## Existing ACP Implementation - -Goose already has ACP support. - -Relevant files: - -- ACP agent implementation: `crates/goose/src/acp/server.rs` -- ACP transport: `crates/goose/src/acp/transport/mod.rs` -- ACP custom methods: `crates/goose/src/acp/server/custom_dispatch.rs` -- Custom request types: `crates/goose-sdk/src/custom_requests.rs` -- CLI entry points: `crates/goose-cli/src/cli.rs` - -There are two existing ACP modes: - -### `goose acp` - -Runs ACP over stdio: - -```rust -Some(Command::Acp { builtins }) => goose::acp::server::run(builtins).await -``` - -This is standard ACP, but in Electron it would require the main process to own stdio and bridge to the renderer. - -### `goose serve` - -Runs ACP over HTTP/SSE/WebSocket: - -```rust -Some(Command::Serve { host, port, builtins }) => handle_serve_command(host, port, builtins).await -``` - -The transport router registers: - -```rust -/health -/status -/acp POST -/acp GET -/acp DELETE -``` - -`GET /acp` upgrades to WebSocket when requested. Otherwise it behaves as SSE. `POST /acp` accepts JSON-RPC messages. - -## Bundle Size Finding - -Local release binary sizes: - -```text -target/release/goose 230M -target/release/goosed 218M -``` - -Bundling both would add roughly: - -```text -goosed + goose = 448M uncompressed -``` - -`ui/desktop/src/bin` currently contains both binaries locally: - -```text -ui/desktop/src/bin/goose 230M -ui/desktop/src/bin/goosed 218M -``` - -This is too large as a long-term plan. - -## Recommended Backend Strategy - -Do not bundle both `goose` and `goosed` for production migration. The temporary bridge is to mount `/acp` into `goosed`; the final backend is `goose serve`. - -Migration backend: - -1. Add ACP serving to `goosed agent`. -2. Keep REST routes available on the same process during migration. -3. Mount `/acp` in the existing `goosed` Axum app. -4. Gradually migrate desktop feature areas from REST to ACP. -5. Remove each relevant REST endpoint once its feature area is migrated. - -Migration shape: - -```text -ui/desktop renderer - -> http(s)://127.0.0.1:/acp - goosed process - existing REST routes temporarily - ACP /acp -``` - -This keeps the backend bundle around current `goosed` size plus a small ACP wiring delta, instead of adding a second 230M binary. - -Final backend: - -```text -ui/desktop renderer - -> ws(s)://127.0.0.1:/acp?token= - goose serve - standard ACP methods - _goose/* custom ACP methods -``` - -In the final state `goosed` is not bundled or spawned by the desktop app. - -Before deleting the `goosed` bridge, verify that the final `goose serve` path has the same -desktop-specific ACP behavior that the bridge depended on during migration. In particular: - -- config and data directories match the desktop app's expected Goose paths -- builtin extension setup matches what desktop needs -- ACP initialization uses the correct desktop platform identity -- any state initialized today by `goosed` startup is either no longer needed or is initialized by - the final desktop `goose serve` launch path - -## Migration Bridge vs Final Server - -Mounting `/acp` in `goosed` should be treated as a bridge, not the destination. - -```text -During migration: - ui/desktop - -> REST for unmigrated features - -> /acp for migrated features - - bundled backend: - goosed - REST routes - temporary /acp route -``` - -```text -After migration: - ui/desktop - -> /acp only - - bundled backend: - goose serve - standard ACP - _goose/* custom ACP methods -``` - -The migration rule is: - -```text -When a feature moves to /acp: - remove its corresponding REST endpoint from goosed -``` - -The final cutover from `goosed agent` to `goose serve` is blocked until no desktop runtime feature depends on REST/OpenAPI. - -Expected effort: - -- Mounting `/acp` in `goosed`: relatively easy because both servers are Axum and the ACP router already exists. -- Removing `goosed` later: medium effort, because every REST-only desktop capability must first be moved into standard ACP or `_goose/*` custom ACP methods. - -## Alternative Considered: Move REST Into `goose serve` - -It is possible to make `goose serve` also mount `goose-server` REST routes. That would make `goose-cli` depend on `goose-server`. - -Tradeoffs: - -- Simpler single `goose` binary packaging. -- But the general-purpose CLI binary gets desktop REST/OpenAPI/tunnel/gateway/server dependencies. -- Likely increases `goose` binary size. -- Blurs the CLI and desktop backend boundary. - -The cleaner migration bridge is the reverse: add ACP to `goosed` temporarily. The final target remains `goose serve`, not `goosed`. - -## Streaming Differences - -Adding `/acp` to `goosed` does not make the current desktop streaming code work unchanged. - -### Current REST streaming - -The current desktop chat stream expects: - -- `GET /sessions/{id}/events` -- Goose-specific `MessageEvent` objects -- `ActiveRequests` -- `request_id` / `chat_request_id` -- `Message`, `Finish`, `Error`, `Notification` - -### ACP streaming - -ACP streaming is protocol-level: - -- Client sends JSON-RPC `session/prompt`. -- Server emits `session/update` notifications. -- Updates include: - - agent message chunks - - user message chunks - - tool calls - - tool call updates - - usage updates - - session info updates - - config option updates - -Tool approval also changes: - -- REST uses `/action-required/tool-confirmation`. -- ACP sends `RequestPermissionRequest`. -- The client must respond on the ACP connection. - -## Recommended Client Strategy - -Use WebSocket ACP directly from the renderer. - -Preferred shape: - -```text -renderer -> wss://127.0.0.1:/acp -send initialize -send session/new -send session/load -send session/prompt -receive session/update notifications continuously -respond to permission requests -``` - -WebSocket is preferable to HTTP POST + SSE because it gives one bidirectional connection for requests, responses, notifications, and permission responses. Do not add an Electron-main IPC transport layer for normal ACP chat traffic. - -## Existing Reference: `ui/goose2` - -`ui/goose2` already has a client pattern that can be reused or adapted. - -Relevant files: - -- `ui/goose2/src/shared/api/createWebSocketStream.ts` -- `ui/goose2/src/shared/api/acpConnection.ts` -- `ui/goose2/src/shared/api/acpApi.ts` -- `ui/goose2/src-tauri/src/services/acp/goose_serve.rs` - -`ui/goose2`: - -- gets a `/acp` WebSocket URL from Tauri -- creates a WebSocket stream -- creates a `GooseClient` -- initializes ACP with client capabilities -- routes `sessionUpdate` notifications through a handler -- exposes APIs such as: - - `listSessions` - - `newSession` - - `loadSession` - - `prompt` - - `cancelSession` - - `setProvider` - - `setModel` - - `_goose/*` custom methods - -`ui/desktop` can use the same pattern, replacing Tauri URL lookup with Electron URL lookup. -Because `ui/goose2` is expected to move out of this repo in the future, desktop should not share runtime code with it. Treat `ui/goose2` as a reference implementation and copy/adapt the small ACP client pieces into `ui/desktop`. - -Example URL derivation: - -```ts -const baseUrl = await window.electron.getGoosedHostPort(); -const acpUrl = baseUrl.replace(/^http/, 'ws') + '/acp'; -``` - -If `goosed` is running HTTPS, this becomes `wss://.../acp`. - -## ACP Auth Decision - -Current `goosed` REST uses `X-Secret-Key`. - -Browser WebSocket does not support arbitrary request headers. If `/acp` is mounted behind the same auth middleware, direct renderer WebSocket may fail. - -Chosen direction: `/acp` should have ACP-compatible token auth. - -During migration: - -```text -REST routes: - X-Secret-Key header - -ACP route: - ws(s)://127.0.0.1:/acp?token= -``` - -After REST is removed: - -```text -ACP only: - ws(s)://127.0.0.1:/acp?token= -``` - -This preserves a security boundary for the long-term desktop API while still allowing direct renderer WebSocket connections. - -Guardrails: - -- Use a random ACP token for the `goosed` process. -- Prefer an ACP-specific token over reusing the raw REST `X-Secret-Key`. -- Never log full `/acp?token=...` URLs. -- Keep accepting `X-Secret-Key` only for REST during migration. -- Accept ACP token auth only for the local desktop backend. -- Keep REST and ACP as separate route branches so auth policy and endpoint removal stay clear. - -Alternatives considered: - -1. Mount `/acp` outside `X-Secret-Key` auth, relying on localhost binding. -2. Allow auth through a query parameter for `/acp`, for example `/acp?token=...`. -3. Open the WebSocket from Electron main, where headers are easier, and bridge to renderer via IPC. -4. Use HTTP/SSE transport where headers are possible, though this is less ergonomic for ACP permission/request-response flow. - -Option 2 is the preferred approach. Option 1 matches current `ui/goose2` behavior, but it is weaker as the final desktop backend shape. - -## Migration Routing - -Keep REST and ACP side-by-side only for feature areas that have not moved yet. - -Example shape: - -```ts -const backend = { - sessions: flags.acpSessions ? acpSessions : restSessions, - chat: flags.acpChat ? acpChat : restChat, - providers: flags.acpProviders ? acpProviders : restProviders, -}; -``` - -Rules: - -- `goosed` remains default for unmigrated surfaces. -- ACP is opt-in per feature area. -- New functionality should prefer ACP unless blocked. -- Once a feature area is migrated to `/acp`, remove the corresponding REST endpoint from `goosed` rather than keeping a permanent fallback. -- Any missing behavior discovered during migration should be added to ACP before removing the REST endpoint. -- Final milestone is no runtime dependency on generated REST APIs. - -## Migration Order - -### 1. Mount `/acp` in `goosed` - -Add ACP Axum router to the `goosed agent` app. - -Likely places: - -- `crates/goose-server/src/commands/agent.rs` -- `crates/goose-server/src/routes/mod.rs` - -Use: - -```rust -goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig} -goose::acp::transport::create_router -``` - -Need to decide: - -- route merge order -- whether ACP MCP app proxy should be included once or deduplicated against existing goosed MCP app proxy routes - -Auth model: use token-authenticated `/acp` for direct renderer WebSocket, separate from REST `X-Secret-Key`. - -### 2. Add desktop ACP client - -Add something like: - -```text -ui/desktop/src/acp/createWebSocketStream.ts -ui/desktop/src/acp/acpConnection.ts -ui/desktop/src/acp/acpApi.ts -``` - -This can be based on `ui/goose2`. - -### 3. Migrate session list/create/load - -Map: - -```text -REST /agent/start -> ACP session/new -REST /agent/resume -> ACP session/load -REST /sessions -> ACP session/list -REST /sessions/{id} -> ACP session/load plus replay/session metadata -``` - -This proves: - -- session IDs -- history replay -- session metadata -- current model/provider/mode state - -### 4. Migrate chat streaming - -Map: - -```text -REST /sessions/{id}/reply -REST /sessions/{id}/events - -> ACP session/prompt + session/update notifications -``` - -Prefer moving the chat state toward ACP-native events and data structures rather than preserving the old goosed `MessageEvent` model. A temporary adapter into existing desktop `Message` and `TokenState` shapes is acceptable only as a short bridge if it substantially lowers rollout risk. - -### 5. Migrate tool approval and tool display - -Map: - -```text -REST /action-required/tool-confirmation - -> ACP RequestPermissionRequest response -``` - -Tool display should move toward ACP-native `tool_call` / `tool_call_update` state. Any old desktop message-shape adapter should be treated as temporary migration glue. - -### 6. Migrate provider/model/mode - -Use ACP session config options: - -```text -setSessionConfigOption({ configId: "provider" }) -setSessionConfigOption({ configId: "model" }) -setSessionConfigOption({ configId: "mode" }) -``` - -Use existing Goose custom methods for provider inventory and setup: - -```text -_goose/providers/list -_goose/providers/config/read -_goose/providers/config/save -_goose/providers/config/status -_goose/providers/custom/* -_goose/providers/catalog/* -``` - -### 7. Migrate extensions/tools/resources - -Existing custom ACP methods cover: - -```text -_goose/extensions/add -_goose/extensions/remove -_goose/config/extensions -_goose/config/extensions/add -_goose/config/extensions/remove -_goose/config/extensions/toggle -_goose/session/extensions -_goose/tools -_goose/tool/call -_goose/resource/read -_goose/working_dir/update -``` - -### 8. Migrate settings and secondary surfaces - -Existing ACP custom methods cover many settings/product surfaces: - -```text -_goose/preferences/* -_goose/defaults/* -_goose/onboarding/import/* -_goose/sources/* -_goose/dictation/* -``` - -## First Spike Plan: Configured Extensions List - -Before the ACP session-list PR lands, use configured extensions as the first migration slice. This avoids session ID semantics and chat streaming complexity while still proving the core `/acp` path. - -### Scope - -Migrate the read-only configured extensions list behind a feature flag. - -```text -Current REST: - GET /config/extensions - -Target ACP: - _goose/config/extensions -``` - -Do not migrate add/update/delete in this first slice. Those can remain REST until the read path and ACP client plumbing are proven. - -### Why This Is A Good First Slice - -- No session ID dependency. -- No chat streaming or prompt lifecycle. -- No tool approval. -- No message-shape conversion. -- Exercises direct renderer WebSocket to `/acp`. -- Exercises ACP initialization and custom `_goose/*` request dispatch. -- Easy to compare REST and ACP responses. - -### Current Gap Found - -REST `GET /config/extensions` filters hidden extensions: - -```rust -goose::config::get_all_extensions() - .into_iter() - .filter(|ext| !goose::agents::extension_manager::is_hidden_extension(&ext.config.name())) -``` - -ACP `_goose/config/extensions` currently calls `crate::config::extensions::get_all_extensions()` and does not apply the same hidden-extension filter. - -For this spike, either: - -1. Fix ACP to match REST before enabling the feature flag, or -2. Allow the mismatch only in local spike mode and track it as the first ACP gap. - -Recommendation: fix ACP to match REST. The goal is to prove migration, not introduce UI-visible behavior differences. - -### Backend Steps - -1. Mount `/acp` in `goosed agent`. - - Add or merge the ACP Axum router into `crates/goose-server`. - - Keep REST and ACP as separate route branches. - - Avoid collisions with existing `/status`, `/mcp-app-proxy`, and `/mcp-app-guest` routes. - -2. Add ACP token auth for `/acp`. - - Direct renderer WebSocket should connect with: - ```text - ws(s)://127.0.0.1:/acp?token= - ``` - - Do not put `/acp` behind REST `X-Secret-Key` middleware. - - Do not log full token-bearing URLs. - -3. Fix `_goose/config/extensions` parity. - - Apply the same hidden-extension filtering as REST. - - Preserve `warnings`. - - Preserve enough shape for existing desktop extension rendering. - -### Desktop Client Steps - -1. Add ACP client files under `ui/desktop/src/acp/`. - Suggested files: - ```text - ui/desktop/src/acp/createWebSocketStream.ts - ui/desktop/src/acp/acpConnection.ts - ui/desktop/src/acp/acpApi.ts - ``` - -2. Base these on `ui/goose2`, but copy/adapt rather than share. - - Replace Tauri URL lookup with Electron lookup. - - Build ACP URL from the existing goosed host. - - Append the ACP token. - - Initialize ACP once and reconnect on close. - -3. Add an ACP extensions API wrapper: - ```ts - getConfigExtensionsViaAcp(): Promise - ``` - It should call: - ```text - _goose/config/extensions - ``` - and normalize the response to the existing desktop `ExtensionResponse` shape. - -4. Add a feature flag. - Suggested name: - ```text - acpConfigExtensions - ``` - -5. Route only the read path through ACP when the flag is enabled. - Primary integration point: - - `ui/desktop/src/components/ConfigContext.tsx` - - Keep mutation paths on REST for this first spike: - - `addExtension` - - `removeExtension` - - `toggleExtension` - - bundled extension sync/prune - -### Validation - -Compare REST and ACP for the same local config: - -- same visible extension count -- same names -- same `enabled` values -- same extension types (`builtin`, `stdio`, `streamable_http`, etc.) -- same warnings -- hidden extensions do not appear -- settings/extensions UI renders correctly -- add/update/delete still work through REST and refresh the list -- failed ACP connection falls back to REST while the spike flag is enabled - -### Removal Rule - -Do not remove `GET /config/extensions` after this first read-only spike if writes still use REST sync logic that depends on the endpoint. - -Remove the REST extension endpoints only after the full extension config surface has migrated: - -```text -GET /config/extensions -POST /config/extensions -DELETE /config/extensions/{name} -``` - -Corresponding ACP methods: - -```text -_goose/config/extensions -_goose/config/extensions/add -_goose/config/extensions/remove -_goose/config/extensions/toggle -``` - -## Known Gaps To Investigate - -Likely REST-only or partially covered areas: - -- recipe encode/decode/scan/schedule/create-from-session -- schedules -- local inference model management/downloads -- tunnel/gateway -- diagnostics/system info -- telemetry -- session sharing -- app export/import/list app flows -- MCP UI proxy details -- current `ActiveRequests` reattach semantics - -During the transition these can remain on REST fallback. The final target is to expose each required capability through Goose custom ACP methods under `_goose/...` unless it maps cleanly to standard ACP. - -## Recommended End State - -Short term: - -```text -goosed exposes REST + temporary /acp -desktop uses REST by default, ACP by feature flag -``` - -Migration: - -```text -desktop moves one feature area at a time to ACP -missing backend behavior is added as standard ACP use or _goose custom methods -matching goosed REST endpoints are removed as each feature migrates -``` - -End state: - -```text -desktop talks to /acp directly -goose serve is the single bundled desktop backend -goosed is no longer bundled or spawned -REST/OpenAPI is removed from desktop runtime behavior -``` - -## Open Decisions - -No major architecture decisions remain from this spike. Implementation details still need validation around route merge order, MCP app proxy deduplication, and exact token plumbing. diff --git a/ui/goose2/.claude/skills/code-review b/ui/goose2/.claude/skills/code-review deleted file mode 120000 index 2d88efdf94..0000000000 --- a/ui/goose2/.claude/skills/code-review +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/code-review \ No newline at end of file diff --git a/ui/goose2/.claude/skills/create-pr b/ui/goose2/.claude/skills/create-pr deleted file mode 120000 index d5b42b7251..0000000000 --- a/ui/goose2/.claude/skills/create-pr +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/create-pr \ No newline at end of file diff --git a/ui/goose2/.claude/skills/edge-case-finder b/ui/goose2/.claude/skills/edge-case-finder deleted file mode 120000 index 93db35644c..0000000000 --- a/ui/goose2/.claude/skills/edge-case-finder +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/edge-case-finder \ No newline at end of file diff --git a/ui/goose2/.codex/skills/code-review b/ui/goose2/.codex/skills/code-review deleted file mode 120000 index 2d88efdf94..0000000000 --- a/ui/goose2/.codex/skills/code-review +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/code-review \ No newline at end of file diff --git a/ui/goose2/.codex/skills/create-pr b/ui/goose2/.codex/skills/create-pr deleted file mode 120000 index d5b42b7251..0000000000 --- a/ui/goose2/.codex/skills/create-pr +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/create-pr \ No newline at end of file diff --git a/ui/goose2/.codex/skills/edge-case-finder b/ui/goose2/.codex/skills/edge-case-finder deleted file mode 120000 index 93db35644c..0000000000 --- a/ui/goose2/.codex/skills/edge-case-finder +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/edge-case-finder \ No newline at end of file diff --git a/ui/goose2/.gitignore b/ui/goose2/.gitignore deleted file mode 100644 index 51ef710678..0000000000 --- a/ui/goose2/.gitignore +++ /dev/null @@ -1,48 +0,0 @@ -# Dependencies -node_modules/ - -# Build output -dist/ - -# Rust/Tauri build artifacts -/target/ -src-tauri/target/ - -# Environment files -.env -.env.* -!.env.example - -# Editor / IDE -.idea/ -.vscode/ -*.swp -*.swo -*~ -.*.sw? - -# OS artifacts -.DS_Store -Thumbs.db - -# Scratch / working files -.scratch/ - -# Playwright artifacts -playwright-report/ -test-results/ - -# App E2E screenshots -tests/app-e2e/screenshots/ - -# Testing coverage -coverage/ - -# Logs -*.log - -# Hermit (toolchain manager cache) -.hermit/ - -# Claude -.claude diff --git a/ui/goose2/AGENTS.md b/ui/goose2/AGENTS.md deleted file mode 100644 index b26f3e0473..0000000000 --- a/ui/goose2/AGENTS.md +++ /dev/null @@ -1,275 +0,0 @@ -# AGENTS.md - -Guidelines for AI agents (and developers) working on this codebase. - -## Project Overview - -Goose2 is a Tauri 2 + React 19 desktop app. It uses TypeScript strict mode, Vite, and Tailwind CSS 4. The codebase follows a feature-sliced architecture organized under `src/app/`, `src/features/`, and `src/shared/`. - -## First Steps - -Treat this repo as partially Hermit-managed. Do not assume `just`, `pnpm`, `node`, or `lefthook` are available globally. - -- In bash/zsh, run `source ./bin/activate-hermit` before using repo tools if the shell cannot find `just`, `pnpm`, or other managed binaries. -- In fish, run `source ./bin/activate-hermit.fish`. -- If PATH still looks wrong or you want to avoid shell assumptions, prefer repo-local binaries such as `./bin/just`, `./bin/pnpm`, and `./bin/lefthook`. -- Biome is installed from `package.json` devDependencies, not from Hermit. Run it through `pnpm`, `pnpm exec biome`, or `npx biome` after `just setup`. -- On a fresh clone, a newly created worktree, or after `just clean`, run `just setup` before relying on `pnpm`, Biome, or app-local tooling. -- In new clones and worktrees, ensure git hooks are installed early with `lefthook install`. If `lefthook` is not on PATH, use `./bin/lefthook install`. -- Agents starting in a fresh clone or worktree should do the setup and hook-install steps proactively rather than assuming the environment is already bootstrapped. -- Use `just dev` for the normal desktop workflow. Use `just dev-frontend` only when you intentionally want the Vite app without Tauri. - -## Common Commands - -- `just setup` installs frontend dependencies with `pnpm install` and builds the Rust backend once. -- `just dev` starts the desktop app in dev mode and wires Tauri to the local Vite server. -- `just check` runs Biome checks and file-size checks. -- `just test` runs the Vitest suite. -- `just tauri-check` runs `cargo check` in `src-tauri`. -- `just ci` is the main local verification gate. -- `just clean` removes Rust build artifacts, `dist`, and `node_modules`, so `just setup` is required again before `just dev`. - -## Architecture - -### The frontend → ACP → goose core path - -**All frontend ↔ backend communication in goose2 flows through a single path:** - -``` -React UI ──► features//api/ ──► @aaif/goose-sdk (TS) ──► goose-acp (WebSocket, ACP) ──► goose (core) -``` - -**YOU MUST TREAT THE CLIENT as a THIN CLIENT**** - -- The Tauri shell spawns a long-lived `goose serve` process and exposes its WebSocket URL via the `get_goose_serve_url` Tauri command. That is essentially the only Tauri command the frontend needs for backend work — it is how the renderer discovers the ACP endpoint. -- The frontend opens a WebSocket to `goose serve` and talks to it using `@aaif/goose-sdk` (published from `ui/sdk/`). The SDK is generated from the ACP custom-method definitions in `crates/goose-sdk/src/custom_requests.rs`, so every backend method has a typed TypeScript client method. -- The goose2 TypeScript code should only be UI -- `goose-acp` (`crates/goose-acp/src/server.rs`) is the server side of the WebSocket. It implements handlers for the custom ACP methods and calls into the `goose` core crate to do the actual work (providers, config, sessions, dictation, etc.). -- `goose` is the pure domain crate. It knows nothing about Tauri or WebSockets — it just exposes Rust APIs that `goose-acp` handlers invoke. - -**This is the pattern you must follow when adding any new backend-touching feature.** When you are vibecoding in this app, it is very tempting to reach for `invoke()` or add an HTTP fetch — don't. The rule is: if a feature needs to talk to `goose` core, it goes through the SDK → ACP → goose chain above. - -### Don't build features entirely in the frontend - -If a feature involves data, persistence, secrets, provider config, sessions, filesystem access, network calls to external services, or anything else that could plausibly be reused by the CLI or another goose surface, **the logic belongs in the `goose` core crate, exposed via a typed ACP method.** Do not: - -- Stand up a feature whose business logic lives in a Zustand store, a React hook, or a `features//api/` adapter that calls `localStorage`, `fetch`, or filesystem APIs directly. -- Reach for `invoke()` to add a new Tauri command that proxies into `goose` — add an ACP custom method instead. -- Do not use `localStorage` except for things it's absolutely needed for (specifically outlined in other parts of this AGENTS.md) -- Duplicate types between TS and Rust — let the SDK generation produce them from `crates/goose-sdk/src/custom_requests.rs` and use the generated types ALWAYS. Do not create a shadow of a type you could use from the generated types. - -The frontend's job is presentation, navigation, and orchestration of typed SDK calls. If you find a feature growing real logic on the React side, that's a signal to push it down into `goose-acp` + `goose`. - -### File structure - -The directory layout is organized to reinforce the path above. Every feature that touches the backend has an `api/` module that wraps `GooseClient` calls — UI and stores never touch the SDK directly. - -``` -src/ - app/ — App shell, entry point, top-level providers - features/ — Feature modules (see Feature Organization below) - / - ui/ — React components (required) - hooks/ — Custom React hooks for feature logic (when needed) - stores/ — Zustand state management (frontend-only UI state; not a substitute for goose core) - api/ — Thin wrappers around GooseClient SDK calls (the ONLY place ACP is touched) - types.ts — Feature-specific type definitions (when needed) - shared/ - ui/ — Reusable UI components (button, etc.) - lib/ — Utilities (cn.ts for class merging) - theme/ — Theme provider, appearance settings - styles/ — Global CSS, design tokens - hooks/ — Shared hooks - api/ — Shared GooseClient wrappers used by multiple features (e.g. dictation) - constants/ — Shared constants - context/ — Shared contexts -``` - -### Feature Organization - -Not every feature needs every subdirectory. Use only what the feature requires. Note that anything beyond `ui/` only — i.e. anything with state or backend calls — should already have a corresponding ACP method on the goose side. - -| Pattern | Structure | Examples | Backend shape | -|----------------------|----------------------------------|---------------------------------|------------------------------------------------| -| **Full-featured** | `stores/` + `hooks/` + `ui/` + `api/` | agents, chat | Typed ACP methods drive the store | -| **Data-driven** | `stores/` + `api/` + `ui/` | projects | CRUD via ACP, store caches results | -| **API features** | `api/` + `ui/` | skills, providers | Pure pass-through to ACP | -| **Simple features** | `ui/` only | home, settings, sidebar, status | No backend (pure presentation) | -| **Tabs** | `ui/` + `types.ts` | tabs | No backend (frontend-only UI state) | - -If you're tempted to build a "Full-featured" or "Data-driven" feature without a corresponding `api/` module that calls a typed ACP method, stop and add the ACP method first. See "The canonical example" below. - -### Import Rules for Features - -- Shared types live in `src/shared/types/` — this is the single source of truth for cross-feature types. -- There should be NO root-level `src/stores/` or `src/types/` directories. -- Feature stores use feature-relative imports (e.g., `../stores/featureStore`). -- Cross-feature imports use `@/features/*/stores/` or `@/shared/types/`. -- Only `features//api/` and `shared/api/` modules may import from `@aaif/goose-sdk` or call `getClient()`. UI components, hooks, and stores must go through those wrappers. - -## Coding Conventions - -- Use `cn()` from `@/shared/lib/cn` for Tailwind class merging. -- Import paths use the `@/` alias (maps to `./src`). -- Components are controlled where possible (state lifted to parent). -- Use `@tabler/icons-react` for icons (transitioning from `lucide-react`; existing `lucide-react` usage is fine until migrated). -- All ` - {promptExpanded && ( -