diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000000..39d0e4accc --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1 @@ +notify = [ "bash", "-c", "curl -sf -X POST -H 'Content-Type: application/json' -H \"X-Emdash-Token: $EMDASH_HOOK_TOKEN\" -H \"X-Emdash-Pty-Id: $EMDASH_PTY_ID\" -H \"X-Emdash-Event-Type: notification\" -d \"$1\" \"http://127.0.0.1:$EMDASH_HOOK_PORT/hook\" || true", "_" ] diff --git a/.eslintrc.json b/.eslintrc.json index 634d89c2b0..13ec039a5e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,23 +11,18 @@ } ], "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-empty-function": "off", "prefer-const": "warn", "react-hooks/set-state-in-effect": "warn" }, - "overrides": [ - { - "files": ["src/main/**/*.ts", "src/main/**/*.tsx"], - "rules": { - "@typescript-eslint/no-var-requires": "off" - } - } - ], + "overrides": [], "env": { "browser": true, "es2020": true, "node": true - } + }, + "ignorePatterns": ["**/_*/**"] } diff --git a/.github/workflows/code-consistency-check.yml b/.github/workflows/code-consistency-check.yml index e514214cf5..8126031c53 100644 --- a/.github/workflows/code-consistency-check.yml +++ b/.github/workflows/code-consistency-check.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22.20.0' + node-version: '24' cache: 'pnpm' - name: Install dependencies @@ -48,7 +48,7 @@ jobs: # run: pnpm run lint - name: Type check - run: pnpm run type-check + run: pnpm run typecheck vitest: runs-on: ubuntu-latest @@ -65,7 +65,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22.20.0' + node-version: '24' cache: 'pnpm' - name: Install Linux native deps diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 385da7f123..da99edbd03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - 'v*' + - 'v1.*' workflow_dispatch: inputs: arch: @@ -11,9 +11,9 @@ on: required: false default: 'both' dry_run: - description: 'Build signed+stapled but DO NOT publish a GitHub Release (artifacts only)' + description: 'Build signed+stapled but DO NOT publish (artifacts only)' required: false - default: 'false' + default: 'true' permissions: contents: read @@ -26,7 +26,6 @@ jobs: id: init run: | set -euo pipefail - # Normalize dry_run from workflow_dispatch inputs (string: true/false) DRY=${{ inputs.dry_run || '' }} if [ -z "$DRY" ]; then DRY=${{ github.event.inputs.dry_run || '' }}; fi DRY=$(printf "%s" "$DRY" | tr '[:upper:]' '[:lower:]') @@ -35,240 +34,92 @@ jobs: *) DRY=false ;; esac echo "dry_run=$DRY" >> "$GITHUB_OUTPUT" - - name: Checkout - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: version: 10.28.2 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' cache: 'pnpm' - - - name: Setup Python 3.11 for node-gyp - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Install Python build deps (setuptools shim for distutils) - run: | - python -m pip install --upgrade pip setuptools wheel - echo "python=$(which python3)" >> $GITHUB_ENV - - - name: Install dependencies (strict lockfile) - env: - npm_config_python: ${{ env.python }} - run: pnpm install --frozen-lockfile + - run: python -m pip install --upgrade pip setuptools wheel + - run: pnpm install --frozen-lockfile + - run: pnpm run build - - name: Build app (ts + vite) - run: pnpm run build + - run: echo "NODE_OPTIONS=${NODE_OPTIONS:-not set}" && node --version - - name: Inject PostHog config (dev build job) + - name: Inject telemetry env: PH_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }} PH_HOST: ${{ secrets.POSTHOG_HOST }} - run: | - set -euo pipefail - if [ -z "${PH_KEY:-}" ] || [ -z "${PH_HOST:-}" ]; then - echo "PostHog secrets not set; skipping telemetry injection (DMG will have telemetry disabled)." - exit 0 - fi - mkdir -p dist/main - cat > dist/main/appConfig.json <<'JSON' - { - "posthogHost": "__PH_HOST__", - "posthogKey": "__PH_KEY__" - } - JSON - sed -i '' "s#__PH_HOST__#${PH_HOST}#g" dist/main/appConfig.json - sed -i '' "s#__PH_KEY__#${PH_KEY}#g" dist/main/appConfig.json - echo "Wrote dist/main/appConfig.json" - - - name: Build DMG(s) (no publish) + run: node --experimental-strip-types scripts/release/inject-telemetry.ts + + - name: Build unsigned DMGs env: CSC_IDENTITY_AUTO_DISCOVERY: 'false' - run: | - set -euo pipefail - ARCH_INPUT="${{ github.event.inputs.arch }}" - if [ -z "$ARCH_INPUT" ] || [ "$ARCH_INPUT" = "both" ]; then - ARCHS=(x64 arm64) - elif [ "$ARCH_INPUT" = "arm64" ]; then - ARCHS=(arm64) - elif [ "$ARCH_INPUT" = "x64" ]; then - ARCHS=(x64) - else - echo "Unknown arch input: $ARCH_INPUT" && exit 1 - fi - ELECTRON_VERSION=$(node -p "require('electron/package.json').version") - # Build each architecture separately to ensure native modules match. - # Building both at once with --x64 --arm64 would use the same node_modules/ - # for both, causing architecture mismatches (issue #706). - for A in "${ARCHS[@]}"; do - echo "=== Building for $A ===" - echo "Rebuilding native modules for $A (Electron $ELECTRON_VERSION)" - npm_config_build_from_source=true pnpm exec electron-rebuild -f -a "$A" -v "$ELECTRON_VERSION" -o sqlite3,node-pty,keytar - echo "Packaging $A DMG" - pnpm exec electron-builder --mac dmg --$A --publish never --config.npmRebuild=false - done - - - name: Verify native module architectures match Electron - run: | - set -euo pipefail - # Verify that each build has native modules matching its Electron binary architecture. - # This catches issues like #706 where x64 builds had arm64 native modules. - VERIFIED_COUNT=0 - for APP_DIR in release/mac-*/emdash.app; do - [ -d "$APP_DIR" ] || continue - ARCH_DIR=$(basename "$(dirname "$APP_DIR")") - case "$ARCH_DIR" in - mac-arm64) EXPECTED_ARCH="arm64" ;; - mac-x64|mac) EXPECTED_ARCH="x86_64" ;; - *) echo "Unknown arch dir: $ARCH_DIR"; continue ;; - esac - echo "=== Checking $APP_DIR (expected: $EXPECTED_ARCH) ===" - ELECTRON_BIN="$APP_DIR/Contents/MacOS/emdash" - SQLITE_NODE="$APP_DIR/Contents/Resources/app.asar.unpacked/node_modules/sqlite3/build/Release/node_sqlite3.node" - # Check Electron binary architecture - ELECTRON_ARCH=$(file "$ELECTRON_BIN" | grep -o 'arm64\|x86_64' | head -1) - echo "Electron binary: $ELECTRON_ARCH" - if [ "$ELECTRON_ARCH" != "$EXPECTED_ARCH" ]; then - echo "::error::Electron arch mismatch: got $ELECTRON_ARCH, expected $EXPECTED_ARCH" - exit 1 - fi - # Check sqlite3 native module architecture - if [ -f "$SQLITE_NODE" ]; then - SQLITE_ARCH=$(file "$SQLITE_NODE" | grep -o 'arm64\|x86_64' | head -1) - echo "sqlite3 native module: $SQLITE_ARCH" - if [ "$SQLITE_ARCH" != "$EXPECTED_ARCH" ]; then - echo "::error::sqlite3 arch mismatch: got $SQLITE_ARCH, expected $EXPECTED_ARCH" - exit 1 - fi - else - echo "::warning::sqlite3 native module not found at $SQLITE_NODE" - fi - VERIFIED_COUNT=$((VERIFIED_COUNT + 1)) - done - if [ "$VERIFIED_COUNT" -eq 0 ]; then - echo "::error::No app bundles found to verify" - exit 1 - fi - echo "Verified $VERIFIED_COUNT app bundle(s)" + run: > + node --experimental-strip-types scripts/release/build.ts + --platform mac --arch ${{ github.event.inputs.arch || 'both' }} --targets dmg - - name: Smoke test sqlite3 in packaged app (arm64) - if: ${{ github.event.inputs.arch == '' || github.event.inputs.arch == 'arm64' || github.event.inputs.arch == 'both' }} - run: | - set -euo pipefail - APP="release/mac-arm64/emdash.app" - if [ -d "$APP" ]; then - echo "Requiring sqlite3 using packaged Electron (arm64)…" - NODE_PATH="$APP/Contents/Resources/app.asar.unpacked/node_modules" \ - ELECTRON_RUN_AS_NODE=1 "$APP/Contents/MacOS/emdash" -e "require('sqlite3'); console.log('sqlite3 OK')" - else - echo "Arm64 app not found at $APP" && exit 1 - fi + - name: Verify macOS bundle + run: node --experimental-strip-types scripts/release/verify-mac.ts release-linux: if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: - contents: write + contents: read environment: release steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: version: 10.28.2 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' cache: 'pnpm' - - - name: Setup Python 3.11 for node-gyp - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Install Python build deps (setuptools) - run: | - python -m pip install --upgrade pip setuptools wheel - echo "python=$(which python3)" >> $GITHUB_ENV - + - run: python -m pip install --upgrade pip setuptools wheel - name: Install system build dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential pkg-config libsecret-1-dev rpm - - - name: Install dependencies (allow platform optional deps) - env: - npm_config_python: ${{ env.python }} - run: | - # pnpm lockfiles are cross-platform; keep lockfile strict for reproducible builds - pnpm install --frozen-lockfile + run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config libsecret-1-dev rpm + - run: pnpm install --frozen-lockfile + - run: pnpm run build - - name: Build app (ts + vite) - run: pnpm run build + - run: echo "NODE_OPTIONS=${NODE_OPTIONS:-not set}" && node --version - - name: Inject PostHog config + - name: Inject telemetry env: PH_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }} PH_HOST: ${{ secrets.POSTHOG_HOST }} - run: | - set -euo pipefail - if [ -z "${PH_KEY:-}" ] || [ -z "${PH_HOST:-}" ]; then - echo "PostHog secrets not set; skipping telemetry injection (app will have telemetry disabled)." - exit 0 - fi - mkdir -p dist/main - cat > dist/main/appConfig.json <<'JSON' - { - "posthogHost": "__PH_HOST__", - "posthogKey": "__PH_KEY__" - } - JSON - sed -i "s#__PH_HOST__#${PH_HOST}#g" dist/main/appConfig.json - sed -i "s#__PH_KEY__#${PH_KEY}#g" dist/main/appConfig.json - echo "Wrote dist/main/appConfig.json" - - - name: Rebuild native modules for Electron (x64) - run: | - set -euo pipefail - ELECTRON_VERSION=$(node -p "require('electron/package.json').version") - npm_config_build_from_source=true pnpm exec electron-rebuild -f -a x64 -v "$ELECTRON_VERSION" -o sqlite3,node-pty,keytar + run: node --experimental-strip-types scripts/release/inject-telemetry.ts - - name: Build Linux packages (AppImage, .deb and .rpm) - run: pnpm run package:linux + - name: Build Linux packages + run: > + node --experimental-strip-types scripts/release/build.ts + --platform linux --arch x64 - - name: Upload Linux packages to GitHub Release + - name: Upload to R2 env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - TAG="${GITHUB_REF_NAME}" - # Create if missing, or publish if draft - if gh release view "$TAG" >/dev/null 2>&1; then - gh release edit "$TAG" --draft=false --prerelease=false - else - gh release create "$TAG" --title "$TAG" --generate-notes --latest || gh release edit "$TAG" --draft=false --prerelease=false - fi - # Upload Linux packages and metadata - FILES=(release/emdash-*.AppImage release/emdash-*.deb release/emdash-*.rpm) - if [ -f release/latest-linux.yml ]; then FILES+=(release/latest-linux.yml); fi - gh release upload "$TAG" "${FILES[@]}" --clobber + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_BUCKET: ${{ secrets.R2_BUCKET }} + run: node --experimental-strip-types scripts/release/upload-r2.ts release-win: if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' runs-on: windows-2022 permissions: - contents: write + contents: read environment: release steps: - name: Init flags @@ -278,7 +129,6 @@ jobs: set -euo pipefail DRY=${{ inputs.dry_run || '' }} if [ -z "$DRY" ]; then DRY=${{ github.event.inputs.dry_run || '' }}; fi - # For branch pushes, always do a dry run to avoid creating "releases" for branch names. if [ "${GITHUB_EVENT_NAME:-}" = "push" ] && [ "${GITHUB_REF_TYPE:-}" = "branch" ]; then DRY=true fi @@ -289,34 +139,25 @@ jobs: esac echo "dry_run=$DRY" >> "$GITHUB_OUTPUT" - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: version: 10.28.2 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: - # Node 22 can trigger native module source builds that are flakier on Windows. - # Node 20 is within engines range and closer to Electron's Node baseline. - node-version: '20' + node-version: '24' cache: 'pnpm' - - - name: Setup Python 3.11 for node-gyp - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Install Python build deps (setuptools shim for distutils) + - name: Install Python build deps shell: bash run: | python -m pip install --upgrade pip setuptools wheel echo "python=$(which python)" >> "$GITHUB_ENV" - - name: Install dependencies (strict lockfile) + - name: Install dependencies shell: bash env: npm_config_python: ${{ env.python }} @@ -324,30 +165,19 @@ jobs: GYP_MSVS_VERSION: 2022 run: pnpm install --frozen-lockfile - - name: Build app (ts + vite) + - name: Build app shell: bash run: pnpm run build - - name: Inject PostHog config + - shell: bash + run: echo "NODE_OPTIONS=${NODE_OPTIONS:-not set}" && node --version + + - name: Inject telemetry shell: bash env: PH_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }} PH_HOST: ${{ secrets.POSTHOG_HOST }} - run: | - set -euo pipefail - if [ -z "${PH_KEY:-}" ] || [ -z "${PH_HOST:-}" ]; then - echo "PostHog secrets not set; skipping telemetry injection (app will have telemetry disabled)." - exit 0 - fi - node - <<'NODE' - const fs = require('node:fs'); - fs.mkdirSync('dist/main', { recursive: true }); - fs.writeFileSync( - 'dist/main/appConfig.json', - JSON.stringify({ posthogHost: process.env.PH_HOST, posthogKey: process.env.PH_KEY }, null, 2) - ); - console.log('Wrote dist/main/appConfig.json'); - NODE + run: node --experimental-strip-types scripts/release/inject-telemetry.ts - name: Check Azure Trusted Signing secrets id: signing @@ -359,80 +189,37 @@ jobs: run: | if [ -n "$AZ_TENANT" ] && [ -n "$AZ_CLIENT" ] && [ -n "$AZ_SECRET" ]; then echo "has_signing=true" >> "$GITHUB_OUTPUT" - echo "Azure Trusted Signing secrets are configured." else echo "has_signing=false" >> "$GITHUB_OUTPUT" echo "::warning::Azure Trusted Signing secrets not configured. Windows build will be unsigned." fi - - name: Rebuild native modules (x64) - shell: bash - run: | - set -euo pipefail - ELECTRON_VERSION=$(node -p "require('electron/package.json').version") - npm_config_build_from_source=true pnpm exec electron-rebuild -f -a x64 -v "$ELECTRON_VERSION" -o sqlite3,node-pty,keytar - - - name: Build Windows packages (NSIS + MSI) (no publish) + - name: Build Windows packages shell: bash env: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - run: | - set -euo pipefail - pnpm exec electron-builder --win nsis msi --x64 --publish never --config.npmRebuild=false - ls -lah release || true + run: > + node --experimental-strip-types scripts/release/build.ts + --platform win --arch x64 - name: Verify Windows code signature if: ${{ steps.signing.outputs.has_signing == 'true' }} - shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - $files = @() - $files += Get-ChildItem -Path release -Filter 'emdash-*.exe' -ErrorAction SilentlyContinue - $files += Get-ChildItem -Path release -Filter 'emdash-*.msi' -ErrorAction SilentlyContinue - if ($files.Count -eq 0) { - Write-Host "::error::No .exe or .msi files found in release/ to verify" - exit 1 - } - $failed = $false - foreach ($f in $files) { - Write-Host "Verifying signature on $($f.Name)..." - $sig = Get-AuthenticodeSignature -FilePath $f.FullName - if ($sig.Status -ne 'Valid') { - Write-Host "::error::Signature invalid on $($f.Name): $($sig.Status) - $($sig.StatusMessage)" - $failed = $true - } else { - Write-Host " Status: $($sig.Status)" - Write-Host " Subject: $($sig.SignerCertificate.Subject)" - Write-Host " Issuer: $($sig.SignerCertificate.Issuer)" - Write-Host " Thumbprint: $($sig.SignerCertificate.Thumbprint)" - } - } - if ($failed) { exit 1 } - Write-Host "All Windows installers are properly signed." - - - name: Publish GitHub Release and upload Windows artifacts + shell: bash + run: node --experimental-strip-types scripts/release/verify-win.ts + + - name: Upload to R2 if: ${{ steps.init.outputs.dry_run != 'true' }} shell: bash env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - TAG="${GITHUB_REF_NAME}" - # Create if missing, or publish if draft - if gh release view "$TAG" >/dev/null 2>&1; then - gh release edit "$TAG" --draft=false --prerelease=false - else - gh release create "$TAG" --title "$TAG" --generate-notes --latest || gh release edit "$TAG" --draft=false --prerelease=false - fi - FILES=(release/emdash-*.exe release/emdash-*.msi) - if ls release/*.blockmap >/dev/null 2>&1; then FILES+=(release/*.blockmap); fi - if [ -f release/latest.yml ]; then FILES+=(release/latest.yml); fi - if [ -f release/latest-win.yml ]; then FILES+=(release/latest-win.yml); fi - gh release upload "$TAG" "${FILES[@]}" --clobber + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_BUCKET: ${{ secrets.R2_BUCKET }} + run: node --experimental-strip-types scripts/release/upload-r2.ts - - name: Upload Windows artifacts (dry-run) + - name: Upload artifacts (dry-run) if: ${{ steps.init.outputs.dry_run == 'true' }} uses: actions/upload-artifact@v4 with: @@ -441,22 +228,20 @@ jobs: release/emdash-*.exe release/emdash-*.msi release/*.blockmap - release/latest*.yml + release/v1-stable*.yml if-no-files-found: error release-mac: - # Run for tagged pushes (normal release) and for manual runs (dry run or ad-hoc release) if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' runs-on: macos-latest permissions: - contents: write + contents: read environment: release steps: - name: Init flags id: init run: | set -euo pipefail - # Normalize dry_run from workflow_dispatch inputs (string: true/false) DRY=${{ inputs.dry_run || '' }} if [ -z "$DRY" ]; then DRY=${{ github.event.inputs.dry_run || '' }}; fi DRY=$(printf "%s" "$DRY" | tr '[:upper:]' '[:lower:]') @@ -465,34 +250,23 @@ jobs: *) DRY=false ;; esac echo "dry_run=$DRY" >> "$GITHUB_OUTPUT" - - name: Checkout - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: version: 10.28.2 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' cache: 'pnpm' - - - name: Setup Python 3.11 for node-gyp - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Install Python build deps (setuptools shim for distutils) - run: | + - run: | python -m pip install --upgrade pip setuptools wheel echo "python=$(which python3)" >> $GITHUB_ENV - - - name: Install dependencies (strict lockfile) - env: - npm_config_python: ${{ env.python }} - run: pnpm install --frozen-lockfile + - run: pnpm install --frozen-lockfile - name: Import Apple signing certificate uses: apple-actions/import-codesign-certs@v2 @@ -500,299 +274,56 @@ jobs: p12-file-base64: ${{ secrets.CERTIFICATE_P12 }} p12-password: ${{ secrets.CERTIFICATE_PASSWORD }} - - name: Build app (ts + vite) - run: pnpm run build + - run: pnpm run build + + - run: echo "NODE_OPTIONS=${NODE_OPTIONS:-not set}" && node --version - - name: Inject PostHog config (release job) + - name: Inject telemetry env: PH_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }} PH_HOST: ${{ secrets.POSTHOG_HOST }} - run: | - set -euo pipefail - if [ -z "${PH_KEY:-}" ] || [ -z "${PH_HOST:-}" ]; then - echo "::warning::PostHog secrets not set; telemetry will be disabled in this build." - exit 0 - fi - mkdir -p dist/main - cat > dist/main/appConfig.json <<'JSON' - { - "posthogHost": "__PH_HOST__", - "posthogKey": "__PH_KEY__" - } - JSON - sed -i '' "s#__PH_HOST__#${PH_HOST}#g" dist/main/appConfig.json - sed -i '' "s#__PH_KEY__#${PH_KEY}#g" dist/main/appConfig.json - echo "Wrote dist/main/appConfig.json" - - - name: Check notarization secrets - id: flags - env: - K: ${{ secrets.APPLE_API_KEY }} - KID: ${{ secrets.APPLE_API_KEY_ID }} - ISS: ${{ secrets.APPLE_API_ISSUER }} - run: | - if [ -n "$K" ] && [ -n "$KID" ] && [ -n "$ISS" ]; then - echo "has_apple_api=true" >> $GITHUB_OUTPUT - else - echo "has_apple_api=false" >> $GITHUB_OUTPUT - fi - - - name: Prepare Apple API key (if provided) - if: ${{ steps.flags.outputs.has_apple_api == 'true' }} - run: | - echo "${{ secrets.APPLE_API_KEY }}" > ./apple_api_key.p8 - echo "APPLE_API_KEY=$(pwd)/apple_api_key.p8" >> $GITHUB_ENV - echo "APPLE_API_KEY_ID=${{ secrets.APPLE_API_KEY_ID }}" >> $GITHUB_ENV - echo "APPLE_API_ISSUER=${{ secrets.APPLE_API_ISSUER }}" >> $GITHUB_ENV + run: node --experimental-strip-types scripts/release/inject-telemetry.ts - - name: Build signed DMG(s) (do not publish yet) + - name: Build signed DMGs + ZIPs env: CSC_IDENTITY_AUTO_DISCOVERY: 'true' APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - # Guard against legacy env vars overriding cert-import flow - unset CSC_LINK CSC_KEY_PASSWORD CSC_NAME - # Force Apple ID notarization for the app build to avoid bad APPLE_API_KEY paths - unset APPLE_API_KEY APPLE_API_KEY_ID APPLE_API_ISSUER - export TEAM_ID="${APPLE_TEAM_ID}" - export NOTARIZE_TEAM_ID="${APPLE_TEAM_ID}" - echo "App notarization method: Apple ID (TEAM_ID=$TEAM_ID)" - ARCH_INPUT="${{ github.event.inputs.arch }}" - if [ -z "$ARCH_INPUT" ] || [ "$ARCH_INPUT" = "both" ]; then - ARCHS=(x64 arm64) - elif [ "$ARCH_INPUT" = "arm64" ]; then - ARCHS=(arm64) - elif [ "$ARCH_INPUT" = "x64" ]; then - ARCHS=(x64) - else - echo "Unknown arch input: $ARCH_INPUT" && exit 1 - fi - ELECTRON_VERSION=$(node -p "require('electron/package.json').version") - # Build each architecture separately to ensure native modules match. - # Building both at once with --x64 --arm64 would use the same node_modules/ - # for both, causing architecture mismatches (issue #706). - for A in "${ARCHS[@]}"; do - echo "=== Building for $A ===" - echo "Rebuilding native modules for $A (Electron $ELECTRON_VERSION)" - npm_config_build_from_source=true pnpm exec electron-rebuild -f -a "$A" -v "$ELECTRON_VERSION" -o sqlite3,node-pty,keytar - echo "Packaging $A DMG and ZIP" - pnpm exec electron-builder --mac dmg zip --$A --publish never --config.npmRebuild=false - done - - - name: Verify native module architectures match Electron - run: | - set -euo pipefail - # Verify that each build has native modules matching its Electron binary architecture. - # This catches issues like #706 where x64 builds had arm64 native modules. - VERIFIED_COUNT=0 - for APP_DIR in release/mac-*/emdash.app; do - [ -d "$APP_DIR" ] || continue - ARCH_DIR=$(basename "$(dirname "$APP_DIR")") - case "$ARCH_DIR" in - mac-arm64) EXPECTED_ARCH="arm64" ;; - mac-x64|mac) EXPECTED_ARCH="x86_64" ;; - *) echo "Unknown arch dir: $ARCH_DIR"; continue ;; - esac - echo "=== Checking $APP_DIR (expected: $EXPECTED_ARCH) ===" - ELECTRON_BIN="$APP_DIR/Contents/MacOS/emdash" - SQLITE_NODE="$APP_DIR/Contents/Resources/app.asar.unpacked/node_modules/sqlite3/build/Release/node_sqlite3.node" - # Check Electron binary architecture - ELECTRON_ARCH=$(file "$ELECTRON_BIN" | grep -o 'arm64\|x86_64' | head -1) - echo "Electron binary: $ELECTRON_ARCH" - if [ "$ELECTRON_ARCH" != "$EXPECTED_ARCH" ]; then - echo "::error::Electron arch mismatch: got $ELECTRON_ARCH, expected $EXPECTED_ARCH" - exit 1 - fi - # Check sqlite3 native module architecture - if [ -f "$SQLITE_NODE" ]; then - SQLITE_ARCH=$(file "$SQLITE_NODE" | grep -o 'arm64\|x86_64' | head -1) - echo "sqlite3 native module: $SQLITE_ARCH" - if [ "$SQLITE_ARCH" != "$EXPECTED_ARCH" ]; then - echo "::error::sqlite3 arch mismatch: got $SQLITE_ARCH, expected $EXPECTED_ARCH" - exit 1 - fi - else - echo "::warning::sqlite3 native module not found at $SQLITE_NODE" - fi - VERIFIED_COUNT=$((VERIFIED_COUNT + 1)) - done - if [ "$VERIFIED_COUNT" -eq 0 ]; then - echo "::error::No app bundles found to verify" - exit 1 - fi - echo "Verified $VERIFIED_COUNT app bundle(s)" + run: > + node --experimental-strip-types scripts/release/build.ts + --platform mac --arch ${{ github.event.inputs.arch || 'both' }} --targets dmg,zip - - name: Smoke test sqlite3 in packaged app (arm64, signed) - if: ${{ github.event.inputs.arch == '' || github.event.inputs.arch == 'arm64' || github.event.inputs.arch == 'both' }} - run: | - set -euo pipefail - APP="release/mac-arm64/emdash.app" - if [ -d "$APP" ]; then - echo "Requiring sqlite3 using packaged Electron (arm64, signed)…" - NODE_PATH="$APP/Contents/Resources/app.asar.unpacked/node_modules" \ - ELECTRON_RUN_AS_NODE=1 "$APP/Contents/MacOS/emdash" -e "require('sqlite3'); console.log('sqlite3 OK')" - else - echo "Arm64 app not found at $APP" && exit 1 - fi + - name: Verify macOS bundle + run: > + node --experimental-strip-types scripts/release/verify-mac.ts + --expected-team-id ${{ secrets.APPLE_TEAM_ID }} - - name: Verify Developer ID signing - env: - EXPECT_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - set -euo pipefail - FAILED=0 - for APP in release/mac*/emdash.app; do - if [ -d "$APP" ]; then - echo "Checking codesign for $APP" - META=$(codesign -dv --verbose=4 "$APP" 2>&1) - echo "$META" | grep -q "Authority=Developer ID Application" || { echo "::error::Not Developer ID Application signed"; FAILED=1; } - if [ -n "${EXPECT_TEAM_ID:-}" ]; then - TID=$(printf "%s\n" "$META" | awk -F= '/TeamIdentifier=/{print $2; exit}') - if [ "$TID" != "$EXPECT_TEAM_ID" ]; then - echo "::error::TeamIdentifier mismatch (got '$TID', expected '$EXPECT_TEAM_ID')"; FAILED=1 - fi - fi - fi - done - [ "$FAILED" -eq 0 ] || exit 1 - - - name: Verify bundle metadata and integrity (no app notarization required) - run: | - set -euo pipefail - for APP in release/mac*/emdash.app; do - if [ -d "$APP" ]; then - echo "Asserting bundle ID and resources for $APP" - PLIST="$APP/Contents/Info.plist" - if [ ! -f "$PLIST" ]; then - echo "::error::Missing Info.plist at $PLIST"; exit 1 - fi - # Read CFBundleIdentifier robustly (defaults can be flaky on raw files) - BID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$PLIST" 2>/dev/null || true) - if [ -z "$BID" ]; then - BID=$(plutil -extract CFBundleIdentifier xml1 -o - "$PLIST" 2>/dev/null | sed -n 's/.*\(.*\)<\/string>.*/\1/p' | head -n1) - fi - if [ "$BID" != "com.emdash" ]; then - echo "::error::CFBundleIdentifier mismatch (got '$BID', expected 'com.emdash')"; exit 1 - fi - # Ensure packaged resources exist (prevents white-screen adhoc shells) - if [ ! -f "$APP/Contents/Resources/app.asar" ] && [ ! -d "$APP/Contents/Resources/app" ]; then - echo "::error::Missing packaged renderer resources (app.asar or Resources/app)"; exit 1 - fi - echo "codesign --verify --deep --strict" - codesign --verify --deep --strict --verbose=2 "$APP" - echo "Skip spctl on raw app (validated later via DMG end-to-end check)" - fi - done - - - name: Staple and validate artifacts (best-effort for app) - run: | - set -euo pipefail - # Staple apps - for APP in release/mac*/emdash.app; do - if [ -d "$APP" ]; then - echo "Attempt staple $APP (may be not notarized)"; xcrun stapler staple "$APP" || echo "App staple skipped" - echo "Validate $APP (best-effort)"; xcrun stapler validate "$APP" || echo "App validate skipped" - fi - done - # Staple DMGs - for DMG in release/*.dmg; do - if [ -f "$DMG" ]; then - echo "Stapling $DMG (best-effort; only app must be stapled)" - if ! xcrun stapler staple "$DMG"; then - echo "Warning: DMG notarization ticket not found (expected if only app was notarized). Skipping." - else - xcrun stapler validate "$DMG" || echo "Warning: DMG validate failed; app is stapled and validated." - fi - fi - done - - - name: Notarize and staple DMGs (ensures user-downloadable DMG works) - if: ${{ steps.flags.outputs.has_apple_api == 'true' }} - env: - APPLE_API_KEY: ${{ env.APPLE_API_KEY }} - APPLE_API_KEY_ID: ${{ env.APPLE_API_KEY_ID }} - APPLE_API_ISSUER: ${{ env.APPLE_API_ISSUER }} - run: | - set -euo pipefail - for DMG in release/*.dmg; do - [ -f "$DMG" ] || continue - echo "Submitting $DMG to Apple Notary (notarytool)..." - xcrun notarytool submit "$DMG" \ - --key "$APPLE_API_KEY" \ - --key-id "$APPLE_API_KEY_ID" \ - --issuer "$APPLE_API_ISSUER" \ - --wait - echo "Stapling $DMG after notarization" - xcrun stapler staple -v "$DMG" - xcrun stapler validate "$DMG" - done - - - name: 'End-to-end check: app inside DMG passes Gatekeeper' - run: | - set -euo pipefail - for DMG in release/*.dmg; do - [ -f "$DMG" ] || continue - MNT=$(mktemp -d) - echo "Mounting $DMG at $MNT"; hdiutil attach "$DMG" -mountpoint "$MNT" -nobrowse -quiet - APP="$MNT/emdash.app" - if [ ! -d "$APP" ]; then - echo "::error::No emdash.app found inside $DMG"; hdiutil detach "$MNT" -quiet || true; rm -rf "$MNT"; exit 1 - fi - echo "codesign info for app inside DMG:"; codesign -dv --verbose=4 "$APP" 2>&1 | sed -n '1,60p' - echo "Gatekeeper assessment (should be accepted):"; spctl -a -vv --type execute "$APP" - hdiutil detach "$MNT" -quiet || true; rm -rf "$MNT" - done - - - name: Publish GitHub Release and upload artifacts + - name: Notarize and staple if: ${{ steps.init.outputs.dry_run != 'true' }} env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - TAG="${GITHUB_REF_NAME}" - # Create if missing, or publish if draft - if gh release view "$TAG" >/dev/null 2>&1; then - gh release edit "$TAG" --draft=false --prerelease=false - else - gh release create "$TAG" --title "$TAG" --generate-notes --latest || gh release edit "$TAG" --draft=false --prerelease=false - fi - # Upload stapled DMGs, ZIPs (for auto-update), and update files - FILES=(release/emdash-*.dmg release/emdash-*.zip) - if ls release/*.blockmap >/dev/null 2>&1; then FILES+=(release/*.blockmap); fi - if [ -f release/latest-mac.yml ]; then FILES+=(release/latest-mac.yml); fi - gh release upload "$TAG" "${FILES[@]}" --clobber + APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: node --experimental-strip-types scripts/release/notarize-mac.ts - - name: Inspect built artifacts (dry-run) - if: ${{ steps.init.outputs.dry_run == 'true' }} - run: | - set -euo pipefail - echo "Contents of release/:" - ls -lah release || true - echo "Find DMGs:" - find release -maxdepth 1 -type f -name 'emdash-*.dmg' -print - DMG_COUNT=$(find release -maxdepth 1 -type f -name 'emdash-*.dmg' | wc -l | tr -d ' ') - if [ "$DMG_COUNT" -eq 0 ]; then - echo "::error::No DMG files found in release/."; exit 1 - fi + - name: Upload to R2 + if: ${{ steps.init.outputs.dry_run != 'true' }} + env: + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_BUCKET: ${{ secrets.R2_BUCKET }} + run: node --experimental-strip-types scripts/release/upload-r2.ts - - name: Upload signed DMGs (dry-run) + - name: Upload artifacts (dry-run) if: ${{ steps.init.outputs.dry_run == 'true' }} uses: actions/upload-artifact@v4 with: name: SIGNED-STAPLED-DMGS - path: release/emdash-*.dmg - if-no-files-found: error - - - name: Upload supplemental files (blockmaps/yml) (dry-run) - if: ${{ steps.init.outputs.dry_run == 'true' }} - uses: actions/upload-artifact@v4 - with: - name: SIGNED-STAPLED-DMGS-extras path: | + release/emdash-*.dmg + release/emdash-*.zip release/*.blockmap - release/latest-mac.yml - if-no-files-found: ignore + release/v1-stable*.yml + if-no-files-found: error diff --git a/.github/workflows/windows-beta-build.yml b/.github/workflows/windows-beta-build.yml index a6aeb8d12b..fcd374c852 100644 --- a/.github/workflows/windows-beta-build.yml +++ b/.github/workflows/windows-beta-build.yml @@ -48,9 +48,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - # Node 22 can trigger native module source builds that are flakier on Windows. - # Node 20 is within engines range and closer to Electron's Node baseline. - node-version: '20' + node-version: '24' cache: 'pnpm' - name: Setup Python 3.11 for node-gyp @@ -81,7 +79,7 @@ jobs: run: | set -euo pipefail ELECTRON_VERSION=$(node -p "require('electron/package.json').version") - npm_config_build_from_source=true pnpm exec electron-rebuild -f -a x64 -v "$ELECTRON_VERSION" -o sqlite3,node-pty,keytar + npm_config_build_from_source=true pnpm exec electron-rebuild -f -a x64 -v "$ELECTRON_VERSION" -o sqlite3,node-pty - name: Package Windows (NSIS + MSI) (no publish) shell: bash diff --git a/.gitignore b/.gitignore index 8d9e1a2ad5..ff7dafab98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # Dependencies node_modules/ dist/ -release/ +out/ +/release/ # Logs *.log @@ -59,9 +60,6 @@ Thumbs.db .notes .claude/ -# Local generated worktrees for perf experiments -worktrees/ - # Checkouts directory .checkouts/ @@ -69,3 +67,6 @@ worktrees/ .env .cursor +.codex/config.toml + +src/main/appConfig.json \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 442c7587a9..d845d9d88d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.20.0 +24.14.0 diff --git a/.prettierignore b/.prettierignore index 227943b019..09dada1cb4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ **/.next **/build **/.turbo +**/out # ignore config files that shouldn't be formatted **/.env* diff --git a/.prettierrc b/.prettierrc index cb35617fac..3453c182a0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,5 +6,15 @@ "tabWidth": 2, "bracketSpacing": true, "bracketSameLine": false, - "plugins": ["prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-tailwindcss", "@ianvs/prettier-plugin-sort-imports"], + "tailwindStylesheet": "./src/renderer/index.css", + "importOrder": [ + "^@/(.*)$", + "^@root/(.*)$", + "^@shared/(.*)$", + "^@main/(.*)$", + "^@renderer/(.*)$", + "^[./]" + ], + "importOrderTypeScriptVersion": "5.0.0" } diff --git a/AGENTS.md b/AGENTS.md index 4a99a3a78f..5fd350c342 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,15 +1,16 @@ + --- default_branch: main package_manager: pnpm -node_version: "22.20.0" +node_version: "24.x.x" start_command: "pnpm run d" dev_command: "pnpm run dev" build_command: "pnpm run build" test_commands: - "pnpm run format" - "pnpm run lint" - - "pnpm run type-check" - - "pnpm exec vitest run" + - "pnpm run typecheck" + - "pnpm run test" ports: dev: 3000 required_env: [] @@ -23,329 +24,74 @@ optional_env: - CODEX_APPROVAL_POLICY --- -# Emdash - -Cross-platform Electron app that orchestrates multiple CLI coding agents (Claude Code, Codex, Qwen Code, Amp, etc.) in parallel. Each agent runs in its own Git worktree for isolation. Also supports remote development over SSH. - -### Tech Stack - -- **Runtime**: Electron 30.5.1, Node.js >=20.0.0 <23.0.0 (recommended: 22.20.0 via `.nvmrc`) -- **Frontend**: React 18, TypeScript 5.3, Vite 5, Tailwind CSS 3 -- **Backend**: Node.js, TypeScript, Drizzle ORM 0.32, SQLite3 5.1 -- **Editor**: Monaco Editor 0.55, **Terminal**: @xterm/xterm 6.0 + node-pty 1.0 -- **Native Modules**: node-pty, sqlite3, keytar 7.9 (require `pnpm run rebuild` after updates) -- **SSH**: ssh2 1.17 -- **UI**: Radix UI primitives, lucide-react icons, framer-motion - -## Quickstart - -1. `nvm use` (installs Node 22.20.0 if missing) or install Node 22.x manually. -2. `pnpm run d` to install dependencies and launch Electron + Vite. -3. If `pnpm run d` fails mid-stream, rerun `pnpm install`, then `pnpm run dev` (main + renderer). - -## Development Commands - -```bash -# Quick start (installs deps, starts dev) -pnpm run d - -# Development (runs main + renderer concurrently) -pnpm run dev -pnpm run dev:main # Electron main process only (tsc + electron) -pnpm run dev:renderer # Vite dev server only (port 3000) - -# Quality checks (ALWAYS run before committing) -pnpm run format # Format with Prettier -pnpm run lint # ESLint -pnpm run type-check # TypeScript type checking (uses tsconfig.json — renderer/shared/types) -pnpm exec vitest run # Run all tests - -# Run a specific test -pnpm exec vitest run src/test/main/WorktreeService.test.ts - -# Native modules -pnpm run rebuild # Rebuild native modules for Electron -pnpm run reset # Clean install (removes node_modules, reinstalls) - -# Building & Packaging -pnpm run build # Build main + renderer -pnpm run package:mac # macOS .dmg (arm64) -pnpm run package:linux # Linux AppImage/deb (x64) -pnpm run package:win # Windows nsis/portable (x64) -``` - -## Testing - -Tests use `vi.mock()` to stub `electron`, `DatabaseService`, `logger`, etc. Integration tests create real git repos in `os.tmpdir()`. No shared test setup file — mocks are per-file. - -- **Framework**: Vitest (configured in `vite.config.ts`, `environment: 'node'`) -- **Test locations**: `src/test/main/` (15 service tests), `src/test/renderer/` (3 UI tests), `src/main/utils/__tests__/` (2 utility tests) - -## Guardrails - -- **ALWAYS** run `pnpm run format`, `pnpm run lint`, `pnpm run type-check`, and `pnpm exec vitest run` before committing. -- **NEVER** modify `drizzle/meta/` or numbered migration files — always use `drizzle-kit generate`. -- **NEVER** modify `build/` entitlements or updater config without review. -- **ALWAYS** use feature branches or worktrees; never commit directly to `main`. -- Do limit edits to `src/**`, `docs/**`, or config files you fully understand; keep `dist/`, `release/`, and `build/` untouched. -- Don't modify telemetry defaults or updater logic unless intentional and reviewed. -- Don't run commands that mutate global environments (global package installs, git pushes) from agent scripts. -- Put temporary notes or scratch content in `.notes/` (gitignored). - -## Architecture - -### Process Model - -- **Main process** (`src/main/`): Electron main — IPC handlers, services, database, PTY management -- **Renderer process** (`src/renderer/`): React UI built with Vite — components, hooks, terminal panes -- **Shared** (`src/shared/`): Provider registry (21 agent definitions), PTY ID helpers, shared utilities - -### Boot Sequence - -`entry.ts` → `main.ts` → IPC registration → window creation - -- `entry.ts` — Sets app name (must happen before `app.getPath('userData')`, or Electron defaults to `~/Library/Application Support/Electron`). Monkey-patches `Module._resolveFilename` to resolve `@shared/*` and `@/*` path aliases at runtime in compiled JS. -- `main.ts` — Loads `.env`, fixes PATH for CLI discovery on macOS/Linux/Windows (adds Homebrew, npm global, nvm paths so agents like `gh`, `codex`, `claude` are found when launched from Finder), detects `SSH_AUTH_SOCK` from user's login shell, then initializes Electron windows and registers all IPC handlers. -- `preload.ts` — Exposes secure `electronAPI` to renderer via `contextBridge`. - -### Main Process (`src/main/`) - -**Key services** (`src/main/services/`): -- `WorktreeService.ts` — Git worktree lifecycle, file preservation patterns -- `WorktreePoolService.ts` — Worktree pooling/reuse for instant task starts -- `DatabaseService.ts` — All SQLite CRUD operations -- `ptyManager.ts` — PTY (pseudo-terminal) lifecycle, session isolation, agent spawning -- `SkillsService.ts` — Cross-agent skill installation and catalog management -- `GitHubService.ts` / `GitService.ts` — Git and GitHub operations via `gh` CLI -- `PrGenerationService.ts` — Automated PR generation -- `TaskLifecycleService.ts` — Task lifecycle orchestration -- `TerminalSnapshotService.ts` — Terminal state snapshots -- `TerminalConfigParser.ts` — Terminal configuration parsing -- `RepositoryManager.ts` — Repository management -- `RemotePtyService.ts` / `RemoteGitService.ts` — Remote development over SSH -- `ssh/` — SSH connection management, credentials (via keytar), host key verification - -Note: Some IPC handler files are colocated in `services/` (e.g., `worktreeIpc.ts`, `ptyIpc.ts`, `updateIpc.ts`, `lifecycleIpc.ts`, `planLockIpc.ts`, `fsIpc.ts`). - -**IPC Handlers** (`src/main/ipc/`): -- 25+ handler files total (19 in `ipc/` + 6 colocated in `services/`) covering app, db, git, github, browser, connections, project, settings, telemetry, SSH, Linear, Jira, skills, and more -- All return `{ success: boolean, data?: any, error?: string }` format -- Types defined in `src/renderer/types/electron-api.d.ts` (~1,870 lines) - -**Database** (`src/main/db/`): -- Schema: `schema.ts` — Migrations: `drizzle/` (auto-generated) -- Locations: macOS `~/Library/Application Support/emdash/emdash.db`, Linux `~/.config/emdash/emdash.db`, Windows `%APPDATA%\emdash\emdash.db` -- Override with `EMDASH_DB_FILE` env var - -### Renderer Process (`src/renderer/`) - -**Key components** (`components/`): -- `App.tsx` — Root orchestration (~790 lines), located at `src/renderer/App.tsx` -- `EditorMode.tsx` — Monaco code editor -- `ChatInterface.tsx` — Conversation UI -- `FileChangesPanel.tsx` / `ChangesDiffModal.tsx` — Diff visualization and review -- `CommandPalette.tsx` — Command/action palette -- `FileExplorer/` — File tree navigation -- `BrowserPane.tsx` — Webview preview -- `skills/` — Skills catalog and management UI -- `ssh/` — SSH connection UI components - -**Key hooks** (`hooks/`, 42 total): -- `useAppInitialization` — Two-round project/task loading (fast skeleton then full), restores last active project/task from localStorage -- `useTaskManagement` — Full task lifecycle (~864 lines): create, delete, rename, archive, restore. Handles optimistic UI removal with rollback, lifecycle teardown, PTY cleanup -- `useCliAgentDetection` — Detects which CLI agents are installed on the system -- `useInitialPromptInjection` / `usePendingInjection` — Manages initial prompt sent to agents on task start - -### Path Aliases - -**Important**: `@/*` resolves differently in main vs renderer: - -| Alias | tsconfig.json (renderer) | tsconfig.main.json (main) | -|-------|-------------------------|--------------------------| -| `@/*` | `src/renderer/*` | `src/*` | -| `@shared/*` | `src/shared/*` | `src/shared/*` | -| `#types/*` | `src/types/*` | _(not available)_ | -| `#types` | `src/types/index.ts` | _(not available)_ | - -At runtime in compiled main process, `entry.ts` monkey-patches `Module._resolveFilename` to map `@shared/*` → `dist/main/shared/*` and `@/*` → `dist/main/main/*`. - -Main uses `module: "CommonJS"` (required by Electron), renderer uses `module: "ESNext"` (Vite handles compilation). - -### IPC Pattern - -```typescript -// Main (src/main/ipc/exampleIpc.ts) -ipcMain.handle('example:action', async (_event, args) => { - try { - return { success: true, data: await service.doSomething(args) }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// Renderer — call via window.electronAPI -const result = await window.electronAPI.exampleAction({ id: '123' }); -``` - -All new IPC methods must be declared in `src/renderer/types/electron-api.d.ts`. - -### Services - -Singleton classes with module-level export: -```typescript -export class ExampleService { /* ... */ } -export const exampleService = new ExampleService(); -``` - -## Provider Registry (`src/shared/providers/registry.ts`) - -All 21 CLI agents are defined as `ProviderDefinition` objects. Key fields: - -- `cli` — binary name, `commands` — detection commands (may differ from cli) -- `autoApproveFlag` — e.g. `--dangerously-skip-permissions` for Claude -- `initialPromptFlag` — how to pass the initial prompt (`-i`, positional, etc.) -- `useKeystrokeInjection` — `true` for agents with no CLI prompt flag (Amp, OpenCode); Emdash types the prompt into the TUI after startup -- `sessionIdFlag` — only Claude; enables multi-chat session isolation via `--session-id` -- `resumeFlag` — e.g. `-c -r` for Claude, `--continue` for Kilocode - -To add a new provider: add a definition here AND add any API key to the `AGENT_ENV_VARS` list in `ptyManager.ts`. - -## PTY Management (`src/main/services/ptyManager.ts`) - -Three spawn modes: -1. **`startPty()`** — Shell-based: `{cli} {args}; exec {shell} -il` (user gets a shell after agent exits) -2. **`startDirectPty()`** — Direct spawn without shell wrapper using cached CLI path. Faster. Falls back to `startPty` when CLI path isn't cached or `shellSetup` is configured. -3. **`startSshPty()`** — Wraps `ssh -tt {target}` for remote development. - -**Session isolation**: For Claude, generates a deterministic UUID from task/conversation ID for `--session-id`/`--resume`. Session map persisted to `{userData}/pty-session-map.json`. - -**PTY ID format** (`src/shared/ptyId.ts`): `{providerId}-main-{taskId}` or `{providerId}-chat-{conversationId}`. - -**Environment**: PTYs use a minimal env (not `process.env`). The `AGENT_ENV_VARS` list in `ptyManager.ts` is the definitive passthrough list for API keys. Data is flushed over IPC every 16ms. - -## Worktree System - -**WorktreeService** (`src/main/services/WorktreeService.ts`): -- Creates worktrees at `../worktrees/{slugged-name}-{3-char-hash}` on branch `{prefix}/{slugged-name}-{hash}` -- Branch prefix defaults to `emdash`, configurable in settings -- Preserves gitignored files (`.env`, `.envrc`, etc.) from main repo to worktree -- Custom preserve patterns via `.emdash.json` at project root: `{ "preservePatterns": [".claude/**"] }` - -**WorktreePoolService** (`src/main/services/WorktreePoolService.ts`): -Eliminates 3-7s worktree creation delay: -1. Pre-creates a `_reserve/{hash}` worktree in the background on project open -2. On task creation, instant `git worktree move` + `git branch -m` rename -3. Replenishes reserve in background after claiming -4. Reserves expire after 30 minutes; orphaned reserves cleaned on startup - -## Multi-Chat Conversations - -Tasks can have multiple conversation tabs, each with their own provider and PTY. Database `conversations` table tracks `isMain`, `provider`, `displayOrder`. For Claude, each conversation gets its own session UUID. - -## Skills System - -Implements the [Agent Skills](https://agentskills.io) standard — cross-agent reusable skill packages (`SKILL.md` with YAML frontmatter). - -- **Central storage**: `~/.agentskills/{skill-name}/`, metadata in `~/.agentskills/.emdash/` -- **Agent sync**: Symlinks from central storage into each agent's native directory (`~/.claude/commands/`, `~/.codex/skills/`, etc.) -- **Aggregated catalog**: Merges from OpenAI repo, Anthropic repo, and local user-created skills -- **Key files**: `src/shared/skills/` (types, validation, agent targets), `src/main/services/SkillsService.ts` (core logic), `src/main/ipc/skillsIpc.ts`, `src/renderer/components/skills/`, `src/main/services/skills/bundled-catalog.json` (offline fallback) - -## SSH Remote Development - -Orchestrates agents on remote machines over SSH. - -- **Connections**: Password, key, or agent auth. Credentials stored via `keytar` in OS keychain. -- **Remote worktrees**: Created at `/.emdash/worktrees//` on the server -- **Remote PTY**: Agent shells via `ssh2`'s shell API, streaming to UI in real-time -- **Key files**: `src/main/services/ssh/` (SshService, SshCredentialService, SshHostKeyService), `src/main/services/RemotePtyService.ts`, `src/main/services/RemoteGitService.ts`, `src/main/utils/shellEscape.ts` - -**Local-only (not yet remote)**: file diffs, file watching, branch push, worktree pooling, GitHub/PR features. - -**Security**: Shell args escaped via `quoteShellArg()` from `src/main/utils/shellEscape.ts`. Env var keys validated against `^[A-Za-z_][A-Za-z0-9_]*$`. Remote PTY restricted to allowlisted shell binaries. File access gated by `isPathSafe()`. - -## Database & Migrations - -- Schema in `src/main/db/schema.ts` → `pnpm exec drizzle-kit generate` to create migrations -- Browse: `pnpm exec drizzle-kit studio` -- Locations: macOS `~/Library/Application Support/emdash/emdash.db`, Linux `~/.config/emdash/emdash.db`, Windows `%APPDATA%\emdash\emdash.db` -- **NEVER** manually edit files in `drizzle/meta/` or numbered SQL migrations - -## Code Style - -- **TypeScript**: Strict mode enabled in both tsconfigs. Prefer explicit types over `any`. Type imports: `import type { Foo } from './bar'` -- **React**: Functional components with hooks. Both named and default exports are used. -- **File naming**: Components PascalCase (`FileExplorer.tsx`), hooks/utilities camelCase with `use` prefix (`useTaskManagement.ts`) or kebab-case (`use-toast.ts`). Tests: `*.test.ts` -- **Error handling**: Main → `log.error()` from `../lib/logger`, Renderer → `console.error()` or toast, IPC → `{ success: false, error }` -- **Styling**: Tailwind CSS classes - -## Project Configuration - -- **`.emdash.json`** at project root: `{ "preservePatterns": [".claude/**"] }` — controls which gitignored files are copied to worktrees. Also supports `shellSetup` for lifecycle scripts. -- **Branch prefix**: Configurable via app settings (`repository.branchPrefix`), defaults to `emdash` - -## Environment Variables - -All optional: -- `EMDASH_DB_FILE` — Override database file path -- `EMDASH_DISABLE_NATIVE_DB` — Disable native SQLite driver -- `EMDASH_DISABLE_CLONE_CACHE` — Disable clone caching -- `EMDASH_DISABLE_PTY` — Disable PTY support (used in tests) -- `TELEMETRY_ENABLED` — Toggle anonymous telemetry (PostHog) -- `CODEX_SANDBOX_MODE` / `CODEX_APPROVAL_POLICY` — Codex agent configuration - -## Hot Reload - -- **Renderer changes**: Hot-reload via Vite -- **Main process changes**: Require Electron restart (Ctrl+C → `pnpm run dev`) -- **Native modules**: Require `pnpm run rebuild` - -## CI/CD - -- **`code-consistency-check.yml`** (every PR): format check, type check, vitest (workflow name: "CI Check") -- **`release.yml`** (on `v*` tags): per-platform builds. Mac builds each arch separately to prevent native module architecture mismatches. Mac release includes signing + notarization. - -## Common Pitfalls - -1. **PTY resize after exit**: PTYs must be cleaned up on exit. Use `removePty()` in exit handlers. -2. **Worktree path resolution**: Always resolve paths from `WorktreeService`, not manually. -3. **IPC type safety**: Define all new IPC methods in `electron-api.d.ts`. -4. **Native module issues**: After updating node-pty/sqlite3/keytar, run `pnpm run rebuild`. Last resort: `pnpm run reset`. -5. **Monaco disposal**: Editor instances must be disposed to prevent memory leaks. -6. **CLI not found in agent**: If agents can't find `gh`, `codex`, etc., the PATH setup in `main.ts` may need updating for the platform. -7. **New provider integration**: Must add to registry in `src/shared/providers/registry.ts` AND add any API key to `AGENT_ENV_VARS` in `ptyManager.ts`. -8. **SSH shell injection**: All remote shell arguments must use `quoteShellArg()` from `src/main/utils/shellEscape.ts`. - -## Risky Areas - -- `src/main/db/**` + `drizzle/` — Schema migrations; mismatches can corrupt user data. -- `build/` entitlements and updater config — Incorrect changes break signing/auto-update. -- Native dependencies (`sqlite3`, `node-pty`, `keytar`) — Rebuilding is slow; avoid upgrading casually. -- PTY/terminal management — Race conditions or unhandled exits can kill agent runs. -- SSH services (`src/main/services/ssh/**`, `src/main/utils/shellEscape.ts`) — Security-critical: remote connections, credentials, shell command construction. - -## Git Workflow - -- Worktrees: `../worktrees/{workspace-name}-{hash}`, agents run there -- Conventional commits: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:`, `test:` -- Example: `fix(agent): resolve worktree path issue (#123)` - -## Key Configuration Files - -- `vite.config.ts` — Renderer build + Vitest test config -- `drizzle.config.ts` — Database migration config (supports `EMDASH_DB_FILE` override) -- `tsconfig.json` — Renderer/shared TypeScript config (`module: ESNext`, `noEmit: true` — Vite does compilation) -- `tsconfig.main.json` — Main process TypeScript config (`module: CommonJS` — required by Electron main) -- `tailwind.config.js` — Tailwind configuration -- `.nvmrc` — Node version (22.20.0) -- Electron Builder config is in `package.json` under `"build"` key - -## Pre-PR Checklist - -- [ ] Dev server runs: `pnpm run d` (or `pnpm run dev`) starts cleanly. -- [ ] Code is formatted: `pnpm run format`. -- [ ] Lint passes: `pnpm run lint`. -- [ ] Types check: `pnpm run type-check`. -- [ ] Tests pass: `pnpm exec vitest run`. -- [ ] No stray build artifacts or secrets committed. -- [ ] Documented any schema or config changes impacting users. +# Emdash Agent Guide + +Start here. Load only the linked `agents/` docs that are relevant to the task. + +## Start Here + +- Repo map: `agents/README.md` +- Setup and commands: `agents/quickstart.md` +- System overview: `agents/architecture/overview.md` +- Validation flow: `agents/workflows/testing.md` + +## Read By Task + +- Main-process changes: `agents/architecture/main-process.md` +- Renderer/UI changes: `agents/architecture/renderer.md` +- Shared types or provider metadata: `agents/architecture/shared.md` +- Worktree behavior or `.emdash.json`: `agents/workflows/worktrees.md` +- SSH or remote project work: `agents/workflows/remote-development.md` +- Provider integration or CLI behavior: `agents/integrations/providers.md` +- MCP changes: `agents/integrations/mcp.md` + +## High-Risk Areas + +- Database and migrations: `agents/risky-areas/database.md` +- PTY/session orchestration: `agents/risky-areas/pty.md` +- SSH and shell escaping: `agents/risky-areas/ssh.md` +- Auto-update and packaging: `agents/risky-areas/updater.md` + +## Conventions + +- IPC contract and typing: `agents/conventions/ipc.md` +- Main process patterns (controllers, services, Result type, events): `agents/conventions/main-patterns.md` +- Renderer patterns (modals, views, PTY frontend, React Query contexts): `agents/conventions/renderer-patterns.md` +- TypeScript and React norms: `agents/conventions/typescript.md` +- Config files and repo rules: `agents/conventions/config-files.md` +- Never do re exports always import from the original source + +### State Guard Conventions (renderer stores) + +`ProjectStore` and `TaskStore` are mutable MobX class instances that transition through states. Use the following layers — do not mix them: + +**Selectors** (`task-selectors.ts`, `project-selectors.ts`) — pure functions, safe in observer components, effects, and event handlers: +- `getTaskStore(projectId, taskId)` → `TaskStore | undefined` +- `asProvisioned(store)` → `ProvisionedTask | undefined` (use with explicit null check, never `!`) +- `taskViewKind(store, projectId)` → `TaskViewKind` +- `getTaskManagerStore(projectId)` → `TaskManagerStore | undefined` (use this instead of reaching through project store) +- `getProjectStore(projectId)` → `ProjectStore | undefined` +- `asMounted(store)` → `MountedProject | undefined` (use with explicit null check, never `!`) + +**Hooks** (`task-view-context.tsx`) — for `observer` components inside the task view tree: +- `useTaskViewKind()` — routing/state-gating +- `useProvisionedTask()` → `ProvisionedTask | null` — when the component handles a non-provisioned state +- `useRequireProvisionedTask()` → `ProvisionedTask` — when the component must only render when provisioned (throws with a descriptive error if the invariant is violated) + +**Rules:** +- Never `asProvisioned(...)!` or `asMounted(...)!` — use the hook or an explicit null check +- State guards must use `kind !== 'ready'`, never enumerate non-ready states (new states would silently fall through) +- Access task manager via `getTaskManagerStore(projectId)`, not through `project.taskManager` +- Access mounted project via `asMounted(getProjectStore(id))`, not via inline `isMountedProject` guards + +## Non-Negotiables + +- Run `pnpm run format`, `pnpm run lint`, `pnpm run typecheck`, and `pnpm test` before merging. +- Do not hand-edit numbered Drizzle migrations or `drizzle/meta/`. +- New RPC methods go in the appropriate `src/main/core/*/controller.ts` and are auto-registered via `src/main/rpc.ts`. +- Only use manual IPC in `electron-api.d.ts` for methods requiring `event.sender`. +- New modals must be registered in `src/renderer/core/modal/registry.ts`. +- New views must be registered in `src/renderer/core/view/registry.ts`. +- Treat `src/main/core/pty/`, `src/main/core/ssh/`, `src/main/db/`, and updater code as high risk. +- Avoid editing `dist/`, `release/`, and `build/` unless the task is explicitly about packaging or updater/signing behavior. +- The docs app in `docs/` is separate from the Electron renderer and also defaults to port `3000`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30bd3d68f1..96e9dae4af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thanks for your interest in contributing! We favor small, focused PRs and clear Prerequisites -- **Node.js 20.0.0+ (recommended: 22.20.0)** and Git +- **Node.js 24.0.0+ (recommended: 24.14.0)** and Git - Optional (recommended for end‑to‑end testing): - GitHub CLI (`brew install gh`; then `gh auth login`) - At least one supported coding agent CLI (see docs for list) @@ -29,7 +29,7 @@ pnpm install pnpm run dev # Type checking, lint, build - pnpm run type-check + pnpm run typecheck pnpm run lint pnpm run build ``` @@ -62,7 +62,7 @@ Tip: During development, the renderer hot‑reloads. Changes to the Electron mai ``` pnpm run format # Format code with Prettier (required) -pnpm run type-check # TypeScript type checking +pnpm run typecheck # TypeScript type checking pnpm run lint # ESLint pnpm run build # Build both main and renderer ``` @@ -98,7 +98,7 @@ TypeScript + ESLint + Prettier Pre-commit hooks handle formatting and linting automatically on staged files. For full-project checks you can run them manually: - `pnpm run format` -- format all files with Prettier -- `pnpm run type-check` -- TypeScript type checking (whole project) +- `pnpm run typecheck` -- TypeScript type checking (whole project) - `pnpm run lint` -- ESLint across all files - `pnpm exec vitest run` -- run the test suite diff --git a/README.md b/README.md index 94bf85887a..c19c640a4a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
-[![MIT License](https://img.shields.io/badge/License-MIT-555555.svg?labelColor=333333&color=666666)](./LICENSE.md) +[![Apache 2.0 License](https://img.shields.io/badge/License-Apache_2.0-555555.svg?labelColor=333333&color=666666)](./LICENSE.md) [![Downloads](https://img.shields.io/github/downloads/generalaction/emdash/total?labelColor=333333&color=666666)](https://github.com/generalaction/emdash/releases) [![GitHub Stars](https://img.shields.io/github/stars/generalaction/emdash?labelColor=333333&color=666666)](https://github.com/generalaction/emdash) [![Last Commit](https://img.shields.io/github/last-commit/generalaction/emdash?labelColor=333333&color=666666)](https://github.com/generalaction/emdash/commits/main) @@ -17,23 +17,31 @@
- - Download for Windows + + Download for Windows - - Download for macOS + + Download for macOS - - Download for Linux + + Download for Linux +

+ Download Emdash v1 +

+

+ Stable v1 is now available for macOS, Windows, and Linux · + Read the launch post +

+
Emdash is a provider-agnostic desktop app that lets you run multiple coding agents in parallel, each isolated in its own git worktree, either locally or over SSH on a remote machine. We call it an Agentic Development Environment (ADE). -Emdash supports 22 (and growing) CLI agents, such as Claude Code, Qwen Code, Amp, and Codex. Users can directly pass Linear, GitHub, or Jira tickets to an agent, review diffs, test changes, create PRs, see CI/CD checks, and merge. +Emdash supports 23 CLI agents, including Claude Code, Qwen Code, Hermes Agent, Amp, and Codex. Users can directly pass Linear, GitHub, or Jira tickets to an agent, review diffs, test changes, create PRs, see CI/CD checks, and merge. **Develop on remote servers via SSH** @@ -45,7 +53,7 @@ Connect to remote machines via SSH/SFTP to work with remote codebases. Emdash su -Emdash product +Emdash product # Installation @@ -53,9 +61,6 @@ Connect to remote machines via SSH/SFTP to work with remote codebases. Emdash su - Apple Silicon: https://github.com/generalaction/emdash/releases/latest/download/emdash-arm64.dmg - Intel x64: https://github.com/generalaction/emdash/releases/latest/download/emdash-x64.dmg -[![Homebrew](https://img.shields.io/badge/-Homebrew-000000?style=for-the-badge&logo=homebrew&logoColor=FBB040)](https://formulae.brew.sh/cask/emdash) -> macOS users can also: `brew install --cask emdash` - ### Windows - Installer (x64): https://github.com/generalaction/emdash/releases/latest/download/emdash-x64.msi - Portable (x64): https://github.com/generalaction/emdash/releases/latest/download/emdash-x64.exe @@ -74,29 +79,30 @@ Connect to remote machines via SSH/SFTP to work with remote codebases. Emdash su ### Supported CLI Providers -Emdash currently supports twenty-two CLI providers and we are adding new providers regularly. If you miss one, let us know or create a PR. +Emdash currently supports 23 CLI providers, and we are adding new ones regularly. If you miss one, let us know or create a PR. | CLI Provider | Status | Install | | ----------- | ------ | ----------- | -| [Amp](https://ampcode.com/manual) | ✅ Supported | npm install -g @sourcegraph/amp@latest | +| [Amp](https://ampcode.com/manual#install) | ✅ Supported | npm install -g @sourcegraph/amp@latest | | [Auggie](https://docs.augmentcode.com/cli/overview) | ✅ Supported | npm install -g @augmentcode/auggie | | [Autohand Code](https://autohand.ai/code/) | ✅ Supported | npm install -g autohand-cli | | [Charm](https://github.com/charmbracelet/crush) | ✅ Supported | npm install -g @charmland/crush | | [Claude Code](https://docs.anthropic.com/claude/docs/claude-code) | ✅ Supported | curl -fsSL https://claude.ai/install.sh | bash | | [Cline](https://docs.cline.bot/cline-cli/overview) | ✅ Supported | npm install -g cline | | [Codebuff](https://www.codebuff.com/docs/help/quick-start) | ✅ Supported | npm install -g codebuff | -| [Codex](https://developers.openai.com/codex/cli/) | ✅ Supported | npm install -g @openai/codex | +| [Codex](https://github.com/openai/codex) | ✅ Supported | npm install -g @openai/codex | | [Continue](https://docs.continue.dev/guides/cli) | ✅ Supported | npm i -g @continuedev/cli | | [Cursor](https://cursor.com/cli) | ✅ Supported | curl https://cursor.com/install -fsS | bash | | [Droid](https://docs.factory.ai/cli/getting-started/quickstart) | ✅ Supported | curl -fsSL https://app.factory.ai/cli | sh | | [Gemini](https://github.com/google-gemini/gemini-cli) | ✅ Supported | npm install -g @google/gemini-cli | -| [GitHub Copilot](https://docs.github.com/en/copilot/how-tos/set-up/installing-github-copilot-in-the-cli) | ✅ Supported | npm install -g @github/copilot | -| [Goose](https://github.com/block/goose) | ✅ Supported | curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash | +| [GitHub Copilot](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) | ✅ Supported | npm install -g @github/copilot | +| [Goose](https://block.github.io/goose/docs/quickstart/) | ✅ Supported | curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash | +| [Hermes Agent](https://hermes-agent.nousresearch.com/docs/) | ✅ Supported | curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash | | [Kilocode](https://kilo.ai/docs/cli) | ✅ Supported | npm install -g @kilocode/cli | -| [Kimi](https://www.kimi.com/code/docs/en/kimi-cli/guides/getting-started.html) | ✅ Supported | uv tool install --python 3.13 kimi-cli | -| [Kiro](https://kiro.dev/docs/cli/) | ✅ Supported | curl -fsSL https://cli.kiro.dev/install | bash | +| [Kimi](https://www.kimi.com/code/docs/en/kimi-cli/guides/getting-started.html) | ✅ Supported | uv tool install kimi-cli | +| [Kiro (AWS)](https://kiro.dev/docs/cli/) | ✅ Supported | curl -fsSL https://cli.kiro.dev/install | bash | | [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ Supported | curl -LsSf https://mistral.ai/vibe/install.sh | bash | -| [OpenCode](https://opencode.ai/docs/) | ✅ Supported | npm install -g opencode-ai | +| [OpenCode](https://opencode.ai/docs/cli/) | ✅ Supported | npm install -g opencode-ai | | [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) | ✅ Supported | npm install -g @mariozechner/pi-coding-agent | | [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ Supported | npm install -g @qwen-code/qwen-code | | [Rovo Dev](https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/) | ✅ Supported | acli rovodev auth login | @@ -132,7 +138,7 @@ Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md) to get star > TELEMETRY_ENABLED=false > ``` > -> Full details: see `docs/telemetry.md`. +> Full details: see [Telemetry](https://emdash.sh/docs/telemetry).
@@ -151,19 +157,6 @@ Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md) to get star > You can reset the local DB by deleting it (quit the app first). The file is recreated on next launch.
-
-Do I need GitHub CLI? - -> **Only if you want GitHub features** (open PRs from Emdash, fetch repo info, GitHub Issues integration). -> Install & sign in: -> -> ```bash -> gh auth login -> ``` -> -> If you don’t use GitHub features, you can skip installing `gh`. -
-
How do I add a new provider? @@ -171,31 +164,11 @@ Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md) to get star > > - Open a PR following the **Contributing Guide** (`CONTRIBUTING.md`). > - Include: provider name, how it’s invoked (CLI command), auth notes, and minimal setup steps. -> - We’ll add it to the **Integrations** matrix and wire up provider selection in the UI. +> - We’ll add it to the **Providers table** and wire up provider selection in the UI. > > If you’re unsure where to start, open an issue with the CLI’s link and typical commands.
-
-I hit a native‑module crash (sqlite3 / node‑pty / keytar). What’s the fast fix? - -> This usually happens after switching Node/Electron versions. -> -> 1) Rebuild native modules: -> -> ```bash -> npm run rebuild -> ``` -> -> 2) If that fails, clean and reinstall: -> -> ```bash -> npm run reset -> ``` -> -> (Resets `node_modules`, reinstalls, and re‑builds Electron native deps.) -
-
What permissions does Emdash need? @@ -222,7 +195,7 @@ Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md) to get star > - Git installed on the remote server > - For agent auth: SSH agent running with your key loaded (`ssh-add -l`) > -> See [docs/ssh-setup.md](./docs/ssh-setup.md) for detailed setup instructions and [docs/ssh-architecture.md](./docs/ssh-architecture.md) for technical details. +> See [Remote Projects](https://emdash.sh/docs/remote-projects) for detailed setup instructions and [Bring Your Own Infrastructure](https://emdash.sh/docs/bring-your-own-infrastructure) for technical details.
[![Follow @emdashsh](https://img.shields.io/twitter/follow/emdashsh?style=social&label=Follow%20%40emdashsh)](https://x.com/emdashsh) diff --git a/agents/README.md b/agents/README.md new file mode 100644 index 0000000000..ea886141c6 --- /dev/null +++ b/agents/README.md @@ -0,0 +1,29 @@ +# Agent Docs + +This directory is the system of record for agent-facing repo guidance. Keep topic pages small, specific, and mechanically checkable where possible. + +## Recommended Reading Order + +1. `quickstart.md` +2. `architecture/overview.md` +3. the task-specific page for the area you are changing + +## Directory Layout + +- `architecture/` + - system structure and major code ownership boundaries +- `workflows/` + - task-oriented procedures like testing, worktrees, and remote development +- `integrations/` + - provider, MCP, and external service guidance +- `risky-areas/` + - places where incorrect changes are expensive +- `conventions/` + - coding contracts and repo rules + +## Maintenance Rules + +- Prefer one page per concrete topic. +- Avoid volatile counts unless you can verify them cheaply. +- Link to the source-of-truth file paths. +- Update the smallest relevant page instead of expanding `AGENTS.md`. diff --git a/agents/architecture/main-process.md b/agents/architecture/main-process.md new file mode 100644 index 0000000000..b8e4f0d432 --- /dev/null +++ b/agents/architecture/main-process.md @@ -0,0 +1,52 @@ +# Main Process + +## Structure + +The main process is organized into domain modules under `src/main/core/`. Each domain typically has a `controller.ts` (RPC handlers) and service/implementation files. + +## Domain Modules (`src/main/core/`) + +- **account** — Emdash account service, credential store, provider token registry +- **agent-hooks** — HTTP hook server for agent callbacks, event enrichment, OS notifications, hook config writer (Claude/Codex) +- **app** — App lifecycle service and controller +- **conversations** — Conversation CRUD, session start, agent event classifiers (per-provider terminal output parsers) +- **dependencies** — CLI agent detection, probing, dependency management +- **editor** — Editor buffer service for Monaco integration +- **fs** — Filesystem operations with provider pattern (`local-fs.ts`, `ssh-fs.ts`) +- **git** — Git operations (`git-service.ts`, `git-repo-utils.ts`, `detectGitInfo.ts`) +- **github** — GitHub auth, PRs, issues, repos (via `gh` CLI) +- **jira** — Jira integration +- **linear** — Linear integration +- **mcp** — MCP service, adapters, config IO, catalog +- **projects** — Project management with provider pattern (`local-project-provider.ts`), worktree service, project settings, CRUD operations +- **pty** — PTY lifecycle (`local-pty.ts`, `ssh2-pty.ts`), session registry, env setup, spawn utilities +- **repository** — Repository controller +- **settings** — App settings service and schema, provider settings (separate controller) +- **shared** — Shared utilities (OAuth flow) +- **skills** — Skills service and controller +- **ssh** — SSH connection management, credentials, config parsing, client proxy +- **tasks** — Task CRUD (create, delete, archive, restore, provision) +- **terminals** — Terminal lifecycle with provider pattern (`local-terminal-provider.ts`, `ssh-terminal-provider.ts`), lifecycle scripts +- **updates** — Auto-update service + +## Other Main Process Areas + +- `src/main/app/` — Menu, protocol handler, window creation +- `src/main/lib/` — Logger, telemetry, events, result type, updater error +- `src/main/db/` — Database schema and initialization +- `src/main/utils/` — Shell environment, shell escaping, child process env, external links +- `src/main/core/agent-hooks/` — Hook server, event enrichment, OS notifications, hook config writer for Claude/Codex + +## IPC / RPC Structure + +- All domain controllers are assembled into a typed RPC router in `src/main/rpc.ts`. +- RPC primitives live in `src/shared/ipc/rpc.ts` (`createRPCRouter`, `createRPCController`, `createRPCClient`). +- Event primitives live in `src/shared/ipc/events.ts`. +- A small number of manual IPC handlers remain in `electron-api.d.ts` for methods requiring `event.sender` (PTY start/input/resize/kill, fsList, openIn). + +## When Editing Here + +- Check `agents/conventions/main-patterns.md` for controller, service, Result type, and event patterns. +- Check `agents/conventions/ipc.md` for the RPC controller pattern and typing rules. +- Check `agents/risky-areas/pty.md` before touching PTY or provider spawn behavior. +- Check `agents/risky-areas/database.md` before changing persistence or migrations. diff --git a/agents/architecture/overview.md b/agents/architecture/overview.md new file mode 100644 index 0000000000..c4562db13d --- /dev/null +++ b/agents/architecture/overview.md @@ -0,0 +1,30 @@ +# Architecture Overview + +## Process Model + +- `src/main/`: Electron main process — app lifecycle, RPC controllers, domain services, database, PTY orchestration, updater, SSH +- `src/preload/`: Electron preload bridge — exposes typed `invoke`, `eventSend`, `eventOn` to renderer +- `src/renderer/`: React UI — views, components, hooks, contexts, typed RPC client +- `src/shared/`: Provider registry, IPC primitives (RPC + events), MCP types, skills types, shared domain types +- `docs/`: Separate Next.js + Fumadocs site + +## Boot Sequence + +`src/main/index.ts` → app lifecycle → IPC/RPC registration → window creation → renderer + +- `index.ts` — Loads `.env`, normalizes PATH, initializes database, registers all RPC controllers via `src/main/rpc.ts`, creates the main window. +- `src/main/rpc.ts` — Assembles the typed RPC router from domain controllers (`src/main/core/*/controller.ts`). +- `src/preload/index.ts` — Exposes `window.electronAPI` (`invoke`, `eventSend`, `eventOn`) via `contextBridge`. +- `src/renderer/core/ipc.ts` — Creates the typed RPC client and event emitter used throughout the renderer. + +## Build Tooling + +- `electron.vite.config.ts` — electron-vite config for main, preload, and renderer builds. +- `vitest.config.ts` — Vitest config with two test projects: `node` (main + renderer unit tests) and `browser` (Playwright-backed renderer tests). +- Single `tsconfig.json` for all targets. + +## Read Next + +- Main process details: `main-process.md` +- Renderer details: `renderer.md` +- Shared modules and provider registry: `shared.md` diff --git a/agents/architecture/renderer.md b/agents/architecture/renderer.md new file mode 100644 index 0000000000..32a0101db8 --- /dev/null +++ b/agents/architecture/renderer.md @@ -0,0 +1,50 @@ +# Renderer + +## Main Entry Points + +- `src/renderer/App.tsx`: top-level provider composition +- `src/renderer/views/Workspace.tsx`: main post-onboarding shell +- `src/renderer/components/MainContent.tsx`: switches between views (projects, tasks, settings, skills, MCP, home) +- `src/renderer/core/ipc.ts`: typed RPC client (`rpc`) and event emitter (`events`) used throughout renderer + +## View Areas (`src/renderer/views/`) + +- `projects/` — project management: active project, pending project, create task modal, settings panel, task panel, branch selector, titlebar +- `tasks/` — task experience: + - `conversations/` — conversation panel and tabs + - `diff-viewer/` — file changes panel, diff views (file, stacked), PR section, git state providers + - `editor/` — Monaco code editor, file tree, editor providers, conflict dialog + - `terminals/` — terminal panel and tabs + - `hooks/` — task-scoped hooks (use-task, use-conversations, use-terminals, use-task-view-navigation) +- `settings/` — settings view +- `home-view.tsx`, `mcp-view.tsx`, `skills-view.tsx`, `Welcome.tsx` + +## Component Areas (`src/renderer/components/`) + +- `sidebar/` — app sidebar +- `diff/` — diff-related components +- `skills/` — skills catalog and management +- `mcp/` — MCP server management +- `kanban/` — kanban board +- `integrations/` — integration management +- `ssh/` — SSH connection UI +- `FileExplorer/` — file tree navigation +- `settings/` — settings components +- `projects/` — project-related components +- `ui/` — shared UI primitives + +## Supporting Structure + +- Context providers: `src/renderer/contexts/` +- Hooks: `src/renderer/hooks/` +- Client-side state helpers, stores, and utilities: `src/renderer/lib/` +- Core infrastructure: `src/renderer/core/` (IPC client, modals, project state, PTY helpers, view management) + +## When Editing Here + +- Check `agents/conventions/renderer-patterns.md` for modal, view, PTY frontend, and context patterns. +- Call RPC methods via the typed `rpc` client from `src/renderer/core/ipc.ts` (e.g., `rpc.tasks.create(...)`). +- New modals must be registered in `src/renderer/core/modal/registry.ts`. +- New views must be registered in `src/renderer/core/view/registry.ts`. +- Only methods in `src/renderer/types/electron-api.d.ts` use direct `window.electronAPI` calls (PTY ops, fsList, openIn). +- If you change user-visible workflows, update the matching page in `docs/` when appropriate. diff --git a/agents/architecture/shared.md b/agents/architecture/shared.md new file mode 100644 index 0000000000..4affe2cc5d --- /dev/null +++ b/agents/architecture/shared.md @@ -0,0 +1,45 @@ +# Shared Modules + +## Main Shared Areas + +- Provider registry: + - `src/shared/agent-provider-registry.ts` +- IPC primitives: + - `src/shared/ipc/rpc.ts` — typed RPC router, controller, and client + - `src/shared/ipc/events.ts` — typed event emitter +- Typed event definitions: + - `src/shared/events/` — `agentEvents.ts`, `appEvents.ts`, `editorEvents.ts`, `fsEvents.ts`, `githubEvents.ts`, `hostPreviewEvents.ts`, `lifecycleEvents.ts`, `ptyEvents.ts`, `sshEvents.ts` +- MCP types: + - `src/shared/mcp/` +- Skills types and validation: + - `src/shared/skills/` +- Domain type modules (flat files): + - `conversations.ts`, `fs.ts`, `git.ts`, `github.ts`, `hostPreview.ts`, `lifecycle.ts`, `projects.ts`, `pull-requests.ts`, `ssh.ts`, `tasks.ts`, `terminals.ts`, `urls.ts`, `utils.ts` +- PTY helpers: + - `ptyId.ts`, `ptySessionId.ts` +- App settings types: + - `app-settings.ts` + +## Path Aliases + +All aliases are defined in a single `tsconfig.json` and mirrored in `electron.vite.config.ts`: + +| Alias | Resolves to | +| --- | --- | +| `@/*` | `src/*` | +| `@renderer/*` | `src/renderer/*` | +| `@main/*` | `src/main/*` | +| `@shared/*` | `src/shared/*` | +| `@root/*` | `./*` | + +Aliases are resolved at build time by electron-vite. No runtime monkey-patching is needed. + +## Provider Registry Rules + +When adding a provider: + +1. update `src/shared/agent-provider-registry.ts` +2. add any required env passthrough in `src/main/core/pty/pty-env.ts` +3. add an agent event classifier in `src/main/core/conversations/impl/agent-event-classifiers/` +4. update renderer surfaces that assume provider metadata +5. add tests for non-standard spawn or detection behavior diff --git a/agents/conventions/config-files.md b/agents/conventions/config-files.md new file mode 100644 index 0000000000..6a5632af54 --- /dev/null +++ b/agents/conventions/config-files.md @@ -0,0 +1,20 @@ +# Config Files And Repo Rules + +## Key Files + +- `package.json` +- `electron.vite.config.ts` +- `vitest.config.ts` +- `tsconfig.json` +- `drizzle.config.ts` +- `.emdash.json` +- `.nvmrc` +- `.husky/` +- `.github/workflows/` +- `flake.nix` + +## Repo Rules + +- avoid editing `dist/`, `release/`, and `build/` unless the task is explicitly about packaging or signing +- the docs app in `docs/` is separate from the Electron renderer +- update the narrowest relevant page in `agents/` instead of growing `AGENTS.md` diff --git a/agents/conventions/ipc.md b/agents/conventions/ipc.md new file mode 100644 index 0000000000..aadfadb560 --- /dev/null +++ b/agents/conventions/ipc.md @@ -0,0 +1,45 @@ +# IPC Conventions + +## RPC Pattern + +The primary IPC mechanism is a typed RPC system: + +- **Controllers**: `src/main/core/*/controller.ts` — define handler functions using `createRPCController`. +- **Router**: `src/main/rpc.ts` — assembles all controllers into a typed router using `createRPCRouter`. +- **Registration**: `registerRPCRouter(router, ipcMain)` in `src/main/index.ts` — auto-registers `namespace.method` channels. +- **Client**: `src/renderer/core/ipc.ts` — creates a proxy-based typed client using `createRPCClient`. + +```ts +// Main — src/main/core/example/controller.ts +import { createRPCController } from '@shared/ipc/rpc'; +export const exampleController = createRPCController({ + async doSomething(id: string) { + return await service.doSomething(id); + }, +}); + +// Renderer — call via typed client +import { rpc } from '@renderer/core/ipc'; +const result = await rpc.example.doSomething('123'); +``` + +## Manual IPC (electron-api.d.ts) + +A small set of IPC methods that depend on `event.sender` remain as manual handlers declared in `src/renderer/types/electron-api.d.ts` (~92 lines): + +- PTY operations: `ptyStart`, `ptyStartDirect`, `ptyInput`, `ptyResize`, `ptyKill` +- Filesystem listing: `fsList` +- Open in external app: `openIn` +- Update events: `onUpdateEvent` + +## Event System + +Typed events use `createEventEmitter` from `src/shared/ipc/events.ts`. Event type definitions live in `src/shared/events/`. + +## Rules + +- Prefer the RPC pattern for new IPC methods — add a handler to the appropriate controller. +- Only use manual IPC when `event.sender` is required. +- Keep the RPC router type (`RpcRouter`) importable by the renderer for type inference. +- Prefer existing service boundaries over adding logic directly inside controllers. +- Update tests when controller shape or IPC wiring changes. diff --git a/agents/conventions/main-patterns.md b/agents/conventions/main-patterns.md new file mode 100644 index 0000000000..86193b912a --- /dev/null +++ b/agents/conventions/main-patterns.md @@ -0,0 +1,105 @@ +# Main Process Patterns + +## Controller Pattern + +Each domain in `src/main/core/` exposes a `controller.ts` that defines RPC handlers: + +```ts +// src/main/core/tasks/controller.ts +import { createRPCController } from '@shared/ipc/rpc'; +import { createTask } from './createTask'; +import { getTasks } from './getTasks'; + +export const taskController = createRPCController({ + createTask, + getTasks, + deleteTask, + // ... +}); +``` + +Controllers are assembled into the router in `src/main/rpc.ts`: + +```ts +export const rpcRouter = createRPCRouter({ + tasks: taskController, + projects: projectController, + // ... +}); +``` + +**Rules:** +- Controller handlers are imported functions — keep logic in separate operation files, not inline +- Each controller becomes an RPC namespace (e.g., `rpc.tasks.createTask(...)` on the renderer) +- New domains need their controller added to `src/main/rpc.ts` + + +## Service Pattern + +For stateful concerns, use singleton classes: + +```ts +export class AppService { + private cache = new Map(); + + async initialize() { /* ... */ } + async doSomething(id: string) { /* ... */ } +} + +export const appService = new AppService(); +``` + +**Rules:** +- Module-level singleton export +- Initialization method called from `src/main/index.ts` +- Services hold long-lived state (caches, subscriptions, connections) + +## Provider Pattern + +For domain logic with multiple backends (local vs SSH): + +``` +src/main/core/projects/ +├── project-provider.ts # Interface +├── impl/ +│ ├── local-project-provider.ts +│ └── _ssh-project-provider.ts # Prefixed with _ = not yet implemented +└── project-manager.ts # Orchestrates providers +``` + +Used in: projects, filesystem (`local-fs.ts` / `ssh-fs.ts`), terminals (`local-terminal-provider.ts` / `ssh-terminal-provider.ts`) + +## Result Type (`src/main/lib/result.ts`) + +Explicit error handling via discriminated union: + +```ts +import { ok, err, type Result } from '../lib/result'; + +async function doSomething(): Promise> { + if (problem) return err({ type: 'not_found' as const }); + return ok(data); +} +``` + +**Rules:** +- Prefer `Result` over thrown exceptions for expected failure modes +- Controllers convert Result types to IPC-compatible responses + +## Event System (`src/main/lib/events.ts`) + +Topic-based event emitter for main ↔ renderer communication: + +```ts +import { events } from '../lib/events'; + +// Emit to a specific topic (e.g., session ID) +events.emit(ptyDataChannel, buffer, sessionId); + +// Listen on a specific topic +const unsub = events.on(ptyDataChannel, (data) => {...}, sessionId); +``` + +Channel naming: without topic → `eventName`, with topic → `eventName.{topic}` + +Event type definitions live in `src/shared/events/`. diff --git a/agents/conventions/renderer-patterns.md b/agents/conventions/renderer-patterns.md new file mode 100644 index 0000000000..96976cf1e6 --- /dev/null +++ b/agents/conventions/renderer-patterns.md @@ -0,0 +1,93 @@ +# Renderer Patterns + +## Modal System (`src/renderer/core/modal/`) + +All modals use a registry-based system. Only one modal can be active at a time. + +- `registry.ts` — central registry mapping modal IDs to components +- `modal-provider.tsx` — React context managing active modal state +- `modal-renderer.tsx` — renders the currently active modal + +**Adding a modal:** +1. Create the component accepting `BaseModalProps` (provides `onSuccess` and `onClose` callbacks) +2. Register it in `registry.ts` +3. Open it via the hook: + +```tsx +const { showModal } = useModalContext(); +showModal('myModal', { projectId: '123', onSuccess: (result) => {...} }); +``` + +**Rules:** +- All modals must be registered in `registry.ts` +- `showModal` is type-safe — TypeScript infers required args from the registry +- `hasActiveCloseGuard` prevents dismissal during critical operations + +## View System (`src/renderer/core/view/`) + +Views use a registry + parameterized navigation pattern. + +- `registry.ts` — view definitions with optional `WrapView`, `TitlebarSlot`, `MainPanel`, `RightPanel` +- `provider.tsx` — state management, navigation, param persistence +- `layout-provider.tsx` — panel collapse/expand/drag state + +**Key behaviors:** +- `navigate(viewId, params?)` is type-safe; params are optional when all fields are optional +- Params persist per-view (navigating away and back preserves params) +- Modal automatically closes on navigation +- `updateViewParams(viewId, partial)` updates params without re-navigating + +**Rules:** +- Views are singletons — one per ViewId +- MainPanel is required; RightPanel and WrapView are optional +- Add new views to `registry.ts` + +## PTY Frontend (`src/renderer/core/pty/`) + +Terminal sessions use a registry + pool pattern. + +- `pty.ts` — `FrontendPty` class with `FrontendPtyRegistry` (module-level singleton, survives React unmounts) +- `pty-pool.ts` — `TerminalPool` managing up to 16 reusable xterm.js instances +- `use-pty.ts` — React hook integrating FrontendPty + TerminalPool +- `pty-session-context.tsx` — context for session registration +- `pty-pane.tsx` — terminal component (forwardRef) + +**Lifecycle:** register → attach → detach → unregister + +**Rules:** +- `registerSession()` must happen BEFORE RPC starts the PTY to avoid missing output +- `FrontendPty` buffers output (max 1 MB) when no xterm is attached, drains on `attach()` +- Terminal instances are never disposed — they're parked off-screen and reused from the pool +- `sessionId` format: `makePtySessionId(projectId, taskId, conversationId)` — deterministic +- Panel drag pauses resizing to avoid jank (`panelDragStore`) + +## React Query Context Pattern + +Context providers use React Query for data fetching with optimistic updates: + +```tsx +// Pattern used in AppSettingsProvider, ProjectProvider, etc. +const { data } = useQuery({ queryKey: ['resource'], queryFn: () => rpc.ns.get() }); +const mutation = useMutation({ + mutationFn: (args) => rpc.ns.update(args), + onMutate: async (args) => { + // optimistic update via queryClient.setQueryData + }, + onError: () => { + // rollback via queryClient.setQueryData with previous snapshot + }, +}); +``` + +**Rules:** +- Contexts combine React Query + local state, not standalone useState +- Use `useAppSettingsKey(key)` for fine-grained per-setting hooks +- Optimistic updates must include rollback on error + +## State Outside React + +For state that must survive React unmounts or be shared across unrelated components: + +- **`useSyncExternalStore`-compatible stores** — e.g., `panelDragStore` in `src/renderer/lib/` +- **Module-level singletons** — e.g., `FrontendPtyRegistry`, `TerminalPool` +- **Manager classes** — e.g., `PendingInjectionManager`, `TaskTerminalsStore` diff --git a/agents/conventions/typescript.md b/agents/conventions/typescript.md new file mode 100644 index 0000000000..28b5eaa02b --- /dev/null +++ b/agents/conventions/typescript.md @@ -0,0 +1,23 @@ +# TypeScript And React Conventions + +## TypeScript + +- strict mode is enabled in `tsconfig.json` +- always use explicit types, do not use `any` +- prefer module imports at the top of the file, never use require() +- single `tsconfig.json` for all targets (main, preload, renderer, shared) + +## Renderer + +- functional React components and hooks +- context providers under `src/renderer/contexts/` +- hooks under `src/renderer/hooks/` +- client-side stores and helpers under `src/renderer/lib/` +- core infrastructure under `src/renderer/core/` (IPC client, modal management, view state) +- view-level components under `src/renderer/views/` + +## Naming + +- components: PascalCase +- hooks: `useX` camelCase or existing patterns like `use-toast.ts` +- tests: `*.test.ts` diff --git a/agents/integrations/mcp.md b/agents/integrations/mcp.md new file mode 100644 index 0000000000..04834e5c89 --- /dev/null +++ b/agents/integrations/mcp.md @@ -0,0 +1,26 @@ +# MCP + +## Main Files + +- `src/main/core/mcp/services/McpService.ts` +- `src/main/core/mcp/utils/` — adapters, catalog, config IO, config paths, conversion +- `src/main/core/mcp/controller.ts` +- `src/shared/mcp/` +- `src/renderer/components/mcp/` +- `src/renderer/views/mcp-view.tsx` + +## Current Behavior + +- MCP server configs are read, adapted, merged, and written across supported agent ecosystems +- provider-specific config formats are handled through adapters in `src/main/core/mcp/utils/` +- the renderer MCP UI manages installed servers and catalog entries + +## Important Constraint + +- Codex currently supports stdio MCP servers only + +## Rules + +- do not assume all providers support the same MCP transport types +- keep canonical MCP data in shared types and adapt at the edges +- if you add provider-specific MCP behavior, update both service and UI compatibility handling diff --git a/agents/integrations/providers.md b/agents/integrations/providers.md new file mode 100644 index 0000000000..eabd67a7d9 --- /dev/null +++ b/agents/integrations/providers.md @@ -0,0 +1,40 @@ +# Providers + +## Source Of Truth + +- `src/shared/agent-provider-registry.ts` +- `src/main/core/dependencies/dependency-manager.ts` +- `src/main/core/pty/` + +## Current Providers (22) + +codex, claude, qwen, droid, gemini, cursor, copilot, amp, opencode, charm, auggie, goose, kimi, kilocode, kiro, rovo, cline, continue, codebuff, mistral, pi, autohand + +## Provider Metadata Includes + +- CLI and detection commands +- version args +- install command and docs URL +- auto-approve flags +- initial prompt handling +- keystroke injection behavior +- resume and session flags +- optional plan activation and auto-start commands + +## Agent Event Classifiers + +Each provider has a terminal output classifier in `src/main/core/conversations/impl/agent-event-classifiers/`. These parse agent terminal output to detect events (task completion, errors, etc.) and forward them to the renderer via the agent hooks module (`src/main/core/agent-hooks/`). + +## Provider Runtime Notes + +- Claude uses deterministic `--session-id` values for conversation isolation. +- Agents with no CLI prompt flag (e.g., Amp, OpenCode) use keystroke injection — Emdash types the prompt into the TUI after startup. +- `src/main/core/agent-hooks/service.ts` forwards hook events to renderer windows and can show OS notifications. Also writes hook config files (`.claude/settings.local.json`, `.codex/config.toml`) into worktrees. + +## Adding Or Changing A Provider + +1. update `src/shared/agent-provider-registry.ts` +2. update allowlisted agent env vars in `src/main/core/pty/pty-env.ts` if needed +3. add an agent event classifier in `src/main/core/conversations/impl/agent-event-classifiers/` +4. validate detection behavior in `src/main/core/dependencies/` +5. add or update tests for any non-standard behavior diff --git a/agents/quickstart.md b/agents/quickstart.md new file mode 100644 index 0000000000..22643e52f9 --- /dev/null +++ b/agents/quickstart.md @@ -0,0 +1,41 @@ +# Quickstart + +## Toolchain + +- Node: `24.14.0` from `.nvmrc` +- Package manager: `pnpm@10.28.2` +- Electron app root: this repo +- Docs app: `docs/` + +## Core Commands + +```bash +pnpm run d +pnpm run dev +pnpm run dev:main +pnpm run dev:renderer +pnpm run build +pnpm run rebuild +pnpm run reset +``` + +## Validation Commands + +```bash +pnpm run format +pnpm run lint +pnpm run typecheck +pnpm test run +``` + +## Docs Commands + +```bash +pnpm run docs:build +``` + +## Important Notes + +- The docs app and the Electron renderer both default to port `3000`. +- After native dependency changes (`sqlite3`, `node-pty`), run `pnpm run rebuild`. +- Husky and lint-staged run formatting and linting on staged files during commit. diff --git a/agents/risky-areas/database.md b/agents/risky-areas/database.md new file mode 100644 index 0000000000..738ee8b9f0 --- /dev/null +++ b/agents/risky-areas/database.md @@ -0,0 +1,20 @@ +# Risky Area: Database + +## Main Files + +- `src/main/db/schema.ts` +- `src/main/db/initialize.ts` +- `drizzle/` + +## Rules + +- never hand-edit numbered migrations +- never hand-edit `drizzle/meta/` +- use `pnpm exec drizzle-kit generate` for new migrations +- treat schema invariants and data migrations as high risk + +## Current Behavior + +- database path is resolved by main-process db path helpers +- `EMDASH_DB_FILE` overrides the default location +- database initialization happens in `src/main/db/initialize.ts` diff --git a/agents/risky-areas/pty.md b/agents/risky-areas/pty.md new file mode 100644 index 0000000000..fe1e344d9b --- /dev/null +++ b/agents/risky-areas/pty.md @@ -0,0 +1,24 @@ +# Risky Area: PTY And Sessions + +## Main Files + +- `src/main/core/pty/` — `local-pty.ts`, `ssh2-pty.ts`, `pty.ts`, `pty-env.ts`, `pty-session-registry.ts`, `spawn-utils.ts`, `exit-signals.ts`, `controller.ts` +- `src/main/core/terminals/` — terminal lifecycle, local and SSH terminal providers +- `src/main/core/conversations/impl/agent-event-classifiers/` — per-provider terminal output parsers +- `src/main/core/agent-hooks/` — hook server, event enrichment, OS notifications, hook config writer + +## Core Risks + +- PTY cleanup and exit handling +- resize behavior +- shell quoting and Windows command wrapping +- tmux lifecycle +- provider-specific resume/session behavior +- env passthrough safety + +## Rules + +- use the allowlisted env passthrough model in `src/main/core/pty/pty-env.ts` +- do not weaken quoting or spawn behavior casually +- validate both direct spawn and shell-wrapped spawn cases when changing PTY startup logic +- confirm renderer event flow if hook payload or notification behavior changes diff --git a/agents/risky-areas/ssh.md b/agents/risky-areas/ssh.md new file mode 100644 index 0000000000..3fb8293b0d --- /dev/null +++ b/agents/risky-areas/ssh.md @@ -0,0 +1,16 @@ +# Risky Area: SSH And Shell Escaping + +## Main Files + +- `src/main/core/ssh/` — `ssh-connection-manager.ts`, `ssh-credential-service.ts`, `ssh-client-proxy.ts`, `sshConfigParser.ts`, `build-connect-config.ts`, `controller.ts` +- `src/main/core/fs/impl/ssh-fs.ts` +- `src/main/core/pty/ssh2-pty.ts` +- `src/main/core/terminals/impl/ssh-terminal-provider.ts` +- `src/main/utils/shellEscape.ts` + +## Rules + +- treat remote shell construction as security-sensitive +- use shared escaping and validation helpers +- do not bypass path-safety or shell validation helpers +- verify how a change affects both connection setup and command execution diff --git a/agents/risky-areas/updater.md b/agents/risky-areas/updater.md new file mode 100644 index 0000000000..7830a7f268 --- /dev/null +++ b/agents/risky-areas/updater.md @@ -0,0 +1,23 @@ +# Risky Area: Updater And Packaging + +## Main Files + +- `src/main/core/updates/AutoUpdateService.ts` +- `src/main/core/updates/controller.ts` +- `build/` +- `package.json` +- `.github/workflows/release.yml` +- `.github/workflows/windows-beta-build.yml` +- `.github/workflows/nix-build.yml` + +## Rules + +- avoid changing updater defaults casually +- treat signing, notarization, packaging targets, and native rebuild flow as release-critical +- keep build output directories and packaging config stable unless the task is explicitly about release behavior + +## Current Notes + +- macOS and Linux release jobs rebuild native modules for the target Electron version +- Windows beta builds intentionally use Node 20 in CI for native module stability +- changelog and auto-update behavior are separate but related surfaces in the app diff --git a/agents/workflows/remote-development.md b/agents/workflows/remote-development.md new file mode 100644 index 0000000000..4ac29de06d --- /dev/null +++ b/agents/workflows/remote-development.md @@ -0,0 +1,26 @@ +# Remote Development + +## Main Files + +- `src/main/core/ssh/` — connection management, credentials, config parsing +- `src/main/core/pty/ssh2-pty.ts` +- `src/main/core/fs/impl/ssh-fs.ts` +- `src/main/core/terminals/impl/ssh-terminal-provider.ts` +- `src/main/utils/shellEscape.ts` + +## Current Model + +- remote projects are backed by SSH connections +- remote worktrees live under `/.emdash/worktrees//` +- remote PTYs stream agent shells back to the renderer + +## Authentication And Storage + +- SSH credentials are managed through the SSH services and OS-backed secret storage +- host key handling is implemented under `src/main/core/ssh/` + +## Rules + +- treat all shell construction as security-sensitive +- use shared SSH and shell-escaping helpers instead of ad hoc quoting +- confirm whether a feature is local-only before assuming parity on remote projects diff --git a/agents/workflows/testing.md b/agents/workflows/testing.md new file mode 100644 index 0000000000..39b7c270f5 --- /dev/null +++ b/agents/workflows/testing.md @@ -0,0 +1,41 @@ +# Testing And Validation + +## Core Local Gate + +Run these before merging: + +```bash +pnpm run format +pnpm run lint +pnpm run typecheck +pnpm run test +``` + +## Test Layout + +- main-process tests: colocated in `src/main/core/**/*.test.ts` +- renderer unit tests: `src/renderer/tests/` +- renderer browser tests: `src/renderer/tests/browser/` (run via Playwright) + +## Current Setup + +- Vitest config is in `vitest.config.ts` (separate from the build config in `electron.vite.config.ts`). +- Two test projects: + - `node` — all `src/**/*.test.ts` files excluding `_*` dirs and browser tests + - `browser` — `src/renderer/tests/browser/**/*.test.{ts,tsx}` via `@vitest/browser-playwright` +- Tests use per-file `vi.mock()` setup. +- Integration-style tests create temporary repos and worktrees in `os.tmpdir()`. + +## CI Notes + +- `.github/workflows/code-consistency-check.yml` currently enforces: + - `pnpm run format:check` + - `pnpm run typecheck` + - `pnpm exec vitest run` +- Lint is still expected locally even though it is not enabled in that workflow yet. + +## Focused Validation + +- after IPC/RPC changes: rerun the affected Vitest file and confirm the controller is wired in `src/main/rpc.ts` +- after worktree or PTY changes: rerun the closest `src/main/core/` test files +- after docs changes: run `pnpm run docs:build` diff --git a/agents/workflows/worktrees.md b/agents/workflows/worktrees.md new file mode 100644 index 0000000000..e800eb2297 --- /dev/null +++ b/agents/workflows/worktrees.md @@ -0,0 +1,33 @@ +# Worktrees + +## Main Files + +- `src/main/core/projects/worktrees/worktree-service.ts` +- `src/main/core/projects/project-manager.ts` +- `src/main/core/terminals/runLifecycleScript.ts` +- `.emdash.json` + +## Current Behavior + +- task worktrees are created under `../worktrees/` +- branch prefix defaults to `emdash` and is configurable in app settings +- selected gitignored files are preserved into worktrees +- worktree creation is managed by the project provider pattern + +## `.emdash.json` + +Current supported keys: + +- `preservePatterns` +- `scripts.setup` +- `scripts.run` +- `scripts.teardown` +- `shellSetup` +- `tmux` + +## Rules + +- do not hardcode worktree paths; use service helpers +- use lifecycle config for repo-specific bootstrap and teardown behavior +- `shellSetup` runs inside each PTY before the interactive shell starts +- tmux wrapping is project-configurable and affects PTY lifecycle behavior diff --git a/components.json b/components.json index 0d76b71de4..b0732a2d7e 100644 --- a/components.json +++ b/components.json @@ -1,10 +1,10 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", + "style": "base-vega", "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "", "css": "src/renderer/index.css", "baseColor": "neutral", "cssVariables": true, @@ -12,11 +12,11 @@ }, "iconLibrary": "lucide", "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" + "components": "@renderer/components", + "utils": "@renderer/lib/utils", + "ui": "@renderer/components/ui", + "lib": "@renderer/lib", + "hooks": "@renderer/hooks" }, "registries": { "@ai-elements": "https://registry.ai-sdk.dev/{name}.json" diff --git a/dev-app-update.yml b/dev-app-update.yml new file mode 100644 index 0000000000..48898d1823 --- /dev/null +++ b/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: generic +url: https://releases.emdash.sh +channel: v1-stable diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000..a16ff31cee --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,50 @@ +# ────────────────────────────────────────────────────────────────────────────── +# Emdash SSH Dev Container — docker-compose +# +# Usage: +# cp .env.example .env # fill in your API keys +# docker compose up --build -d +# +# Connect from emdash: +# host: localhost port: 2222 user: devuser auth: password pass: devpass +# ────────────────────────────────────────────────────────────────────────────── +services: + ssh-dev: + build: + context: ./docker-ssh + dockerfile: dockerfile + container_name: emdash-ssh-dev + ports: + - "2222:22" + environment: + # API keys forwarded into SSH sessions via ~/.ssh/environment. + # Define these in a .env file next to this compose file (gitignored). + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - GH_TOKEN=${GH_TOKEN:-} + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - GEMINI_API_KEY=${GEMINI_API_KEY:-} + - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} + - AMP_API_KEY=${AMP_API_KEY:-} + - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-} + - KIMI_API_KEY=${KIMI_API_KEY:-} + - MISTRAL_API_KEY=${MISTRAL_API_KEY:-} + - CODEBUFF_API_KEY=${CODEBUFF_API_KEY:-} + - FACTORY_API_KEY=${FACTORY_API_KEY:-} + - CURSOR_API_KEY=${CURSOR_API_KEY:-} + volumes: + # ── Project mounts ─────────────────────────────────────────────────── + # Option A: mount a real local git repo from your Mac (recommended): + # - /Users/yourname/code/myrepo:/home/devuser/projects/myrepo + # + # Option B: use a named volume for a self-contained sandbox: + - projects:/home/devuser/projects + restart: unless-stopped + # Needed for tmux PTY allocation over SSH + tty: true + stdin_open: true + shm_size: '256mb' + +volumes: + projects: + driver: local \ No newline at end of file diff --git a/docker-ssh/dockerfile b/docker-ssh/dockerfile new file mode 100644 index 0000000000..05e8fa9f79 --- /dev/null +++ b/docker-ssh/dockerfile @@ -0,0 +1,128 @@ +# ────────────────────────────────────────────────────────────────────────────── +# Emdash SSH Dev Container +# +# A self-contained Linux environment you can SSH into from emdash's SSH remote +# development feature. Includes git, gh, tmux, Node.js, and Claude Code out +# of the box. Other agents (codex, gemini, etc.) can be installed on top. +# +# Usage: +# docker compose up --build -d +# Then add an SSH connection in emdash: +# host: localhost port: 2222 user: devuser auth: password pass: devpass +# ────────────────────────────────────────────────────────────────────────────── +FROM ubuntu:24.04 + +# Avoid interactive prompts during package install +ENV DEBIAN_FRONTEND=noninteractive + +# ── System packages ───────────────────────────────────────────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + # SSH server + openssh-server \ + # Core tools + git \ + curl \ + wget \ + tmux \ + sudo \ + ca-certificates \ + gnupg \ + # Shell utilities + bash \ + zsh \ + vim \ + nano \ + less \ + # Build tools (needed by some npm native modules) + build-essential \ + python3 \ + # Process utilities + procps \ + && rm -rf /var/lib/apt/lists/* + +# ── GitHub CLI (gh) ───────────────────────────────────────────────────────── +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +# ── Node.js 22 LTS (via NodeSource) ───────────────────────────────────────── +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# ── Global npm config: faster installs, no fund/audit noise ───────────────── +RUN npm config set fund false && npm config set audit false + +# ── Claude Code ───────────────────────────────────────────────────────────── +# Primary agent. Installed globally so it's on PATH for all users. +RUN npm install -g @anthropic-ai/claude-code + +# ── Optional: additional agents (uncomment as needed) ─────────────────────── +# RUN npm install -g @openai/codex # OpenAI Codex CLI +# RUN npm install -g @google/gemini-cli # Gemini CLI +# RUN npm install -g opencode # OpenCode + +# ── Dev user ───────────────────────────────────────────────────────────────── +# 'devuser' is the account emdash will SSH into. +# Password is 'devpass' — change or replace with key auth for security. +RUN useradd -m -s /bin/bash devuser \ + && echo 'devuser:devpass' | chpasswd \ + && usermod -aG sudo devuser \ + && echo 'devuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/devuser + +# ── SSH server config ──────────────────────────────────────────────────────── +# Generate host keys and configure sshd for interactive use. +RUN mkdir -p /var/run/sshd \ + && ssh-keygen -A + +# Allow password auth and keep SSH sessions alive. +# PermitUserEnvironment lets ~/.ssh/environment set API keys on login. +RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config \ + && sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config \ + && echo 'PermitUserEnvironment yes' >> /etc/ssh/sshd_config \ + && echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config \ + && echo 'ClientAliveCountMax 10' >> /etc/ssh/sshd_config \ + && echo 'X11Forwarding no' >> /etc/ssh/sshd_config + +# ── Git global defaults for devuser ───────────────────────────────────────── +USER devuser +WORKDIR /home/devuser + +RUN git config --global user.email "devuser@emdash-dev" \ + && git config --global user.name "Emdash Dev" \ + && git config --global init.defaultBranch main \ + # Required: allow git operations in worktrees under /home/devuser + && git config --global safe.directory '*' + +# ── Projects directory ─────────────────────────────────────────────────────── +# Mount your git repos here, e.g.: +# volumes: +# - /path/to/my/project:/home/devuser/projects/myproject +RUN mkdir -p /home/devuser/projects + +# ── tmux default config: no status bar clutter ────────────────────────────── +RUN echo 'set -g default-terminal "xterm-256color"' > /home/devuser/.tmux.conf \ + && echo 'set -g history-limit 50000' >> /home/devuser/.tmux.conf \ + && echo 'set-option -g allow-rename on' >> /home/devuser/.tmux.conf + +# ── Optional: SSH authorized_keys ─────────────────────────────────────────── +# Uncomment and mount your public key for key-based auth: +# COPY --chown=devuser:devuser authorized_keys /home/devuser/.ssh/authorized_keys +# RUN chmod 700 /home/devuser/.ssh && chmod 600 /home/devuser/.ssh/authorized_keys +RUN mkdir -p /home/devuser/.ssh && chmod 700 /home/devuser/.ssh + +# ── Startup ────────────────────────────────────────────────────────────────── +# sshd must run as root; use an entrypoint script to inject env vars +# from docker-compose into the user's environment file, then start sshd. +USER root + +COPY --chmod=755 entrypoint.sh /entrypoint.sh + +EXPOSE 22 + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/docker-ssh/entrypoint.sh b/docker-ssh/entrypoint.sh new file mode 100755 index 0000000000..a5a53c665d --- /dev/null +++ b/docker-ssh/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Write any ANTHROPIC_API_KEY / GH_TOKEN / etc. passed as container env vars +# into devuser's ~/.ssh/environment so they're available in SSH sessions +# (requires PermitUserEnvironment yes in sshd_config). +ENV_FILE=/home/devuser/.ssh/environment +: > "$ENV_FILE" +chown devuser:devuser "$ENV_FILE" +chmod 600 "$ENV_FILE" + +AGENT_VARS=( + ANTHROPIC_API_KEY + OPENAI_API_KEY + GH_TOKEN + GITHUB_TOKEN + GEMINI_API_KEY + GOOGLE_API_KEY + AMP_API_KEY + DASHSCOPE_API_KEY + KIMI_API_KEY + MISTRAL_API_KEY + CODEBUFF_API_KEY + FACTORY_API_KEY + CURSOR_API_KEY +) + +for var in "${AGENT_VARS[@]}"; do + val="${!var:-}" + if [ -n "$val" ]; then + echo "${var}=${val}" >> "$ENV_FILE" + fi +done + +exec /usr/sbin/sshd -D -e \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 906741640e..0000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -node_modules - -.DS_Store -.cache -.vercel -.output -.nitro -.next -/build/ -/api/ -/server/build -/public/build -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ -.tanstack - -src/routeTree.gen.ts -.source/ -next-env.d.ts -public/md-src/ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index d219822c4d..0000000000 --- a/docs/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Emdash - -This is a Tanstack Start application generated with -[Create Fumadocs](https://github.com/fuma-nama/fumadocs). - -Run development server: - -```bash -pnpm run dev -# or -pnpm dev -# or -yarn dev -``` diff --git a/docs/app/[[...slug]]/layout.tsx b/docs/app/[[...slug]]/layout.tsx deleted file mode 100644 index b70a3b4560..0000000000 --- a/docs/app/[[...slug]]/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { source } from '@/lib/source'; -import { DocsLayout } from 'fumadocs-ui/layouts/docs'; -import type { ReactNode } from 'react'; -import { baseOptions } from '@/lib/layout.shared'; - -export default function Layout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/docs/app/[[...slug]]/page.tsx b/docs/app/[[...slug]]/page.tsx deleted file mode 100644 index fea5b88f57..0000000000 --- a/docs/app/[[...slug]]/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { source } from '@/lib/source'; -import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/page'; -import { notFound } from 'next/navigation'; -import defaultMdxComponents from 'fumadocs-ui/mdx'; -import type { Metadata } from 'next'; -import { CopyMarkdownButton } from '@/components/CopyMarkdownButton'; -import { LastUpdated } from '@/components/LastUpdated'; -import { getGithubLastEdit } from 'fumadocs-core/content/github'; - -async function getLastModifiedFromGitHub(filePath: string): Promise { - if (process.env.NODE_ENV === 'development') { - return null; - } - - try { - const time = await getGithubLastEdit({ - owner: 'generalaction', - repo: 'emdash', - path: `docs/content/docs/${filePath}.mdx`, - token: process.env.GIT_TOKEN ? `Bearer ${process.env.GIT_TOKEN}` : undefined, - }); - return time ? new Date(time) : null; - } catch { - return null; - } -} - -export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) { - const { slug } = await params; - const page = source.getPage(slug); - - if (!page) { - notFound(); - } - - const MDX = page.data.body; - - // Prefer plugin-derived lastModified, fallback to GitHub API - let lastModified: Date | undefined = page.data.lastModified; - if (!lastModified) { - const filePath = slug?.join('/') || 'index'; - lastModified = (await getLastModifiedFromGitHub(filePath)) ?? undefined; - } - - return ( - - {page.data.title} - {page.data.description} -
- -
- - - - {lastModified && } -
- ); -} - -export async function generateStaticParams() { - return source.generateParams(); -} - -export async function generateMetadata(props: { - params: Promise<{ slug?: string[] }>; -}): Promise { - const params = await props.params; - const page = source.getPage(params.slug); - - if (!page) notFound(); - - return { - title: page.data.title, - description: page.data.description, - }; -} diff --git a/docs/app/api/search/route.ts b/docs/app/api/search/route.ts deleted file mode 100644 index b02d7cd3a7..0000000000 --- a/docs/app/api/search/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { source } from '@/lib/source'; -import { createFromSource } from 'fumadocs-core/search/server'; - -export const { GET } = createFromSource(source, { - language: 'english', -}); diff --git a/docs/app/global.css b/docs/app/global.css deleted file mode 100644 index b17f8a2e97..0000000000 --- a/docs/app/global.css +++ /dev/null @@ -1,18 +0,0 @@ -@import 'tailwindcss'; -@import 'fumadocs-ui/css/neutral.css'; -@import 'fumadocs-ui/css/preset.css'; - -/* Custom colors from Emdash palette */ -:root { - --color-fd-primary: #c26157; - --color-fd-primary-foreground: #f7fbfc; - --color-fd-accent: #e8ebee; - --color-fd-accent-foreground: #1f2931; -} - -.dark { - --color-fd-primary: #c26157; - --color-fd-primary-foreground: #eef0f2; - --color-fd-accent: #212a2d; - --color-fd-accent-foreground: #eef0f2; -} diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx deleted file mode 100644 index 59df42f33f..0000000000 --- a/docs/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import './global.css'; -import { RootProvider } from 'fumadocs-ui/provider/next'; -import type { ReactNode } from 'react'; - -export default function RootLayout({ children }: { children: ReactNode }) { - return ( - - - {children} - - - ); -} - -export const metadata = { - title: 'Emdash - Docs', - description: 'Open source Agentic Development Environment', - icons: { - icon: '/brand/favicon.ico', - }, -}; diff --git a/docs/components/Changelog.tsx b/docs/components/Changelog.tsx deleted file mode 100644 index 8d8ff65487..0000000000 --- a/docs/components/Changelog.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import React from 'react'; - -interface GitHubRelease { - id: number; - tag_name: string; - name: string; - body: string | null; - published_at: string; - html_url: string; - draft: boolean; - prerelease: boolean; -} - -async function getGithubReleases(): Promise { - try { - const response = await fetch('https://api.github.com/repos/generalaction/emdash/releases', { - headers: { - Accept: 'application/vnd.github.v3+json', - ...(process.env.GITHUB_TOKEN && { - Authorization: `token ${process.env.GITHUB_TOKEN}`, - }), - }, - next: { revalidate: 3600 }, // Cache for 1 hour - }); - - if (!response.ok) { - console.error('GitHub API error:', response.status); - return []; - } - - const releases = await response.json(); - return releases.filter((r: GitHubRelease) => !r.draft); - } catch (error) { - console.error('Failed to fetch releases:', error); - return []; - } -} - -function formatDate(dateString: string): string { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); -} - -function formatVersion(tagName: string): string { - return tagName.startsWith('v') ? tagName.substring(1) : tagName; -} - -export async function Changelog() { - const releases = await getGithubReleases(); - - if (releases.length === 0) { - return ( -
-

- No releases found. Check the{' '} - - GitHub Releases - {' '} - page. -

-
- ); - } - - return ( -
- {releases.slice(0, 20).map((release) => ( -
-
-

- - v{formatVersion(release.tag_name)} - -

- -
- - {release.body ? ( -
- -
- ) : ( -

No release notes available.

- )} -
- ))} -
- ); -} - -function ReleaseNotes({ content }: { content: string }) { - // Process content line by line, handling both markdown and HTML - const lines = content.split('\n'); - const elements: React.ReactNode[] = []; - let currentListItems: React.ReactNode[] = []; - let inCodeBlock = false; - let codeBlockContent: string[] = []; - let codeBlockStartIndex = 0; - - // Helper function to flush list items - const flushListItems = () => { - if (currentListItems.length > 0) { - elements.push( -
    - {currentListItems} -
- ); - currentListItems = []; - } - }; - - // Helper function to process links in text - const processLinks = (text: string): string => { - return ( - text - // Handle GitHub URLs, but not if they're followed by punctuation - .replace( - /https:\/\/github\.com\/[^\s\)\]]+/g, - (url) => - `${url.replace(/[.,;!?]+$/, '')}` - ) - // Handle @mentions, but avoid matching email addresses - // Look for @mentions that are preceded by whitespace or start of string - .replace( - /(^|[\s\(])@(\w+)(?=\s|$|[^\w@])/g, - (match, prefix, username) => - `${prefix}@${username}` - ) - ); - }; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Check for code block start/end - if (line.startsWith('```')) { - if (inCodeBlock) { - // End code block - elements.push( -
-            {codeBlockContent.join('\n')}
-          
- ); - codeBlockContent = []; - inCodeBlock = false; - } else { - // Start code block - flushListItems(); // Flush any pending list items - inCodeBlock = true; - codeBlockStartIndex = i; - } - continue; - } - - // If we're in a code block, just collect the content - if (inCodeBlock) { - codeBlockContent.push(line); - continue; - } - - // Handle HTML img tags - if (line.includes(']+>/); - if (imgMatch) { - const srcMatch = imgMatch[0].match(/src="([^"]+)"/); - const altMatch = imgMatch[0].match(/alt="([^"]+)"/); - const widthMatch = imgMatch[0].match(/width="([^"]+)"/); - const heightMatch = imgMatch[0].match(/height="([^"]+)"/); - - if (srcMatch) { - elements.push( -
- {altMatch -
- ); - continue; - } - } - } - - // Headers - if (line.startsWith('## ')) { - flushListItems(); // Flush any pending list items - elements.push( -

- {line.substring(3)} -

- ); - continue; - } - if (line.startsWith('### ')) { - flushListItems(); // Flush any pending list items - elements.push( -

- {line.substring(4)} -

- ); - continue; - } - - // List items (support both * and - prefixes) - if (line.startsWith('* ') || line.startsWith('- ')) { - const item = line.substring(2); - const withLinks = processLinks(item); - currentListItems.push(
  • ); - continue; - } - - // If we hit a non-list item, flush any pending list items - if (currentListItems.length > 0 && !line.startsWith(' ')) { - flushListItems(); - } - - // Bold text (like **New Contributors**) - if (line.includes('**')) { - flushListItems(); // Flush any pending list items - const formatted = line.replace(/\*\*(.*?)\*\*/g, '$1'); - const withLinks = processLinks(formatted); - elements.push( -

    - ); - continue; - } - - // Full Changelog link - if (line.includes('Full Changelog:')) { - flushListItems(); // Flush any pending list items - const match = line.match(/https:\/\/github\.com\/[^\s\)\]]+/); - if (match) { - elements.push( -

    - Full Changelog:{' '} - - View diff - -

    - ); - continue; - } - } - - // Empty lines - if (line.trim() === '') { - flushListItems(); // Flush any pending list items - continue; - } - - // Regular text with link processing - flushListItems(); // Flush any pending list items - const withLinks = processLinks(line); - elements.push(

    ); - } - - // Flush any remaining list items - flushListItems(); - - return <>{elements}; -} diff --git a/docs/components/CopyMarkdownButton.tsx b/docs/components/CopyMarkdownButton.tsx deleted file mode 100644 index f6d8fc0e26..0000000000 --- a/docs/components/CopyMarkdownButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import * as React from 'react'; - -export function CopyMarkdownButton({ markdownUrl }: { markdownUrl: string }) { - const [state, setState] = React.useState<'idle' | 'copied' | 'error'>('idle'); - - async function onCopy() { - try { - setState('idle'); - const res = await fetch(markdownUrl, { cache: 'no-store' }); - if (!res.ok) throw new Error(`Failed to fetch markdown: ${res.status}`); - const md = await res.text(); - await navigator.clipboard.writeText(md); - setState('copied'); - window.setTimeout(() => setState('idle'), 1200); - } catch { - setState('error'); - window.setTimeout(() => setState('idle'), 1500); - } - } - - return ( - - ); -} diff --git a/docs/components/LastUpdated.tsx b/docs/components/LastUpdated.tsx deleted file mode 100644 index b0670491ad..0000000000 --- a/docs/components/LastUpdated.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export function LastUpdated({ date }: { date: Date }) { - if (!date || isNaN(date.getTime())) { - return null; - } - - const formatted = new Intl.DateTimeFormat(undefined, { - year: 'numeric', - month: 'long', - day: 'numeric', - }).format(date); - - return ( -

    -
    - Last updated on {formatted} -
    -
    - ); -} diff --git a/docs/content/docs/best-of-n.mdx b/docs/content/docs/best-of-n.mdx deleted file mode 100644 index 5c730617f8..0000000000 --- a/docs/content/docs/best-of-n.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Best of N -description: Run multiple agents in parallel and pick the best result ---- - -Run multiple agents on the same task, let them work in parallel, then pick the best output. Each agent gets its own worktree so they don't interfere with each other. - -Running multiple agents in parallel - -## Why Use It - -Different agents have different strengths. Running them in parallel lets you: - -- Compare solutions from different providers (Claude vs Codex vs Gemini) -- Get multiple attempts at a tricky problem -- Race agents to see which finishes first - -## How to Start - -When creating a task: - -1. Click the provider dropdown -2. Select multiple providers, or increase the count for a single provider (e.g., 3× Claude Code) -3. Click Create - -Emdash creates a separate git branch and worktree for each agent. They all start from the same base commit but work independently. - -Multiple agents working in separate worktrees - -## Working with Multiple Agents - -Each agent spawns in its own terminal tab. Switch between tabs to watch them work, or use the shared input bar to send the same message to all of them. - -## Comparing Results - -After agents finish, review their changes in the [diff view](/diff-view). Each agent's worktree has its own set of changes. Check the diff stats (files changed, lines added/removed) for a quick sense of each approach. - -Pick the best solution and merge that branch. Discard the rest. - -## Tips - -- Start with 2-3 agents. More than that gets hard to compare. -- Use the same initial prompt for fair comparison. -- Complex tasks benefit most from multiple perspectives. diff --git a/docs/content/docs/changelog.mdx b/docs/content/docs/changelog.mdx deleted file mode 100644 index 1a0eec111a..0000000000 --- a/docs/content/docs/changelog.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Changelog -description: Version history and release notes for Emdash ---- - -import { Changelog } from '../../components/Changelog'; - - - ---- - -## Release Process - -Emdash follows [Semantic Versioning](https://semver.org/): - -- **Patch versions** (0.0.x): Bug fixes and minor improvements -- **Minor versions** (0.x.0): New features and capabilities -- **Major versions** (x.0.0): Breaking changes - -## Contributing - -See our [Contributing Guide](/contributing) to learn how you can help improve Emdash. diff --git a/docs/content/docs/ci-checks.mdx b/docs/content/docs/ci-checks.mdx deleted file mode 100644 index ad892b4389..0000000000 --- a/docs/content/docs/ci-checks.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: CI/CD Checks -description: Monitor GitHub Actions check runs directly in Emdash ---- - -Emdash surfaces your GitHub Actions CI/CD status inside the app so you can monitor check runs without switching to a browser. - -CI/CD checks panel in Emdash - -## Viewing Checks - -Once a task has a Pull Request, a **Checks** tab appears in the file changes panel. Click it to see all check runs for that PR's branch. - -Each check shows: - -- **Status**: Green checkmark (pass), red X (fail), amber spinner (pending), or grey dash (skipped/cancelled) -- **Name and workflow**: Which check ran and from which workflow -- **Duration**: How long the check took -- **External link**: Click to open the full check run on GitHub - -A summary at the top shows the total count of passed, failed, and pending checks. - -## Auto-Refresh - -Emdash automatically polls for check updates: - -- **Every 10 seconds** while checks are still running -- **Every 60 seconds** once all checks complete -- **On window focus** — checks refresh when you switch back to Emdash - -Polling pauses when the app is in the background to save resources. - -## Requirements - -- The [GitHub CLI](https://cli.github.com/) (`gh`) must be installed and authenticated -- The task's branch must have an open Pull Request on GitHub -- Your repository must have GitHub Actions workflows configured - -If the Checks tab is disabled, it means no PR exists for the current task yet. Push a branch and open a PR to start seeing checks. - -## Tips - -- The Checks tab automatically activates when a PR exists but there are no local uncommitted changes. -- Failed checks show a red dot on the tab badge so you can spot failures at a glance. -- A spinning indicator on the tab badge means checks are still in progress. -- Click the external link icon on any check to jump straight to the run details on GitHub. diff --git a/docs/content/docs/contributing.mdx b/docs/content/docs/contributing.mdx deleted file mode 100644 index ecaab77fed..0000000000 --- a/docs/content/docs/contributing.mdx +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: Contributing -description: Help improve Emdash - contribution guidelines and development setup ---- - -Thanks for your interest in contributing! We favor small, focused PRs and clear intent over big bangs. This guide explains how to get set up, the workflow we use, and a few project‑specific conventions. - -## Quick Start - -### Prerequisites - -- **Node.js 20.0.0+ (recommended: 22.20.0)** and Git -- Optional (recommended for end‑to‑end testing): - - GitHub CLI (`brew install gh`; then `gh auth login`) - - At least one supported coding agent CLI (see [Providers](/providers)) - -### Setup - -```bash -# Fork this repo, then clone your fork -git clone https://github.com//emdash.git -cd emdash - -# Use the correct Node.js version (if using nvm) -nvm use - -# Quick start: install dependencies and run dev server -pnpm run d - -# Or run separately: -pnpm install -pnpm run dev - -# Type checking, lint, build -pnpm run type-check -pnpm run lint -pnpm run build -``` - -**Tip:** During development, the renderer hot‑reloads. Changes to the Electron main process (files in `src/main`) require a restart of the dev app. - -## Project Overview - -- `src/main/` – Electron main process, IPC handlers, services (Git, worktrees, PTY manager, DB, etc.) -- `src/renderer/` – React UI (Vite), hooks, components -- Local database – SQLite file created under the OS userData folder (see "Local DB" below) -- Worktrees – Git worktrees are created outside your repo root in a sibling `worktrees/` folder -- Logs – Agent terminal output and app logs are written to the OS userData folder (not inside repos) - -## Development Workflow - -### 1. Create a feature branch - -```bash -git checkout -b feat/ -``` - -### 2. Make changes and keep PRs small and focused - -- Prefer a series of small PRs over one large one. -- Include UI screenshots/GIFs when modifying the interface. -- Update docs (README or inline help) when behavior changes. - -### 3. Run checks locally - -```bash -pnpm run format # Format code with Prettier (required) -pnpm run type-check # TypeScript type checking -pnpm run lint # ESLint -pnpm run build # Build both main and renderer -``` - -Pre-commit hooks run automatically via Husky + lint-staged. On each commit, staged files are auto-formatted with Prettier and linted with ESLint. You don't need to remember to run these manually. Type checking and tests run in CI only since they need the full project context and are slower to execute. - -If you need to skip the hook for a work-in-progress commit, use `git commit --no-verify`. The checks will still run in CI when you open a PR. - -### 4. Commit using Conventional Commits - -- `feat:` – new user‑facing capability -- `fix:` – bug fix -- `chore:`, `refactor:`, `docs:`, `perf:`, `test:` etc. - -**Examples:** - -``` -fix(opencode): change initialPromptFlag from -p to --prompt for TUI - -feat(docs): add changelog tab with GitHub releases integration -``` - -### 5. Open a Pull Request - -- Describe the change, rationale, and testing steps. -- Link related Issues. -- Keep the PR title in Conventional Commit format if possible. - -## Code Style and Patterns - -### TypeScript + ESLint + Prettier - -Pre-commit hooks handle formatting and linting automatically on staged files. For full-project checks you can run them manually: - -- `pnpm run format` -- format all files with Prettier -- `pnpm run type-check` -- TypeScript type checking (whole project) -- `pnpm run lint` -- ESLint across all files -- `pnpm exec vitest run` -- run the test suite - -### Electron main (Node side) - -- Prefer `execFile` over `exec` to avoid shell quoting issues. -- Never write logs into Git worktrees. All logs belong in the Electron `userData` folder. -- Be conservative with console logging; noisy logs reduce signal. Use clear prefixes. - -### Git and worktrees - -- The app creates worktrees in a sibling `../worktrees/` folder. -- Do not delete worktree folders from Finder/Explorer; if you need cleanup, use: - - `git worktree prune` (from the main repo) - - or the in‑app workspace removal - -### Documentation - -When writing or updating docs, keep the tone clear and conversational. Use complete sentences and natural language rather than long bullet point lists. Avoid em dashes (—); use commas, periods, or rephrase instead. - -### Renderer (React) - -- Components live under `src/renderer/components`; hooks under `src/renderer/hooks`. -- Agent CLIs are embedded via terminal emulation (xterm.js) - each agent runs in its own PTY. -- Use existing UI primitives and Tailwind utility classes for consistency. -- Aim for accessible elements (labels, `aria-*` where appropriate). - -### Local DB (SQLite) - -**Location** (Electron `app.getPath('userData')`): - -- macOS: `~/Library/Application Support/emdash/emdash.db` -- Linux: `~/.config/emdash/emdash.db` -- Windows: `%APPDATA%\emdash\emdash.db` - -**Reset:** quit the app, delete the file, relaunch (the schema is recreated). - -## Issue Reports and Feature Requests - -Use GitHub Issues. Include: - -- OS, Node version -- Steps to reproduce -- Relevant logs (renderer console, terminal output) -- Screenshots/GIFs for UI issues - -## Release Process (maintainers) - -Use pnpm's built-in versioning to ensure consistency: - -```bash -# For bug fixes (0.2.9 → 0.2.10) -pnpm version patch - -# For new features (0.2.9 → 0.3.0) -pnpm version minor - -# For breaking changes (0.2.9 → 1.0.0) -pnpm version major -``` - -This automatically: - -1. Updates `package.json` and `pnpm-lock.yaml` -2. Creates a git commit with the version number (e.g., `"0.2.10"`) -3. Creates a git tag (e.g., `v0.2.10`) - -Then push to trigger the CI/CD pipeline. - -### What happens next - -Two GitHub Actions workflows trigger on version tags: - -**macOS Release** (`.github/workflows/release.yml`): - -1. Builds the TypeScript and Vite bundles -2. Signs the app with Apple Developer ID -3. Notarizes via Apple's notary service -4. Creates a GitHub Release with DMG artifacts for arm64 and x64 - -**Linux/Nix Build** (`.github/workflows/nix-build.yml`): - -1. Computes the correct dependency hash from `pnpm-lock.yaml` -2. Builds the x86_64-linux package via Nix flake -3. Pushes build artifacts to Cachix diff --git a/docs/content/docs/diff-view.mdx b/docs/content/docs/diff-view.mdx deleted file mode 100644 index 8c8ad22d8d..0000000000 --- a/docs/content/docs/diff-view.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Diff View -description: Review and manage code changes ---- - -The diff view shows all file changes in a task's worktree. Stage files, review diffs, write commit messages, and create pull requests from one panel. - -Diff view interface in Emdash - -## Where to Find It - -The diff view lives in the right sidebar when you have a task open. It updates automatically as the agent makes changes (polls every 5 seconds). - -## What You See - -For each changed file: - -- File path and type icon -- Lines added (green) and removed (red) -- Staged or unstaged status - -The header shows total files changed, overall additions/deletions, and PR status if a pull request exists. - -## Actions - -**Stage a file**: Click the + icon to add a file to the staging area. - -**Unstage or revert**: Click the undo icon. For staged files, this unstages them. For unstaged files, this discards all changes (resets to last commit). - -**View diff**: Click a file to open it in the diff viewer. You can edit the file directly in the diff view and save your changes. - -**View all changes**: Click "View All" to see diffs for every changed file in one scrollable view. - -**Commit and push**: Type a commit message and press Enter. Emdash commits staged changes and pushes to the branch. - -**Create PR**: After pushing, a "Create PR" button appears if your branch is ahead of main. - -## Inline Editing - -The diff viewer isn't read-only. Edit the modified version directly, then save. This is useful for quick fixes without switching to your editor. diff --git a/docs/content/docs/file-editor.mdx b/docs/content/docs/file-editor.mdx deleted file mode 100644 index 3243aa564a..0000000000 --- a/docs/content/docs/file-editor.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: File Editor -description: Edit files directly in Emdash ---- - -The file editor lets you browse and edit code in a task's worktree without switching to an external editor. Use it to explore the codebase, make edits yourself, or review what an agent has done. - -File editor interface in Emdash - -## Opening the Editor - -Click the code icon in the titlebar when viewing a task. The editor opens in a side panel with a file tree on the left and the code editor on the right. - -## Navigating Files - -The left panel shows your project's file tree. Click a file to open it. Files open in tabs at the top of the editor, so you can switch between multiple files. - -Common system directories like `node_modules`, `.git`, and build output are hidden by default. Click the eye icon to show or hide them. - -## Editing - -The editor is a full-featured code editor with syntax highlighting, find/replace, and the usual keyboard shortcuts. Changes auto-save after 2 seconds of inactivity, or press `⌘S` to save immediately. - -Git diff markers appear in the gutter: - -- Green dots for added lines -- Orange dots for modified lines -- Red markers for deleted lines - -These update automatically as you edit, so you can see what's changed compared to the last commit. - -## Saving - -Files with unsaved changes show a dot in their tab. Use `⌘⇧S` to save all open files at once, or click "Save All" in the header. - -After saving, the [diff view](/diff-view) updates to reflect your changes. - -## Images - -The editor also handles images. Click an image file to preview it instead of showing raw bytes. - -## Resizing - -Drag the divider between the file tree and editor to adjust panel widths. The layout remembers your preference. diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx deleted file mode 100644 index b1caebf069..0000000000 --- a/docs/content/docs/index.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Emdash Overview -description: An Open Source Agentic Development Environment (ADE) ---- - -Emdash is an open source desktop app for running multiple coding agents in parallel. Each agent works in an isolated Git worktree, so they don't interfere with each other. - -## Capabilities - -- **[Parallel agents](/parallel-agents):** Run multiple agents simultaneously, each in its own worktree -- **[Provider support](/providers):** Use any of 18+ CLI-based agents: Claude Code, Codex, Gemini, OpenCode, and more -- **[Best-of-N](/best-of-n):** Run multiple agents on the same task and pick the best result -- **[Diff view](/diff-view):** Review changes across agents side-by-side -- **[Kanban view](/kanban-view):** Organize tasks visually across your workflow -- **[Issue integration](/issues):** Pull tasks from Linear, Jira, or GitHub Issues directly - -## How It Works - -Click "Add Task" to create one or more worktrees. Each worktree runs its own agent. You can then review the diff when done, iterate if needed, and open a PR inside of Emdash. - -## Demo - -