cleanup: remove current goose2 code (#9368)
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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:-<default>}"
|
||||
echo "Version: ${INPUT_VERSION:-<from Cargo.toml>}"
|
||||
echo "Signing: ${INPUT_SIGNING}"
|
||||
echo "CLI run ID: ${INPUT_CLI_RUN_ID:-<build from source>}"
|
||||
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-<triple>.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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:<port>/acp?token=<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:<port>/acp?token=<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<ExtensionResponse>
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -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:<port>/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:<port>/acp?token=<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:<port>/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:<port>/acp?token=<acp-token>
|
||||
```
|
||||
|
||||
After REST is removed:
|
||||
|
||||
```text
|
||||
ACP only:
|
||||
ws(s)://127.0.0.1:<port>/acp?token=<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:<port>/acp?token=<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<ExtensionResponse>
|
||||
```
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/code-review
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/create-pr
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/edge-case-finder
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/code-review
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/create-pr
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/edge-case-finder
|
||||
@@ -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
|
||||
@@ -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/<feature>/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/<feature>/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)
|
||||
<feature>/
|
||||
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/<feature>/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 `<button>` elements must have `type="button"` to prevent form submission.
|
||||
- Use semantic HTML (`<aside>`, `<nav>`, `<header>`, `<main>`).
|
||||
|
||||
## Localization
|
||||
|
||||
- UI copy should go through `react-i18next`, not hardcoded English strings, for app areas that are already on i18n.
|
||||
- Shared localization lives in `src/shared/i18n/`; use `useTranslation()` for text and the helpers in `src/shared/i18n/format.ts` for dates, times, numbers, currency, and relative time.
|
||||
- Keep translations in feature-scoped JSON namespaces under `src/shared/i18n/locales/<locale>/` instead of one large file, and use stable keys rather than English sentences as keys.
|
||||
- Do not translate user-authored content, agent/model output, or backend-only strings unless they are rendered directly as Goose UI.
|
||||
- `pnpm check` includes `check:i18n`, which flags obvious new raw UI strings in migrated surfaces. Use a narrow `i18n-check-ignore` comment only when the string should stay literal.
|
||||
|
||||
## Theming System
|
||||
|
||||
ThemeProvider manages three axes:
|
||||
|
||||
| Axis | Values | Persistence | Mechanism |
|
||||
|--------------|---------------------------------|-----------------|----------------------------------------------|
|
||||
| Theme mode | `light`, `dark`, `system` | localStorage | `.dark` class on `<html>` |
|
||||
| Accent color | Hex color | localStorage | `--brand` / `--color-brand` CSS variables |
|
||||
| Density | `compact`, `comfortable`, `spacious` | localStorage | `--density-spacing` CSS variable (0.75/1/1.25) |
|
||||
|
||||
- CSS variables are defined in `globals.css` with light/dark variants.
|
||||
- Tailwind config maps CSS variables to semantic color names.
|
||||
- Color palette tokens: `background` (primary/secondary/tertiary), `foreground` (primary/secondary/tertiary), `border`, `ring`, plus semantic variants (`info`, `danger`, `success`, `warning`).
|
||||
|
||||
## Component Patterns
|
||||
|
||||
- Small, focused components — aim for under 200 lines.
|
||||
- Props interfaces live in the component file, or in `types.ts` for shared types.
|
||||
- Use `forwardRef` for components that need ref forwarding (React 19 makes this optional, but the pattern is still used).
|
||||
- Animations: CSS transitions via Tailwind classes; respect `prefers-reduced-motion`.
|
||||
- Entrance animations: use the `isLoaded` state pattern with `useEffect` + short timeout.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- ARIA roles on interactive elements (`role="tab"`, `role="tablist"`, `role="status"`).
|
||||
- `aria-label` on icon-only buttons.
|
||||
- `aria-hidden` on visually hidden content.
|
||||
- `aria-selected` on selectable items.
|
||||
- Color-only indicators must have text alternatives.
|
||||
- `prefers-reduced-motion` is respected globally.
|
||||
|
||||
## Tauri Integration
|
||||
|
||||
- The window starts hidden and is shown via `getCurrentWindow().show()` after React mounts.
|
||||
- Use `data-tauri-drag-region` on header areas for window dragging.
|
||||
- Title bar uses `titleBarStyle: "Overlay"` with `hiddenTitle: true` for a custom titlebar.
|
||||
- `tauri-plugin-window-state` persists window size and position.
|
||||
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
|
||||
- Distro bundle behavior, including feature flags, is documented in `distro/README.md`.
|
||||
|
||||
## Working in the ACP path: examples and rules
|
||||
|
||||
### The canonical example: skills-as-sources (PR #8675)
|
||||
|
||||
The skills → sources migration in [#8675](https://github.com/block/goose/pull/8675) is the clearest illustration of the rule. **It deleted 319 lines of Tauri-command code in `src-tauri/src/commands/skills.rs` and replaced them with ACP custom methods.** If you find yourself wanting to add an `invoke()` command that proxies to `goose`, that PR is what "doing it the other way" looks like. Copy this shape when adding new endpoints:
|
||||
|
||||
1. **Define the request/response in `crates/goose-sdk/src/custom_requests.rs`.** Use the `JsonRpcRequest` / `JsonRpcResponse` derives and the `#[request(method = "_goose/<area>/<action>", response = ...)]` attribute. Sources uses namespaced methods like `_goose/sources/create`, `_goose/sources/list`, `_goose/sources/update`, `_goose/sources/delete`, `_goose/sources/export`, `_goose/sources/import` with paired request/response structs (`CreateSourceRequest` / `CreateSourceResponse`, etc.). Keep the docs on those structs aligned with the implementation: today `_goose/sources/list` is still skill-only; create/import take an explicit target scope (`global`, plus `projectDir` for project sources), while update/delete/export operate on an existing skill by absolute `path`.
|
||||
2. **Implement the handler in `crates/goose-acp/src/server.rs`** with `#[custom_method(YourRequest)]`. Keep it thin: unpack the request, call into the `goose` crate, wrap the result. The sources handlers are ~5 lines each — e.g. `on_list_sources` just calls `goose::sources::list_sources(...)` and returns the typed response. Errors map to `agent_client_protocol::Error::invalid_params()` / `internal_error()`.
|
||||
3. **Put the real logic in the `goose` crate.** Sources lives in `crates/goose/src/sources.rs` — filesystem CRUD, frontmatter parsing, scope resolution, all of it. `goose-acp` knows nothing about where skills are stored on disk; it just forwards typed arguments. This separation is the point.
|
||||
4. **Regenerate the SDK.** The TS methods on `GooseClient` are generated into `ui/sdk/src/generated/`. Do not hand-edit generated files.
|
||||
5. **Call it from the frontend via a feature `api/` module.** See `ui/goose2/src/features/skills/api/skills.ts`. It calls `getClient()` from `acpConnection.ts` and invokes the SDK, then adapts the generic `SourceEntry` shape into a feature-friendly `SkillInfo`:
|
||||
```ts
|
||||
export async function listSkills(): Promise<SkillInfo[]> {
|
||||
const client = await getClient();
|
||||
const raw = await client.extMethod("_goose/sources/list", { type: "skill" });
|
||||
const sources = (raw.sources ?? []) as SourceEntry[];
|
||||
return sources.map(toSkillInfo);
|
||||
}
|
||||
```
|
||||
Feature code (hooks, stores, UI) imports from that `api/` module — it never touches the ACP client directly.
|
||||
|
||||
**Note on typed vs untyped calls.** Skills currently uses `client.extMethod("_goose/sources/...", ...)` (the untyped escape hatch) because it reshapes a generic `Source` API into skill-specific types. The **preferred** shape for new features is the typed generated methods — `client.goose.GooseFooBar({ ... })` — as used by dictation (`client.goose.GooseDictationTranscribe`) and the provider inventory (`client.goose.GooseProvidersList`). Reach for `extMethod()` only when you have a real reason to bypass the generated types.
|
||||
|
||||
For a minimal frontend `api/` wrapper using the typed shape, see `ui/goose2/src/features/providers/api/inventory.ts` — ~30 lines, typed SDK calls, thin adapter. For a fully worked end-to-end feature including OS-keychain handling and progress streaming, see the voice dictation feature ([#8609](https://github.com/block/goose/pull/8609)) and `ui/goose2/src/shared/api/dictation.ts`.
|
||||
|
||||
### Typed ACP config contracts
|
||||
|
||||
Build goose2 config flows around typed ACP methods whose contract matches the domain: provider config, preferences, defaults, dictation secrets, or extension config.
|
||||
|
||||
Keep raw Goose storage keys and secret-handling decisions behind backend-owned ACP methods. This lets Goose validate inputs, apply provider metadata, invalidate caches, refresh dependent state, and keep generated SDK types aligned with supported behavior.
|
||||
|
||||
For reference, define contracts in `crates/goose-sdk/src/custom_requests.rs`, implement them in `crates/goose/src/acp/server/`, regenerate `ui/sdk/src/generated/`, then call the generated `client.goose.*` methods from a feature or shared `api/` wrapper.
|
||||
|
||||
### When `invoke()` is still appropriate
|
||||
|
||||
Tauri commands (`invoke()` from `@tauri-apps/api/core`) are reserved for things that genuinely belong to the desktop shell, not to `goose` core. Provider config or secret mutations that affect the Goose runtime must flow through React → SDK → ACP → goose core so core can validate provider metadata, invalidate secret caches, refresh inventory, and apply provider changes consistently. In practice, Tauri is limited to:
|
||||
|
||||
- `get_goose_serve_url` — bootstrapping the ACP connection.
|
||||
- Native auth subprocesses and desktop-shell side effects.
|
||||
- Window state, filesystem dialogs, and other Tauri-plugin-backed capabilities.
|
||||
- Transitional provider cleanup such as `delete_provider_config` while local OAuth/cache side effects still live in the shell. `get_provider_config`, `check_all_provider_status`, and provider deletion duplicate provider/config knowledge in Tauri today and should move behind provider-scoped ACP methods.
|
||||
|
||||
The long-term provider-config API should be provider-scoped rather than a frontend composition of generic config/secret writes. Prefer methods such as `_goose/providers/config/read`, `_goose/providers/config/status`, `_goose/providers/config/save`, and `_goose/providers/config/delete`; inventory refresh can be part of the result, but active provider reload belongs in the config mutation/apply path, not in `_goose/providers/inventory/refresh`.
|
||||
|
||||
If the thing you're building is "get data from goose" or "tell goose to do something," it is **not** one of these cases. Add a custom ACP method instead.
|
||||
|
||||
### Don't
|
||||
|
||||
- Don't add HTTP `fetch` calls to a `goose` HTTP API, or reintroduce an `apiFetch` utility. There is no HTTP API for goose2 — the backend is the ACP WebSocket.
|
||||
- Don't manage a sidecar `goose` process from the renderer. The Tauri shell owns that lifecycle.
|
||||
- Don't add a new `invoke()` command in `src-tauri/` as a proxy to `goose` core. Add an ACP custom method instead.
|
||||
- Don't hand-edit `ui/sdk/src/generated/`. Regenerate.
|
||||
- Don't call the ACP client (`getClient()`) directly from UI components or stores. Go through a `shared/api/*.ts` (or `features/<feature>/api/*.ts`) module so the SDK surface is mockable in tests.
|
||||
|
||||
## Tooling
|
||||
|
||||
| Tool | Purpose |
|
||||
|-------------|-------------------------------------------------|
|
||||
| **Hermit** | Manages repo binaries such as `node`, `pnpm`, `just`, and `lefthook` |
|
||||
| **Just** | Task runner (`just dev`, `just build`, `just check`) |
|
||||
| **Lefthook**| Git hooks (pre-commit, pre-push) |
|
||||
| **Biome** | Linting and formatting |
|
||||
| **pnpm** | Package manager |
|
||||
|
||||
Additional tooling notes:
|
||||
|
||||
- Prefer repo-managed binaries over global tools when there is any ambiguity about PATH.
|
||||
- Hermit manages `node`, `pnpm`, `just`, and `lefthook`, while Biome comes from `node_modules` after `just setup`.
|
||||
- Tauri backend commands still rely on a working Rust/Cargo toolchain.
|
||||
- Pre-commit hooks run formatting plus `just check`.
|
||||
- Pre-push hooks run `just fmt-check`, `just clippy`, `just check`, `just test`, `just build`, and `just tauri-check`.
|
||||
- Do not use `--no-verify` to bypass hooks. Fix the underlying issue instead.
|
||||
|
||||
## Performance Logging
|
||||
|
||||
- Frontend perf logs use `perfLog()` from `@/shared/lib/perfLog`. Messages are tagged `[perf:<channel>]` (startup, conn, load, newtab, prepare, send, api, stream, replay, chatview). Enabled automatically in Vite dev mode, or opt-in via `localStorage.setItem("goose.perf", "1")` in a release build.
|
||||
- Backend perf logs live in `crates/goose-acp/src/server.rs` under `target: "perf"` at `debug!` level. Off by default; enable with `RUST_LOG=perf=debug,info` on the `goose serve` process.
|
||||
- `just dev` and `just dev-debug` export `RUST_LOG=perf=debug,info` so the child `goose serve` emits perf logs without extra setup. Override by setting `RUST_LOG` in the environment before invoking `just`.
|
||||
|
||||
## Testing & Verification
|
||||
|
||||
- Unit/component tests use Vitest and Testing Library via `just test` or `pnpm test`.
|
||||
- E2E tests use Playwright via `just test-e2e` and `just test-e2e-all`.
|
||||
- Before handing off a change, run the smallest relevant verification step. Use `just ci` when you need the full local gate.
|
||||
- GitHub Actions also runs desktop-oriented checks, including Playwright coverage, that are broader than the local pre-push hook.
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- `react` 19.1, `react-dom` 19.1
|
||||
- `@tauri-apps/api` 2.x
|
||||
- `@tanstack/react-query` 5.x
|
||||
- `tailwindcss` 3.x with `tailwindcss-animate`
|
||||
- `@tabler/icons-react` for icons (migrating from `lucide-react`)
|
||||
- `class-variance-authority` for component variants
|
||||
- `clsx` + `tailwind-merge` for class merging
|
||||
- `@radix-ui/react-slot` for polymorphic components
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't import from `../` across feature boundaries — use `@/` paths.
|
||||
- Don't put business logic in UI components — extract to hooks or utilities.
|
||||
- Don't use inline styles except for dynamic values (like animation delays).
|
||||
- Don't add dependencies without checking if an existing one covers the need.
|
||||
- Don't skip `type="button"` on buttons.
|
||||
- Don't use color-only indicators without text alternatives.
|
||||
- Never use `--no-verify` when pushing — fix the underlying lint/hook issues.
|
||||
- Don't create root-level `src/types/` or `src/stores/` directories — types belong in `src/shared/types/`, stores belong in `src/features/<feature>/stores/`.
|
||||
- Don't duplicate type definitions across files — each type has one canonical location.
|
||||
- Don't build features end-to-end in the frontend. If it talks to data, secrets, providers, sessions, or the filesystem, it goes through `goose` core via a typed ACP method. See **Architecture → Don't build features entirely in the frontend**.
|
||||
@@ -1 +0,0 @@
|
||||
@AGENTS.md
|
||||
@@ -1,30 +0,0 @@
|
||||
# Goose2
|
||||
|
||||
Goose2 is a Tauri 2 + React 19 desktop app.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. If your shell cannot find `just`, `pnpm`, or `lefthook`, activate Hermit.
|
||||
bash/zsh: `source ./bin/activate-hermit`
|
||||
fish: `source ./bin/activate-hermit.fish`
|
||||
2. Install git hooks: `lefthook install`
|
||||
3. Prepare workspace dependencies: `just setup`
|
||||
4. Start the app: `just dev`
|
||||
|
||||
`just clean` removes Rust build artifacts, `dist`, and `node_modules`. Run `just setup` again before `just dev`.
|
||||
|
||||
`just setup` installs UI workspace dependencies, builds the SDK package, and builds the local debug `goose` CLI binary. `just dev` exports `GOOSE_BIN` to that local binary and loads `src-tauri/tauri.dev.conf.json`, which clears the production `externalBin` requirement during development.
|
||||
|
||||
Run `just` to list available commands, or see [justfile](./justfile) for the full recipe definitions.
|
||||
|
||||
## Important Files
|
||||
|
||||
- [AGENTS.md](./AGENTS.md) repo conventions and agent guidance
|
||||
- [justfile](./justfile) local setup, dev, test, and CI commands
|
||||
- [CODEOWNERS](./CODEOWNERS) code ownership
|
||||
- [.github/workflows/ci.yml](./.github/workflows/ci.yml) CI checks
|
||||
- [.github/ISSUE_TEMPLATE/](./.github/ISSUE_TEMPLATE/) issue templates
|
||||
- [GOVERNANCE.md](./GOVERNANCE.md) project governance
|
||||
- [LICENSE](./LICENSE) license terms
|
||||
|
||||
Project leads should keep this README, [CODEOWNERS](./CODEOWNERS), and the issue templates current. If this repo grows beyond the quick-start flow above, add a `CONTRIBUTING.md` and link it here once it exists.
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**",
|
||||
"!src-tauri/gen",
|
||||
"!src-tauri/plugins/*/permissions/{schemas,autogenerated}",
|
||||
"!.agents",
|
||||
"!.worktrees",
|
||||
"!.claude/worktrees",
|
||||
"!justfile"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": false
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["src/shared/styles/globals.css"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": { "noImportantStyles": "off" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["src/shared/ui/**", "src/components/ai-elements/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"a11y": {
|
||||
"useSemanticElements": "off",
|
||||
"useFocusableInteractive": "off",
|
||||
"useKeyWithClickEvents": "off",
|
||||
"useAriaPropsForRole": "off",
|
||||
"noRedundantRoles": "off"
|
||||
},
|
||||
"correctness": { "useExhaustiveDependencies": "off" },
|
||||
"suspicious": { "noArrayIndexKey": "off", "noDocumentCookie": "off" },
|
||||
"security": { "noDangerouslySetInnerHtml": "off" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "ghost",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/shared/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/shared/ui",
|
||||
"utils": "@/shared/lib/cn",
|
||||
"ui": "@/shared/ui",
|
||||
"lib": "@/shared/lib"
|
||||
},
|
||||
"registryUrl": "https://block.github.io/ghost/r/registry.json",
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
# Goose2 distro bundles
|
||||
|
||||
A Goose2 distro bundle is an optional app-specific package of configuration and policy that the Tauri shell loads at startup.
|
||||
|
||||
## What a distro bundle is
|
||||
|
||||
A distro bundle lives under `ui/goose2/distro/` in development, and is bundled into the packaged app as a Tauri resource in production.
|
||||
|
||||
Current supported files:
|
||||
|
||||
- `distro.json` — distro manifest
|
||||
- `config.yaml` — optional Goose config passed to `goose serve`
|
||||
- `bin/` — optional executables or helper scripts prepended to `PATH` for `goose serve`
|
||||
|
||||
## How it is discovered
|
||||
|
||||
The Tauri app resolves the distro bundle in this order:
|
||||
|
||||
1. `GOOSE_DISTRO_DIR`, if set
|
||||
2. bundled Tauri resource dir at `resource_dir()/distro`
|
||||
|
||||
In development, `just dev` and `just dev-debug` automatically export `GOOSE_DISTRO_DIR` to `ui/goose2/distro` when that directory exists.
|
||||
|
||||
## Manifest shape
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"appVersion": "development",
|
||||
"featureToggles": {
|
||||
"costTracking": false
|
||||
},
|
||||
"providerAllowlist": "databricks"
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
- `appVersion?: string`
|
||||
- optional app version tag supplied by the distro
|
||||
|
||||
- `featureToggles?: Record<string, boolean>`
|
||||
- optional UI/product flags controlled by the distro
|
||||
- currently supported:
|
||||
- `costTracking`
|
||||
- `false` hides cost UI in the token/context usage surfaces
|
||||
- omitted behaves as enabled
|
||||
|
||||
- `providerAllowlist?: string`
|
||||
- comma-separated provider ids
|
||||
- suggests which model providers to show in Settings
|
||||
- suggests which Goose model options to show in the chat model picker
|
||||
|
||||
- `extensionAllowlist?: string`
|
||||
- comma-separated extension ids
|
||||
- reserved for future UI suggestions
|
||||
|
||||
## Runtime effects
|
||||
|
||||
When a distro bundle is present, Goose2 does two kinds of things with it.
|
||||
|
||||
### Frontend behavior
|
||||
|
||||
The frontend loads `get_distro_bundle` during app startup and stores the manifest in Zustand.
|
||||
|
||||
Today it uses that manifest to:
|
||||
|
||||
- filter model providers shown in provider settings via `providerAllowlist`
|
||||
- filter Goose model options shown in the chat input model picker via `providerAllowlist`
|
||||
- hide cost UI when `featureToggles.costTracking === false`
|
||||
|
||||
These allowlists are UI suggestions only. They do not enforce backend access control and do not invalidate existing sessions or saved model choices.
|
||||
|
||||
### Backend / shell behavior
|
||||
|
||||
When the Tauri shell launches the long-lived `goose serve` process, it applies the distro bundle like this:
|
||||
|
||||
- prepends `distro/bin` to `PATH` when present
|
||||
- adds `distro/config.yaml` to `GOOSE_ADDITIONAL_CONFIG_FILES` when present
|
||||
- sets `GOOSE_DISTRO_DIR` to the resolved distro root
|
||||
|
||||
This is shell-level behavior, so it is implemented as Tauri-side setup rather than an ACP method.
|
||||
|
||||
## Development notes
|
||||
|
||||
- packaged apps discover distro content from bundled Tauri resources
|
||||
- local development uses `GOOSE_DISTRO_DIR`
|
||||
- after changing `distro.json`, restart `just dev` so startup reloads the manifest
|
||||
|
||||
## Scope guidance
|
||||
|
||||
Use distro bundles for packaged-app policy and shell-level defaults.
|
||||
|
||||
Good fits:
|
||||
|
||||
- feature flags for Goose2 UI behavior
|
||||
- allowlists that suggest visible product choices
|
||||
- config or helper binaries that should be present when `goose serve` starts
|
||||
|
||||
Avoid using distro bundles as a replacement for normal app state, user settings, or ACP-backed domain data.
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"featureToggles": {
|
||||
"costTracking": true
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { defineConfig } from "@ghost/core";
|
||||
|
||||
export default defineConfig({
|
||||
designSystems: [
|
||||
{
|
||||
name: "goose2",
|
||||
registry: "https://block.github.io/ghost/r/registry.json",
|
||||
componentDir: "src/shared/ui",
|
||||
styleEntry: "src/shared/styles/globals.css",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script>
|
||||
(() => {
|
||||
const getDefaultAccent = (theme) =>
|
||||
theme === "dark" ? "#ffffff" : "#1a1a1a";
|
||||
|
||||
const normalizeHexColor = (color) => {
|
||||
const value = color?.trim();
|
||||
if (!value || value === "default") return null;
|
||||
|
||||
const hex = value.startsWith("#") ? value.slice(1) : value;
|
||||
if (/^[0-9a-fA-F]{3}$/.test(hex)) {
|
||||
return `#${hex
|
||||
.split("")
|
||||
.map((char) => char + char)
|
||||
.join("")
|
||||
.toLowerCase()}`;
|
||||
}
|
||||
if (/^[0-9a-fA-F]{6}$/.test(hex)) {
|
||||
return `#${hex.toLowerCase()}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getRelativeLuminance = (hexColor) => {
|
||||
const hex = hexColor.slice(1);
|
||||
const channels = [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)]
|
||||
.map((channel) => {
|
||||
const value = Number.parseInt(channel, 16) / 255;
|
||||
return value <= 0.04045
|
||||
? value / 12.92
|
||||
: ((value + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
|
||||
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
||||
};
|
||||
|
||||
const getContrastColor = (hexColor) => {
|
||||
const luminance = getRelativeLuminance(hexColor);
|
||||
const blackContrast = (luminance + 0.05) / 0.05;
|
||||
const whiteContrast = 1.05 / (luminance + 0.05);
|
||||
return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
|
||||
};
|
||||
|
||||
try {
|
||||
const root = document.documentElement;
|
||||
const storedTheme = localStorage.getItem("goose-theme") || "system";
|
||||
const resolvedTheme =
|
||||
storedTheme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: storedTheme;
|
||||
const accent =
|
||||
normalizeHexColor(localStorage.getItem("goose-accent-color")) ||
|
||||
getDefaultAccent(resolvedTheme);
|
||||
const foreground = getContrastColor(accent);
|
||||
const density = localStorage.getItem("goose-density") || "comfortable";
|
||||
|
||||
root.classList.add(resolvedTheme === "dark" ? "dark" : "light");
|
||||
root.style.colorScheme = resolvedTheme === "dark" ? "dark" : "light";
|
||||
root.style.setProperty("--brand", accent);
|
||||
root.style.setProperty("--brand-foreground", foreground);
|
||||
root.style.setProperty("--color-brand", accent);
|
||||
root.style.setProperty("--color-brand-foreground", foreground);
|
||||
root.style.accentColor = accent;
|
||||
if (density === "compact" || density === "spacious") {
|
||||
root.dataset.density = density;
|
||||
}
|
||||
} catch {
|
||||
// ThemeProvider applies the canonical theme state after React mounts.
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<title>Goose</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,285 +0,0 @@
|
||||
# Derive a stable default port from the working directory so the same worktree
|
||||
# usually gets the same port. Set VITE_PORT to override it.
|
||||
default_vite_port := `python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)"`
|
||||
|
||||
# Default recipe
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# ── Dev Environment ──────────────────────────────────────────
|
||||
|
||||
# Install dependencies, build workspace packages, and activate git hooks
|
||||
setup:
|
||||
cd ../ && pnpm install
|
||||
cd ../sdk && pnpm build
|
||||
cargo build --manifest-path ../../Cargo.toml -p goose-cli --bin goose
|
||||
|
||||
# ── Build & Check ────────────────────────────────────────────
|
||||
|
||||
# Run all checks (lint, format, typecheck, file sizes)
|
||||
check:
|
||||
pnpm check
|
||||
pnpm typecheck
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
pnpm format
|
||||
cd src-tauri && cargo fmt
|
||||
|
||||
# Check formatting without modifying
|
||||
fmt-check:
|
||||
pnpm exec biome format .
|
||||
cd src-tauri && cargo fmt --check
|
||||
|
||||
# Run clippy on Tauri backend
|
||||
clippy:
|
||||
cd src-tauri && TAURI_CONFIG='{"bundle":{"externalBin":[]}}' cargo clippy -- -D warnings
|
||||
|
||||
# Build the frontend
|
||||
build:
|
||||
pnpm build
|
||||
|
||||
# Check Tauri Rust formatting
|
||||
tauri-fmt-check:
|
||||
cd src-tauri && cargo fmt --check
|
||||
|
||||
# Check Tauri Rust types
|
||||
tauri-check:
|
||||
cd src-tauri && TAURI_CONFIG='{"bundle":{"externalBin":[]}}' cargo check
|
||||
|
||||
# Full CI gate
|
||||
ci: check clippy test build tauri-check
|
||||
|
||||
bundle:
|
||||
pnpm tauri build
|
||||
|
||||
# ── Test ─────────────────────────────────────────────────────
|
||||
|
||||
# Run unit/component tests
|
||||
test:
|
||||
pnpm test
|
||||
|
||||
# Run tests in watch mode
|
||||
test-watch:
|
||||
pnpm test:watch
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
pnpm test:coverage
|
||||
|
||||
# Run E2E smoke tests (builds first)
|
||||
test-e2e:
|
||||
pnpm test:e2e:smoke
|
||||
|
||||
# Run all E2E tests (builds first)
|
||||
test-e2e-all:
|
||||
pnpm test:e2e
|
||||
|
||||
# ── Run ──────────────────────────────────────────────────────
|
||||
|
||||
# Start the desktop app in dev mode
|
||||
dev: build-core
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DEFAULT_VITE_PORT={{ default_vite_port }}
|
||||
VITE_PORT="${VITE_PORT:-$DEFAULT_VITE_PORT}"
|
||||
export VITE_PORT
|
||||
# Enable perf logs in the child `goose serve` process by default.
|
||||
# Override with e.g. RUST_LOG=info just dev to disable.
|
||||
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
||||
PROJECT_DIR=$(pwd)
|
||||
REPO_ROOT=$(cd ../.. && pwd)
|
||||
DISTRO_DIR="${PROJECT_DIR}/distro"
|
||||
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
|
||||
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
|
||||
fi
|
||||
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
|
||||
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
||||
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_DEBUG}"
|
||||
elif [[ -x "${LOCAL_GOOSE_RELEASE}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_RELEASE}"
|
||||
else
|
||||
unset GOOSE_BIN
|
||||
fi
|
||||
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./scripts/start-dev-vite.sh ${VITE_PORT}\",\"cwd\":\"${PROJECT_DIR}\",\"wait\":false}}}")
|
||||
|
||||
if [[ -n "${GOOSE_BIN:-}" ]]; then
|
||||
echo "Using local goose binary: ${GOOSE_BIN}"
|
||||
else
|
||||
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
|
||||
fi
|
||||
|
||||
if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
|
||||
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
|
||||
fi
|
||||
|
||||
# In worktrees, generate a labeled icon so you can tell instances apart
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
GIT_DIR=$(git rev-parse --git-dir)
|
||||
if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then
|
||||
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
|
||||
WORKTREE_LABEL="${BRANCH_NAME##*/}"
|
||||
|
||||
ICON_DIR="$(pwd)/src-tauri/target/dev-icons"
|
||||
mkdir -p "$ICON_DIR"
|
||||
DEV_ICON="$ICON_DIR/icon.icns"
|
||||
|
||||
if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then
|
||||
echo "🌳 Worktree: ${WORKTREE_LABEL}"
|
||||
EXTRA_CONFIG_ARGS+=(--config "{\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm tauri dev --features app-test-driver --config src-tauri/tauri.dev.conf.json "${EXTRA_CONFIG_ARGS[@]}"
|
||||
|
||||
# Start the desktop app with dev config
|
||||
dev-debug: build-core
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DEFAULT_VITE_PORT={{ default_vite_port }}
|
||||
VITE_PORT="${VITE_PORT:-$DEFAULT_VITE_PORT}"
|
||||
export VITE_PORT
|
||||
# Enable perf logs in the child `goose serve` process by default.
|
||||
# Override with e.g. RUST_LOG=info just dev-debug to disable.
|
||||
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
||||
PROJECT_DIR=$(pwd)
|
||||
REPO_ROOT=$(cd ../.. && pwd)
|
||||
DISTRO_DIR="$(pwd)/distro"
|
||||
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
|
||||
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
|
||||
fi
|
||||
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
|
||||
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
||||
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_DEBUG}"
|
||||
elif [[ -x "${LOCAL_GOOSE_RELEASE}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_RELEASE}"
|
||||
else
|
||||
unset GOOSE_BIN
|
||||
fi
|
||||
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./scripts/start-dev-vite.sh ${VITE_PORT}\",\"cwd\":\"${PROJECT_DIR}\",\"wait\":false}}}")
|
||||
|
||||
if [[ -n "${GOOSE_BIN:-}" ]]; then
|
||||
echo "Using local goose binary: ${GOOSE_BIN}"
|
||||
else
|
||||
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
|
||||
fi
|
||||
|
||||
if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
|
||||
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
|
||||
fi
|
||||
|
||||
# In worktrees, generate a labeled icon so you can tell instances apart
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
GIT_DIR=$(git rev-parse --git-dir)
|
||||
if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then
|
||||
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
|
||||
WORKTREE_LABEL="${BRANCH_NAME##*/}"
|
||||
|
||||
ICON_DIR="$(pwd)/src-tauri/target/dev-icons"
|
||||
mkdir -p "$ICON_DIR"
|
||||
DEV_ICON="$ICON_DIR/icon.icns"
|
||||
|
||||
if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then
|
||||
echo "🌳 Worktree: ${WORKTREE_LABEL}"
|
||||
EXTRA_CONFIG_ARGS+=(--config "{\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm tauri dev --config src-tauri/tauri.dev.conf.json "${EXTRA_CONFIG_ARGS[@]}"
|
||||
|
||||
# Start only the frontend dev server
|
||||
dev-frontend:
|
||||
pnpm dev
|
||||
|
||||
# Parse `git worktree list --porcelain` into tab-separated "path\tbranch" lines.
|
||||
# Uses sub() instead of $2 so worktree paths containing spaces are preserved.
|
||||
# Detached-HEAD worktrees (no branch line) are silently skipped.
|
||||
_worktree_awk := '/^worktree / { sub(/^worktree /, ""); wt=$0 } /^branch refs\/heads\// { b=$2; sub(/^refs\/heads\//, "", b); print wt "\t" b }'
|
||||
|
||||
# Compute a stable vite port from a directory path passed as $1.
|
||||
_port_cmd := "import hashlib,sys; h=int(hashlib.sha256(sys.argv[1].encode()).hexdigest(),16); print(10000+h%55000)"
|
||||
|
||||
# List worktrees with an active dev server
|
||||
running:
|
||||
#!/usr/bin/env bash
|
||||
git worktree list --porcelain | awk '{{ _worktree_awk }}' \
|
||||
| while IFS=$'\t' read -r wt branch; do
|
||||
dir="$wt/ui/goose2"
|
||||
port=$(python3 -c '{{ _port_cmd }}' "$dir")
|
||||
if lsof -ti :"$port" &>/dev/null; then
|
||||
echo "$branch"
|
||||
fi
|
||||
done
|
||||
|
||||
# Kill the dev process (if running). Optionally pass a branch name to kill another worktree's process.
|
||||
kill branch="":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -n "{{ branch }}" ]]; then
|
||||
WORKTREE_PATH=$(git worktree list --porcelain \
|
||||
| awk '{{ _worktree_awk }}' \
|
||||
| awk -F'\t' -v branch="{{ branch }}" '$2 == branch { print $1; exit }')
|
||||
if [[ -z "$WORKTREE_PATH" ]]; then
|
||||
echo "No worktree found for branch '{{ branch }}'"
|
||||
exit 1
|
||||
fi
|
||||
TARGET_DIR="$WORKTREE_PATH/ui/goose2"
|
||||
VITE_PORT=$(python3 -c '{{ _port_cmd }}' "$TARGET_DIR")
|
||||
else
|
||||
DEFAULT_VITE_PORT={{ default_vite_port }}
|
||||
VITE_PORT="${VITE_PORT:-$DEFAULT_VITE_PORT}"
|
||||
fi
|
||||
|
||||
PID=$(lsof -ti :"$VITE_PORT" 2>/dev/null | head -1) || true
|
||||
|
||||
if [[ -z "$PID" ]]; then
|
||||
echo "No process found on port $VITE_PORT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PROC_NAME=$(ps -p "$PID" -o comm= 2>/dev/null) || true
|
||||
PROC_BASENAME="${PROC_NAME##*/}"
|
||||
if [[ "$PROC_BASENAME" != "node" ]]; then
|
||||
echo "Process on port $VITE_PORT is '$PROC_NAME', not 'node' — refusing to kill"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PGID=$(ps -p "$PID" -o pgid= 2>/dev/null | tr -d ' ')
|
||||
if [[ -z "$PGID" || "$PGID" == "0" || "$PGID" == "1" ]]; then
|
||||
echo "Killing node (PID $PID) on port $VITE_PORT"
|
||||
kill -9 "$PID"
|
||||
else
|
||||
echo "Killing process group $PGID (found via node PID $PID on port $VITE_PORT)"
|
||||
kill -9 -"$PGID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Kill all dev processes across all worktrees
|
||||
kill-all:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
branches=$(just running)
|
||||
if [[ -z "$branches" ]]; then
|
||||
echo "No running dev servers found"
|
||||
exit 0
|
||||
fi
|
||||
while read -r branch; do
|
||||
just kill "$branch" || true
|
||||
done <<< "$branches"
|
||||
|
||||
# ── Utilities ────────────────────────────────────────────────
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
cd src-tauri && cargo clean
|
||||
rm -rf dist
|
||||
rm -rf node_modules
|
||||
|
||||
build-core:
|
||||
cargo build -p goose-cli --bin goose
|
||||
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"name": "goose2",
|
||||
"private": true,
|
||||
"version": "0.20.1",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:i18n": "node ./scripts/check-i18n-strings.mjs",
|
||||
"lint": "biome lint .",
|
||||
"check": "biome check . && pnpm check:i18n",
|
||||
"format": "biome format --write .",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "pnpm build && playwright test",
|
||||
"test:e2e:smoke": "pnpm build && playwright test --project=smoke",
|
||||
"test:e2e:personas": "pnpm build && playwright test --project=personas",
|
||||
"test:e2e:skills": "pnpm build && playwright test --project=skills",
|
||||
"test:e2e:drafts": "pnpm build && playwright test --project=drafts",
|
||||
"test:app-e2e": "vitest run --config vitest.app-e2e.config.ts",
|
||||
"test-driver": "tsx tests/app-e2e/lib/test-driver-cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aaif/goose-sdk": "workspace:*",
|
||||
"@agentclientprotocol/sdk": "^0.19.0",
|
||||
"@mcp-ui/client": "7.1.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@rive-app/react-webgl2": "^4.27.3",
|
||||
"@streamdown/cjk": "^1.0.3",
|
||||
"@streamdown/code": "^1.1.1",
|
||||
"@streamdown/math": "^1.0.2",
|
||||
"@streamdown/mermaid": "^1.0.2",
|
||||
"@tabler/icons-react": "^3.41.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "~2.7.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-shell": "~2.3.5",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"ai": "^6.0.142",
|
||||
"ansi-to-react": "^6.2.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"gsap": "^3.14.2",
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"media-chrome": "^4.18.3",
|
||||
"motion": "^12.38.0",
|
||||
"nanoid": "^5.1.7",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.72.0",
|
||||
"react-i18next": "^17.0.2",
|
||||
"react-jsx-parser": "^2.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.8.0",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"recharts": "^3.8.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.0.2",
|
||||
"sonner": "^2.0.7",
|
||||
"split-type": "^0.3.4",
|
||||
"streamdown": "^2.5.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"use-stick-to-bottom": "^1.1.3",
|
||||
"vaul": "^1.1.2",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.9",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.9.0",
|
||||
"vite": "^7.0.4",
|
||||
"vitest": "^4.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const previewPort = 4173;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
timeout: 60_000,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [
|
||||
["list"],
|
||||
["html", { open: "never", outputFolder: "playwright-report" }],
|
||||
],
|
||||
use: {
|
||||
baseURL: `http://127.0.0.1:${previewPort}`,
|
||||
screenshot: "only-on-failure",
|
||||
trace: "on-first-retry",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "smoke",
|
||||
testMatch: ["**/smoke.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "personas",
|
||||
testMatch: ["**/personas.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skills",
|
||||
testMatch: ["**/skills.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "drafts",
|
||||
testMatch: ["**/drafts.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
// Opt-in reuse only. Reusing arbitrary local processes makes the suite
|
||||
// flaky when another test run or dev server happens to be bound here.
|
||||
command: `python3 -m http.server ${previewPort} -d dist`,
|
||||
cwd: ".",
|
||||
reuseExistingServer: process.env.PLAYWRIGHT_REUSE_SERVER === "1",
|
||||
url: `http://127.0.0.1:${previewPort}`,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "1.94.1"
|
||||
profile = "default"
|
||||
@@ -1,254 +0,0 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { extname, join, relative } from "node:path";
|
||||
import ts from "typescript";
|
||||
|
||||
const CHECKED_PATHS = [
|
||||
"src/app/ui",
|
||||
"src/features/agents",
|
||||
"src/features/chat/ui",
|
||||
"src/features/home",
|
||||
"src/features/projects",
|
||||
"src/features/settings",
|
||||
"src/features/skills",
|
||||
"src/features/sidebar",
|
||||
"src/features/status",
|
||||
"src/features/sessions",
|
||||
"src/shared/ui/ai-elements/code-block.tsx",
|
||||
"src/shared/ui/ai-elements/environment-variables.tsx",
|
||||
"src/shared/ui/ai-elements/message.tsx",
|
||||
"src/shared/ui/ai-elements/plan.tsx",
|
||||
"src/shared/ui/ai-elements/snippet.tsx",
|
||||
"src/shared/ui/ai-elements/stack-trace.tsx",
|
||||
"src/shared/ui/ai-elements/terminal.tsx",
|
||||
"src/shared/ui/ai-elements/context.tsx",
|
||||
"src/shared/ui/ai-elements/commit.tsx",
|
||||
];
|
||||
|
||||
const EXCLUDED_PATH_SEGMENTS = ["__tests__"];
|
||||
const EXCLUDED_FILE_MARKERS = [".test.", ".spec."];
|
||||
const CHECKED_EXTENSIONS = new Set([".ts", ".tsx"]);
|
||||
const TEXT_ATTRIBUTE_NAMES = new Set([
|
||||
"aria-label",
|
||||
"title",
|
||||
"placeholder",
|
||||
"alt",
|
||||
]);
|
||||
const TEXT_EXCLUDED_TAGS = new Set(["code", "pre", "kbd"]);
|
||||
const IGNORE_COMMENT = "i18n-check-ignore";
|
||||
|
||||
function walkPath(targetPath) {
|
||||
const entries = [];
|
||||
let statEntries;
|
||||
try {
|
||||
statEntries = readdirSync(targetPath, { withFileTypes: true });
|
||||
} catch {
|
||||
if (CHECKED_EXTENSIONS.has(extname(targetPath))) {
|
||||
return [targetPath];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const entry of statEntries) {
|
||||
const fullPath = join(targetPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
entries.push(...walkPath(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CHECKED_EXTENSIONS.has(extname(entry.name))) {
|
||||
entries.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function isExcluded(filePath) {
|
||||
const rel = relative(".", filePath);
|
||||
const normalizedRel = rel.replace(/\\/g, "/");
|
||||
const pathSegments = normalizedRel.split("/");
|
||||
|
||||
return (
|
||||
EXCLUDED_PATH_SEGMENTS.some((segment) => pathSegments.includes(segment)) ||
|
||||
EXCLUDED_FILE_MARKERS.some((marker) => normalizedRel.includes(marker))
|
||||
);
|
||||
}
|
||||
|
||||
function collapseWhitespace(text) {
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function isProbablyUserFacingText(text) {
|
||||
if (!text) return false;
|
||||
if (!/\p{L}/u.test(text)) return false;
|
||||
if (/^https?:\/\//.test(text)) return false;
|
||||
if (/^[./~@#][\w./-]+$/.test(text)) return false;
|
||||
if (/^[A-Z0-9_:-]+$/.test(text)) return false;
|
||||
if (/^[\w.-]+\.[A-Za-z]{2,}$/.test(text)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLineText(sourceText, sourceFile, lineIndex) {
|
||||
if (lineIndex < 0) return "";
|
||||
|
||||
const lineStarts = sourceFile.getLineStarts();
|
||||
if (lineIndex >= lineStarts.length) return "";
|
||||
|
||||
const lineStart = lineStarts[lineIndex];
|
||||
const lineEnd =
|
||||
lineIndex + 1 < lineStarts.length
|
||||
? lineStarts[lineIndex + 1]
|
||||
: sourceText.length;
|
||||
|
||||
return sourceText.slice(lineStart, lineEnd);
|
||||
}
|
||||
|
||||
function hasIgnoreComment(sourceText, sourceFile, node) {
|
||||
const start = node.getStart(sourceFile);
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(start);
|
||||
|
||||
return [line - 1, line].some((lineIndex) =>
|
||||
getLineText(sourceText, sourceFile, lineIndex).includes(IGNORE_COMMENT),
|
||||
);
|
||||
}
|
||||
|
||||
function getParentTagName(node) {
|
||||
if (ts.isJsxElement(node.parent)) {
|
||||
return node.parent.openingElement.tagName.getText();
|
||||
}
|
||||
|
||||
if (ts.isJsxSelfClosingElement(node.parent)) {
|
||||
return node.parent.tagName.getText();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeJsxText(text) {
|
||||
return collapseWhitespace(text);
|
||||
}
|
||||
|
||||
function extractStringFromExpression(expression) {
|
||||
if (!expression) return null;
|
||||
|
||||
if (ts.isStringLiteralLike(expression)) {
|
||||
return expression.text;
|
||||
}
|
||||
|
||||
if (ts.isNoSubstitutionTemplateLiteral(expression)) {
|
||||
return expression.text;
|
||||
}
|
||||
|
||||
if (ts.isTemplateExpression(expression)) {
|
||||
let text = expression.head.text;
|
||||
for (const span of expression.templateSpans) {
|
||||
text += `{${span.expression.getText()}}${span.literal.text}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatLocation(sourceFile, position) {
|
||||
const { line, character } =
|
||||
sourceFile.getLineAndCharacterOfPosition(position);
|
||||
return `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
||||
}
|
||||
|
||||
function collectViolations(filePath) {
|
||||
const sourceText = readFileSync(filePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
const violations = [];
|
||||
|
||||
function report(node, kind, text) {
|
||||
const normalizedText = collapseWhitespace(text);
|
||||
if (!isProbablyUserFacingText(normalizedText)) return;
|
||||
if (hasIgnoreComment(sourceText, sourceFile, node)) return;
|
||||
|
||||
violations.push({
|
||||
location: formatLocation(sourceFile, node.getStart(sourceFile)),
|
||||
kind,
|
||||
text: normalizedText,
|
||||
});
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isJsxText(node)) {
|
||||
const tagName = getParentTagName(node);
|
||||
if (tagName && TEXT_EXCLUDED_TAGS.has(tagName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = normalizeJsxText(node.getText(sourceFile));
|
||||
report(node, "jsx-text", text);
|
||||
}
|
||||
|
||||
if (ts.isJsxAttribute(node)) {
|
||||
const attributeName = node.name.text;
|
||||
if (TEXT_ATTRIBUTE_NAMES.has(attributeName) && node.initializer) {
|
||||
if (ts.isStringLiteral(node.initializer)) {
|
||||
report(
|
||||
node.initializer,
|
||||
`prop:${attributeName}`,
|
||||
node.initializer.text,
|
||||
);
|
||||
}
|
||||
|
||||
if (ts.isJsxExpression(node.initializer)) {
|
||||
const text = extractStringFromExpression(node.initializer.expression);
|
||||
if (text) {
|
||||
report(node.initializer, `prop:${attributeName}`, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isJsxExpression(node) && node.expression) {
|
||||
if (ts.isJsxAttribute(node.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = extractStringFromExpression(node.expression);
|
||||
if (text) {
|
||||
report(node, "jsx-expression", text);
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return violations;
|
||||
}
|
||||
|
||||
const files = CHECKED_PATHS.flatMap(walkPath)
|
||||
.filter((filePath) => !isExcluded(filePath))
|
||||
.sort();
|
||||
|
||||
const violations = files.flatMap((filePath) => collectViolations(filePath));
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("i18n string check failed:");
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
` - ${violation.location} [${violation.kind}] ${JSON.stringify(violation.text)}`,
|
||||
);
|
||||
}
|
||||
console.error("");
|
||||
console.error(
|
||||
`Wrap user-facing strings in translations or annotate a narrow exception with "${IGNORE_COMMENT}".`,
|
||||
);
|
||||
console.error(
|
||||
"The current enforcement scope is intentionally limited to app areas already migrated to i18n.",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("i18n string check passed.");
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
#!/usr/bin/env swift
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
// Generate a dev icon with worktree name badge
|
||||
// Usage: generate-dev-icon.swift <input-icns> <output-icns> <label>
|
||||
|
||||
guard CommandLine.arguments.count == 4 else {
|
||||
fputs("Usage: \(CommandLine.arguments[0]) <input-icns> <output-icns> <label>\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let inputPath = CommandLine.arguments[1]
|
||||
let outputPath = CommandLine.arguments[2]
|
||||
let label = CommandLine.arguments[3]
|
||||
|
||||
// Load the icns file
|
||||
guard let iconImage = NSImage(contentsOfFile: inputPath) else {
|
||||
fputs("Failed to load image: \(inputPath)\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Get the largest representation for best quality
|
||||
guard let rep = iconImage.representations.max(by: { $0.pixelsWide < $1.pixelsWide }) else {
|
||||
fputs("No image representations found\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let size = NSSize(width: rep.pixelsWide, height: rep.pixelsHigh)
|
||||
|
||||
// Create a new image with the badge
|
||||
let newImage = NSImage(size: size)
|
||||
newImage.lockFocus()
|
||||
|
||||
// Draw the original icon
|
||||
iconImage.draw(in: NSRect(origin: .zero, size: size))
|
||||
|
||||
// Configure badge
|
||||
let singleLineHeight = size.height * 0.22
|
||||
let padding = size.width * 0.03
|
||||
let maxBadgeWidth = size.width * 0.9
|
||||
|
||||
// Text attributes - calculate font size to fit
|
||||
let maxFontSize = singleLineHeight * 0.65
|
||||
var fontSize = maxFontSize
|
||||
var attributes: [NSAttributedString.Key: Any]
|
||||
|
||||
// Helper to wrap text on `-` characters
|
||||
func wrapText(_ text: String, maxWidth: CGFloat, attributes: [NSAttributedString.Key: Any]) -> [String] {
|
||||
let singleLineSize = (text as NSString).size(withAttributes: attributes)
|
||||
if singleLineSize.width <= maxWidth {
|
||||
return [text]
|
||||
}
|
||||
|
||||
// Split on `-` and try to form lines
|
||||
let parts = text.components(separatedBy: "-")
|
||||
if parts.count == 1 {
|
||||
return [text] // No `-` to wrap on
|
||||
}
|
||||
|
||||
var lines: [String] = []
|
||||
var currentLine = ""
|
||||
|
||||
for (index, part) in parts.enumerated() {
|
||||
let separator = index == 0 ? "" : "-"
|
||||
let testLine = currentLine.isEmpty ? part : currentLine + separator + part
|
||||
let testSize = (testLine as NSString).size(withAttributes: attributes)
|
||||
|
||||
if testSize.width <= maxWidth || currentLine.isEmpty {
|
||||
currentLine = testLine
|
||||
} else {
|
||||
lines.append(currentLine)
|
||||
currentLine = part
|
||||
}
|
||||
}
|
||||
if !currentLine.isEmpty {
|
||||
lines.append(currentLine)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// Find font size that fits (allowing up to 2 lines)
|
||||
var lines: [String] = []
|
||||
repeat {
|
||||
attributes = [
|
||||
.font: NSFont.systemFont(ofSize: fontSize, weight: .bold),
|
||||
.foregroundColor: NSColor.white
|
||||
]
|
||||
lines = wrapText(label, maxWidth: maxBadgeWidth - padding * 4, attributes: attributes)
|
||||
fontSize -= 1
|
||||
} while lines.count > 2 && fontSize > 8
|
||||
|
||||
// Calculate text dimensions using typographic metrics
|
||||
let lineHeight = (lines.first! as NSString).size(withAttributes: attributes).height
|
||||
let textHeight = lineHeight * CGFloat(lines.count)
|
||||
let maxLineWidth = lines.map { ($0 as NSString).size(withAttributes: attributes).width }.max() ?? 0
|
||||
|
||||
// Badge dimensions based on text
|
||||
let badgeHeight = textHeight + padding * 2
|
||||
let cornerRadius = badgeHeight * 0.2
|
||||
let badgeWidth = maxLineWidth + padding * 4
|
||||
let badgeX = (size.width - badgeWidth) / 2
|
||||
let badgeY = size.height - badgeHeight - padding - size.height * 0.05
|
||||
|
||||
// Draw badge background (light blue semi-transparent)
|
||||
let badgePath = NSBezierPath(roundedRect: NSRect(x: badgeX, y: badgeY, width: badgeWidth, height: badgeHeight),
|
||||
xRadius: cornerRadius, yRadius: cornerRadius)
|
||||
NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: 0.85).setFill()
|
||||
badgePath.fill()
|
||||
|
||||
// Draw text centered in badge (multiple lines, bottom to top)
|
||||
for (index, line) in lines.reversed().enumerated() {
|
||||
let lineSize = (line as NSString).size(withAttributes: attributes)
|
||||
let textX = badgeX + (badgeWidth - lineSize.width) / 2
|
||||
let textY = badgeY + padding + lineHeight * CGFloat(index)
|
||||
(line as NSString).draw(at: NSPoint(x: textX, y: textY), withAttributes: attributes)
|
||||
}
|
||||
|
||||
newImage.unlockFocus()
|
||||
|
||||
// Convert to icns format
|
||||
// First, create PNG data at multiple sizes for icns
|
||||
guard let tiffData = newImage.tiffRepresentation,
|
||||
let bitmapRep = NSBitmapImageRep(data: tiffData) else {
|
||||
fputs("Failed to create bitmap representation\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// For simplicity, we'll create a PNG and then use iconutil if available,
|
||||
// or just save as PNG for the icon (Tauri can use PNG)
|
||||
guard let pngData = bitmapRep.representation(using: .png, properties: [:]) else {
|
||||
fputs("Failed to create PNG data\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// If output is .icns, we need to create an iconset and use iconutil
|
||||
if outputPath.hasSuffix(".icns") {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
let iconsetPath = tempDir.appendingPathComponent("goose-dev.iconset")
|
||||
|
||||
// Remove existing iconset if present
|
||||
try? FileManager.default.removeItem(at: iconsetPath)
|
||||
try! FileManager.default.createDirectory(at: iconsetPath, withIntermediateDirectories: true)
|
||||
|
||||
// Generate all required sizes for iconset
|
||||
let sizes: [(name: String, size: Int)] = [
|
||||
("icon_16x16", 16),
|
||||
("icon_16x16@2x", 32),
|
||||
("icon_32x32", 32),
|
||||
("icon_32x32@2x", 64),
|
||||
("icon_128x128", 128),
|
||||
("icon_128x128@2x", 256),
|
||||
("icon_256x256", 256),
|
||||
("icon_256x256@2x", 512),
|
||||
("icon_512x512", 512),
|
||||
("icon_512x512@2x", 1024)
|
||||
]
|
||||
|
||||
for (name, targetSize) in sizes {
|
||||
let resizedImage = NSImage(size: NSSize(width: targetSize, height: targetSize))
|
||||
resizedImage.lockFocus()
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
newImage.draw(in: NSRect(x: 0, y: 0, width: targetSize, height: targetSize))
|
||||
resizedImage.unlockFocus()
|
||||
|
||||
guard let resizedTiff = resizedImage.tiffRepresentation,
|
||||
let resizedBitmap = NSBitmapImageRep(data: resizedTiff),
|
||||
let resizedPng = resizedBitmap.representation(using: .png, properties: [:]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let filePath = iconsetPath.appendingPathComponent("\(name).png")
|
||||
try! resizedPng.write(to: filePath)
|
||||
}
|
||||
|
||||
// Use iconutil to create icns
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil")
|
||||
process.arguments = ["-c", "icns", iconsetPath.path, "-o", outputPath]
|
||||
try! process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
// Cleanup
|
||||
try? FileManager.default.removeItem(at: iconsetPath)
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
fputs("iconutil failed\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
} else {
|
||||
// Just save as PNG
|
||||
try! pngData.write(to: URL(fileURLWithPath: outputPath))
|
||||
}
|
||||
|
||||
print("Generated: \(outputPath)")
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reset the provider inventory tables to empty, as if migration 12 just ran.
|
||||
# This lets you test the first-use experience (cold inventory).
|
||||
#
|
||||
# Usage: ./scripts/reset-inventory.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DB="${GOOSE_DB:-$HOME/.local/share/goose/sessions/sessions.db}"
|
||||
|
||||
if [ ! -f "$DB" ]; then
|
||||
echo "Database not found at $DB"
|
||||
echo "Set GOOSE_DB to override the path."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Database: $DB"
|
||||
echo ""
|
||||
echo "Before:"
|
||||
echo " provider_inventory_entries: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_entries;')"
|
||||
echo " provider_inventory_models: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_models;')"
|
||||
|
||||
# ON DELETE CASCADE on provider_inventory_models means deleting entries clears both tables.
|
||||
# Delete models first since CASCADE isn't reliable in all sqlite3 builds,
|
||||
# then delete entries.
|
||||
sqlite3 "$DB" "DELETE FROM provider_inventory_models; DELETE FROM provider_inventory_entries;"
|
||||
|
||||
echo ""
|
||||
echo "After:"
|
||||
echo " provider_inventory_entries: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_entries;')"
|
||||
echo " provider_inventory_models: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_models;')"
|
||||
echo ""
|
||||
echo "Inventory tables are empty. Restart goose to test first-use flow."
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VITE_PORT="${1:-${VITE_PORT:-}}"
|
||||
if [[ -z "${VITE_PORT}" ]]; then
|
||||
echo "VITE_PORT is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PID=$(lsof -ti :"${VITE_PORT}" 2>/dev/null | head -1 || true)
|
||||
if [[ -n "${PID}" ]]; then
|
||||
PROC_ARGS="$(ps -p "${PID}" -o args= 2>/dev/null || true)"
|
||||
PROC_CWD="$(lsof -a -p "${PID}" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1)"
|
||||
if [[ "${PROC_CWD}" == "${PROJECT_DIR}" && "${PROC_ARGS}" == *"--port ${VITE_PORT} --strictPort"* ]]; then
|
||||
echo "Reusing existing Goose2 Vite dev server on port ${VITE_PORT} (PID ${PID})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PROC_NAME="$(ps -p "${PID}" -o comm= 2>/dev/null || true)"
|
||||
echo "Port ${VITE_PORT} is already in use by '${PROC_NAME}' (PID ${PID})." >&2
|
||||
echo "Run 'just goose2 kill' if it is a stale Goose2 dev server, or rerun with VITE_PORT=<free-port> just goose2 dev." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${PROJECT_DIR}"
|
||||
exec pnpm exec vite --port "${VITE_PORT}" --strictPort
|
||||
@@ -1,51 +0,0 @@
|
||||
[package]
|
||||
name = "goose2"
|
||||
version = "0.1.0"
|
||||
description = "Goose desktop app"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "goose-tauri"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "goose2_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["protocol-asset"] }
|
||||
tauri-plugin-app-test-driver = { path = "plugins/app-test-driver" }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
tauri-plugin-log = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
dirs = "6.0.0"
|
||||
log = "0.4.29"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
doctor = { git = "https://github.com/block/builderbot", rev = "8e1c3ec145edc0df5f04b4427cfd758378036862" }
|
||||
ignore = "0.4.25"
|
||||
base64 = "0.22"
|
||||
mime_guess = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
keyring = { version = "3", features = ["apple-native"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
keyring = { version = "3", features = ["windows-native"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
keyring = { version = "3", features = ["linux-native-sync-persistent", "crypto-rust"] }
|
||||
|
||||
[features]
|
||||
app-test-driver = []
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Goose uses your microphone to capture voice input for dictation.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-min-size",
|
||||
"opener:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$HOME/**"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.goose/**"
|
||||
},
|
||||
{
|
||||
"path": "$TEMP/**"
|
||||
},
|
||||
{
|
||||
"path": "/Volumes/**"
|
||||
},
|
||||
{
|
||||
"path": "/mnt/**"
|
||||
},
|
||||
{
|
||||
"path": "/workspace/**"
|
||||
},
|
||||
{
|
||||
"path": "/workspaces/**"
|
||||
},
|
||||
{
|
||||
"path": "/opt/**"
|
||||
},
|
||||
{
|
||||
"path": "/srv/**"
|
||||
},
|
||||
{
|
||||
"path": "*:/**"
|
||||
}
|
||||
]
|
||||
},
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"app-test-driver:default",
|
||||
"core:webview:allow-set-webview-zoom"
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1 +0,0 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","core:window:allow-set-size","core:window:allow-set-min-size","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","app-test-driver:default","core:webview:allow-set-webview-zoom"]}}
|
||||
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 779 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 395 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |