From 01e9e8350e434c6916cd2f4119575a4d9d82ac56 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 11 Apr 2026 19:55:25 +0300 Subject: [PATCH] feat(frontend): improve runtime connection and team setup ux --- .github/workflows/ci.yml | 18 +- .github/workflows/landing.yml | 6 +- .github/workflows/release.yml | 253 +++- docs/RELEASE.md | 7 +- .../get-team-data-parallel-read-plan.md | 28 +- docs/research/lead-session-cache-plan.md | 22 +- package.json | 5 + resources/pricing.json | 174 ++- resources/runtime/.gitkeep | 1 + runtime.lock.json | 28 + scripts/dev-with-runtime.mjs | 9 +- scripts/electron-builder/afterPack.cjs | 392 +++++ scripts/electron-builder/verifyBundle.cjs | 35 + scripts/runtime-lock.mjs | 61 + src/main/ipc/configValidation.ts | 96 +- .../extensions/apikeys/ApiKeyService.ts | 23 +- .../extensions/skills/SkillMetadataParser.ts | 2 + .../infrastructure/CliInstallerService.ts | 109 +- .../services/infrastructure/ConfigManager.ts | 50 +- .../services/infrastructure/UpdaterService.ts | 68 +- .../infrastructure/updaterReleaseMetadata.ts | 79 + .../runtime/ClaudeMultimodelBridgeService.ts | 55 +- .../runtime/ProviderConnectionService.ts | 250 ++++ .../schedule/ScheduledTaskExecutor.ts | 20 +- .../services/team/ClaudeBinaryResolver.ts | 76 +- src/main/services/team/ClaudeDoctorProbe.ts | 183 +++ .../services/team/TeamProvisioningService.ts | 125 +- src/main/services/team/cliFlavor.ts | 14 +- src/main/utils/cliPathMerge.ts | 12 +- src/renderer/api/httpClient.ts | 1 + .../common/CliInstallWarningBanner.tsx | 6 +- .../components/dashboard/CliStatusBanner.tsx | 226 ++- .../extensions/ExtensionStoreView.tsx | 25 +- .../extensions/apikeys/ApiKeyCard.tsx | 6 +- .../runtime/ProviderRuntimeSettingsDialog.tsx | 1321 ++++++++++++++--- .../runtime/providerConnectionUi.ts | 255 ++++ .../settings/hooks/useSettingsHandlers.ts | 44 +- .../settings/sections/CliStatusSection.tsx | 246 ++- .../team/dialogs/TeamModelSelector.tsx | 434 +++--- .../components/team/members/LeadModelRow.tsx | 14 +- .../team/members/MemberDraftRow.tsx | 12 +- .../store/slices/cliInstallerSlice.ts | 42 +- src/renderer/store/slices/configSlice.ts | 2 + src/renderer/store/slices/extensionsSlice.ts | 7 +- src/shared/types/cliInstaller.ts | 22 +- src/shared/types/notifications.ts | 12 +- src/shared/types/team.ts | 2 +- .../build/electronBuilderAfterPack.test.ts | 227 +++ test/main/ipc/configValidation.test.ts | 32 + .../extensions/SkillMetadataParser.test.ts | 7 + .../CliInstallerService.healthCheck.test.ts | 113 ++ .../updaterReleaseMetadata.test.ts | 72 + .../ClaudeMultimodelBridgeService.test.ts | 62 +- .../runtime/ProviderConnectionService.test.ts | 280 ++++ .../team/ClaudeBinaryResolver.test.ts | 131 +- .../services/team/ClaudeDoctorProbe.test.ts | 74 + .../TeamProvisioningServicePrompts.test.ts | 54 +- test/main/services/team/cliFlavor.test.ts | 2 +- test/main/utils/cliPathMerge.test.ts | 10 +- .../cli/CliStatusVisibility.test.ts | 125 +- .../ProviderRuntimeSettingsDialog.test.ts | 896 +++++++++++ .../runtime/providerConnectionUi.test.ts | 185 +++ .../components/team/TeamModelSelector.test.ts | 4 +- .../TeamModelSelectorDisabledState.test.ts | 106 +- test/renderer/store/cliInstallerSlice.test.ts | 42 + test/renderer/store/configSlice.test.ts | 45 + test/shared/utils/reviewState.test.ts | 25 + 67 files changed, 6395 insertions(+), 975 deletions(-) create mode 100644 resources/runtime/.gitkeep create mode 100644 runtime.lock.json create mode 100644 scripts/electron-builder/afterPack.cjs create mode 100644 scripts/electron-builder/verifyBundle.cjs create mode 100644 scripts/runtime-lock.mjs create mode 100644 src/main/services/infrastructure/updaterReleaseMetadata.ts create mode 100644 src/main/services/runtime/ProviderConnectionService.ts create mode 100644 src/main/services/team/ClaudeDoctorProbe.ts create mode 100644 src/renderer/components/runtime/providerConnectionUi.ts create mode 100644 test/main/build/electronBuilderAfterPack.test.ts create mode 100644 test/main/services/infrastructure/CliInstallerService.healthCheck.test.ts create mode 100644 test/main/services/infrastructure/updaterReleaseMetadata.test.ts create mode 100644 test/main/services/runtime/ProviderConnectionService.test.ts create mode 100644 test/main/services/team/ClaudeDoctorProbe.test.ts create mode 100644 test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts create mode 100644 test/renderer/components/runtime/providerConnectionUi.test.ts create mode 100644 test/renderer/store/configSlice.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae068fd6..c599260e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,12 @@ on: branches: [main, dev] paths: - 'src/**' + - 'scripts/**' - 'agent-teams-controller/**' - 'mcp-server/**' - 'packages/**' + - 'resources/runtime/**' + - 'runtime.lock.json' - 'test/**' - '.github/workflows/**' - 'pnpm-workspace.yaml' @@ -21,9 +24,12 @@ on: pull_request: paths: - 'src/**' + - 'scripts/**' - 'agent-teams-controller/**' - 'mcp-server/**' - 'packages/**' + - 'resources/runtime/**' + - 'runtime.lock.json' - 'test/**' - '.github/workflows/**' - 'pnpm-workspace.yaml' @@ -41,15 +47,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Install dependencies @@ -76,15 +82,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Install dependencies diff --git a/.github/workflows/landing.yml b/.github/workflows/landing.yml index 05949955..c31e3bbe 100644 --- a/.github/workflows/landing.yml +++ b/.github/workflows/landing.yml @@ -19,11 +19,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 - name: Install dependencies working-directory: landing diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f370a6f..b6d1c365 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,15 +15,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Install dependencies @@ -57,7 +57,7 @@ jobs: --draft=false 2>/dev/null || echo "Release $TAG already exists, skipping creation" - name: Upload dist artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist path: | @@ -65,8 +65,94 @@ jobs: dist-electron retention-days: 1 + prepare-runtime: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${GITHUB_REF#refs/tags/}" + gh release create "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$TAG" \ + --generate-notes \ + --draft=false 2>/dev/null || echo "Release $TAG already exists, skipping creation" + + - name: Check runtime assets + id: runtime-assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)" + missing=0 + while IFS= read -r asset; do + [ -n "$asset" ] || continue + if ! grep -Fqx "$asset" <<<"$existing"; then + echo "Missing runtime asset: $asset" + missing=1 + fi + done < <(node ./scripts/runtime-lock.mjs asset-list) + echo "missing=$missing" >> "$GITHUB_OUTPUT" + + - name: Dispatch private runtime build + if: steps.runtime-assets.outputs.missing == '1' + env: + GH_TOKEN: ${{ secrets.RUNTIME_BUILD_DISPATCH_TOKEN }} + run: | + set -euo pipefail + TARGET_TAG="${GITHUB_REF#refs/tags/}" + SOURCE_REPO="$(node ./scripts/runtime-lock.mjs source-repository)" + SOURCE_REF="$(node ./scripts/runtime-lock.mjs source-ref)" + RUNTIME_VERSION="$(node ./scripts/runtime-lock.mjs version)" + gh api \ + --method POST \ + "repos/${SOURCE_REPO}/actions/workflows/release-runtime.yml/dispatches" \ + -f ref=main \ + -f inputs[source_ref]="$SOURCE_REF" \ + -f inputs[runtime_version]="$RUNTIME_VERSION" \ + -f inputs[target_release_repo]="$GITHUB_REPOSITORY" \ + -f inputs[target_release_tag]="$TARGET_TAG" + + - name: Wait for runtime assets + if: steps.runtime-assets.outputs.missing == '1' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + for attempt in $(seq 1 60); do + existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)" + all_found=1 + while IFS= read -r asset; do + [ -n "$asset" ] || continue + if ! grep -Fqx "$asset" <<<"$existing"; then + all_found=0 + break + fi + done < <(node ./scripts/runtime-lock.mjs asset-list) + + if [ "$all_found" -eq 1 ]; then + echo "Runtime assets are ready" + exit 0 + fi + + echo "Waiting for runtime assets - attempt $attempt/60" + sleep 10 + done + + echo "Timed out waiting for runtime assets in release $TAG" >&2 + exit 1 + release-mac: - needs: build + needs: [build, prepare-runtime] strategy: fail-fast: false matrix: @@ -81,10 +167,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist @@ -92,9 +178,9 @@ jobs: uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Setup Python for node-gyp @@ -111,6 +197,33 @@ jobs: VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" + - name: Resolve runtime asset name (macOS ${{ matrix.arch }}) + if: startsWith(github.ref, 'refs/tags/v') + id: runtime-asset + shell: bash + run: | + set -euo pipefail + if [[ "${{ matrix.arch }}" == "arm64" ]]; then + platform="darwin-arm64" + else + platform="darwin-x64" + fi + echo "asset_name=$(node ./scripts/runtime-lock.mjs asset-name "$platform")" >> "$GITHUB_OUTPUT" + + - name: Stage bundled runtime (macOS ${{ matrix.arch }}) + if: startsWith(github.ref, 'refs/tags/v') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + rm -rf .runtime-download resources/runtime + mkdir -p .runtime-download resources/runtime + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "${{ steps.runtime-asset.outputs.asset_name }}" --dir .runtime-download + tar -xzf ".runtime-download/${{ steps.runtime-asset.outputs.asset_name }}" -C .runtime-download + cp -R .runtime-download/runtime/. resources/runtime/ + - name: Build app (macOS ${{ matrix.arch }}) env: NODE_OPTIONS: '--max-old-space-size=8192' @@ -126,6 +239,9 @@ jobs: test -f dist-electron/preload/index.js test -f out/renderer/index.html test -f mcp-server/dist/index.js + if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then + test -f resources/runtime/VERSION + fi - name: Package (macOS ${{ matrix.arch }}) env: @@ -137,6 +253,9 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: ${{ matrix.dist_command }} --publish never + - name: Validate packaged bundle (macOS ${{ matrix.arch }}) + run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Claude Agent Teams UI.app" darwin ${{ matrix.arch }} + - name: Upload assets to release if: startsWith(github.ref, 'refs/tags/v') env: @@ -152,15 +271,15 @@ jobs: done release-win: - needs: build + needs: [build, prepare-runtime] runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist @@ -168,9 +287,9 @@ jobs: uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Setup Python for node-gyp @@ -188,6 +307,29 @@ jobs: VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" + - name: Resolve runtime asset name (Windows) + if: startsWith(github.ref, 'refs/tags/v') + id: runtime-asset + shell: bash + run: | + echo "asset_name=$(node ./scripts/runtime-lock.mjs asset-name win32-x64)" >> "$GITHUB_OUTPUT" + + - name: Stage bundled runtime (Windows) + if: startsWith(github.ref, 'refs/tags/v') + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $ErrorActionPreference = "Stop" + $tag = $env:GITHUB_REF.Replace('refs/tags/', '') + Remove-Item .runtime-download -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item resources/runtime/* -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path .runtime-download | Out-Null + New-Item -ItemType Directory -Force -Path resources/runtime | Out-Null + gh release download $tag --repo $env:GITHUB_REPOSITORY --pattern "${{ steps.runtime-asset.outputs.asset_name }}" --dir .runtime-download + Expand-Archive -Path ".runtime-download/${{ steps.runtime-asset.outputs.asset_name }}" -DestinationPath .runtime-download/unpacked -Force + Copy-Item .runtime-download/unpacked/runtime/* resources/runtime -Recurse -Force + - name: Build app (Windows) env: NODE_OPTIONS: '--max-old-space-size=8192' @@ -204,12 +346,19 @@ jobs: test -f dist-electron/preload/index.js test -f out/renderer/index.html test -f mcp-server/dist/index.js + if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then + test -f resources/runtime/VERSION + fi - name: Package (Windows) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm dist:win --publish never + - name: Validate packaged bundle (Windows) + shell: bash + run: node ./scripts/electron-builder/verifyBundle.cjs "release/win-unpacked" win32 x64 + - name: Upload assets to release if: startsWith(github.ref, 'refs/tags/v') shell: bash @@ -226,15 +375,15 @@ jobs: done release-linux: - needs: build + needs: [build, prepare-runtime] runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist @@ -242,9 +391,9 @@ jobs: uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Setup Python for node-gyp @@ -266,6 +415,27 @@ jobs: VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" + - name: Resolve runtime asset name (Linux) + if: startsWith(github.ref, 'refs/tags/v') + id: runtime-asset + shell: bash + run: | + echo "asset_name=$(node ./scripts/runtime-lock.mjs asset-name linux-x64)" >> "$GITHUB_OUTPUT" + + - name: Stage bundled runtime (Linux) + if: startsWith(github.ref, 'refs/tags/v') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + rm -rf .runtime-download resources/runtime + mkdir -p .runtime-download resources/runtime + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "${{ steps.runtime-asset.outputs.asset_name }}" --dir .runtime-download + tar -xzf ".runtime-download/${{ steps.runtime-asset.outputs.asset_name }}" -C .runtime-download + cp -R .runtime-download/runtime/. resources/runtime/ + - name: Build app (Linux) env: NODE_OPTIONS: '--max-old-space-size=8192' @@ -281,12 +451,18 @@ jobs: test -f dist-electron/preload/index.js test -f out/renderer/index.html test -f mcp-server/dist/index.js + if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then + test -f resources/runtime/VERSION + fi - name: Package (Linux) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm dist:linux --publish never + - name: Validate packaged bundle (Linux) + run: node ./scripts/electron-builder/verifyBundle.cjs "release/linux-unpacked" linux x64 + - name: Upload assets to release if: startsWith(github.ref, 'refs/tags/v') env: @@ -313,9 +489,12 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + set -euo pipefail VERSION="${GITHUB_REF#refs/tags/v}" REPO="${GITHUB_REPOSITORY}" DOWNLOAD_BASE="https://github.com/${REPO}/releases/download/v${VERSION}" + TMP_DIR="$(mktemp -d)" + trap 'rm -rf "$TMP_DIR"' EXIT declare -A FILES=( ["Claude-Agent-Teams-UI-arm64.dmg"]="Claude.Agent.Teams.UI-${VERSION}-arm64.dmg" @@ -327,18 +506,13 @@ jobs: ["Claude-Agent-Teams-UI.pacman"]="claude-agent-teams-ui-${VERSION}.pacman" ) - # Remove old stable assets (ignore errors if they don't exist) - for STABLE_NAME in "${!FILES[@]}"; do - gh release delete-asset "v${VERSION}" "$STABLE_NAME" --repo "$REPO" --yes 2>/dev/null || true - done - # Download versioned files and re-upload with stable names for STABLE_NAME in "${!FILES[@]}"; do VERSIONED_NAME="${FILES[$STABLE_NAME]}" echo "Downloading ${VERSIONED_NAME} -> ${STABLE_NAME}" - curl -fSL -o "$STABLE_NAME" "${DOWNLOAD_BASE}/${VERSIONED_NAME}" && \ - gh release upload "v${VERSION}" "$STABLE_NAME" --repo "$REPO" --clobber - rm -f "$STABLE_NAME" + curl -fSL -o "${TMP_DIR}/${VERSIONED_NAME}" "${DOWNLOAD_BASE}/${VERSIONED_NAME}" + cp "${TMP_DIR}/${VERSIONED_NAME}" "${TMP_DIR}/${STABLE_NAME}" + gh release upload "v${VERSION}" "${TMP_DIR}/${STABLE_NAME}" --repo "$REPO" --clobber done - name: Publish canonical updater metadata @@ -397,26 +571,25 @@ jobs: EOF # Canonical macOS feed. - # electron-updater consumes only one latest-mac.yml asset on GitHub releases. - # We publish the x64 feed here because it works on Intel Macs and remains - # installable on Apple Silicon via Rosetta until we add arch-specific feed - # selection or universal packaging. - download_asset "Claude.Agent.Teams.UI-${VERSION}-mac.zip" - download_asset "Claude.Agent.Teams.UI-${VERSION}.dmg" - MAC_ZIP_SHA="$(sha512_base64 Claude.Agent.Teams.UI-${VERSION}-mac.zip)" - MAC_ZIP_SIZE="$(file_size Claude.Agent.Teams.UI-${VERSION}-mac.zip)" - MAC_DMG_SHA="$(sha512_base64 Claude.Agent.Teams.UI-${VERSION}.dmg)" - MAC_DMG_SIZE="$(file_size Claude.Agent.Teams.UI-${VERSION}.dmg)" + # electron-updater on GitHub still consumes a single latest-mac.yml, so we + # publish the Apple Silicon feed here and suppress Intel auto-update in-app + # until we switch to universal packaging or an arch-aware provider. + download_asset "Claude.Agent.Teams.UI-${VERSION}-arm64-mac.zip" + download_asset "Claude.Agent.Teams.UI-${VERSION}-arm64.dmg" + MAC_ZIP_SHA="$(sha512_base64 Claude.Agent.Teams.UI-${VERSION}-arm64-mac.zip)" + MAC_ZIP_SIZE="$(file_size Claude.Agent.Teams.UI-${VERSION}-arm64-mac.zip)" + MAC_DMG_SHA="$(sha512_base64 Claude.Agent.Teams.UI-${VERSION}-arm64.dmg)" + MAC_DMG_SIZE="$(file_size Claude.Agent.Teams.UI-${VERSION}-arm64.dmg)" cat > latest-mac.yml <