diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4015fe6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM jcabillot/opencode:latest + +ARG OPENCHAMBER_WEB_VERSION=1.9.9 + +ENV NPM_CONFIG_UPDATE_NOTIFIER=false \ + NPM_CONFIG_LOGLEVEL=warn \ + NODE_ENV=production + +USER root + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* \ + && npm install -g --no-fund --no-audit "@openchamber/web@${OPENCHAMBER_WEB_VERSION}" \ + && openchamber --version \ + && chown -R opencode:opencode /home/opencode + +WORKDIR /home/opencode/ +USER opencode + +EXPOSE 3000 + +ENTRYPOINT ["openchamber"] +CMD ["serve", "--host", "0.0.0.0", "--port", "3000", "--foreground"] diff --git a/Jenkinsfile b/Jenkinsfile index 1e6267b..ca4d077 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -20,7 +20,7 @@ pipeline { stage('Build image') { steps{ - sh 'docker build --force-rm=true --no-cache=true --pull -f src/Dockerfile -t ${dockerImage} src/' + sh 'docker build --force-rm=true --no-cache=true --pull -f Dockerfile -t ${dockerImage} .' } } diff --git a/src/.dockerignore b/src/.dockerignore deleted file mode 100644 index 3ba6be7..0000000 --- a/src/.dockerignore +++ /dev/null @@ -1,26 +0,0 @@ -.git -.gitignore -node_modules -**/node_modules -dist -**/dist -build -**/build -.DS_Store -.idea -.vscode -coverage -tmp -logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -.env -.env.* -.opencode -data -workspaces -packages/desktop/src-tauri -packages/desktop/target -packages/intellij diff --git a/src/.github/CODEOWNERS b/src/.github/CODEOWNERS deleted file mode 100644 index af8017b..0000000 --- a/src/.github/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# .github/CODEOWNERS -* @btriapitsyn diff --git a/src/.github/ISSUE_TEMPLATE/bug_report.yml b/src/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 6a62a81..0000000 --- a/src/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Bug report -description: Report something broken -title: "[Bug] " -labels: [bug] -body: - - type: textarea - id: what - attributes: - label: What's wrong? Maybe some steps to reproduce. - description: What happened and what you expected. - placeholder: "Expected … but got …" - validations: - required: true - - - type: dropdown - id: runtime - attributes: - label: Where does it happen? - options: - - Desktop (macOS) - - Desktop Web - - Mobile (Web/PWA) - - VS Code extension - - Not sure - validations: - required: true - - - type: input - id: version - attributes: - label: Version (if known) - placeholder: "e.g. 1.2.3" - - - type: textarea - id: screenshots - attributes: - label: Screenshots / recordings (optional) - description: "Anything visual that helps." - - - type: textarea - id: logs - attributes: - label: Logs (optional) - description: "Paste relevant logs." - render: shell diff --git a/src/.github/ISSUE_TEMPLATE/config.yml b/src/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 3ba13e0..0000000 --- a/src/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: false diff --git a/src/.github/ISSUE_TEMPLATE/feature_request.yml b/src/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index d58d57e..0000000 --- a/src/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Feature request -description: Suggest an improvement -title: "[Feature Request] " -labels: [enhancement] -body: - - type: textarea - id: request - attributes: - label: What should we add/change? - description: What you're trying to do + what you'd like to happen. - placeholder: | - I'm trying to … - - It would be great if OpenChamber … - validations: - required: true - - - type: textarea - id: context - attributes: - label: Extra context (optional) - description: Links, screenshots, mockups, constraints, etc. diff --git a/src/.github/workflows/build-macos-arm64-dmg.yml b/src/.github/workflows/build-macos-arm64-dmg.yml deleted file mode 100644 index 71aeaa0..0000000 --- a/src/.github/workflows/build-macos-arm64-dmg.yml +++ /dev/null @@ -1,201 +0,0 @@ -name: Build macOS DMG (arm64) - -on: - workflow_dispatch: - inputs: - macos_version: - description: macOS runner version - required: true - type: choice - options: - - "macos-15" - - "macos-26" - default: "macos-15" - ref: - description: Git ref to build (branch, tag, or sha) - required: false - default: "" - -env: - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: short - -jobs: - build-macos-dmg-arm64: - name: Build DMG (arm64, ${{ inputs.macos_version }}) - runs-on: ${{ inputs.macos_version }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref || github.ref }} - - - name: Setup bun - uses: oven-sh/setup-bun@v2 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-darwin - - - name: Rust cache - uses: swatinem/rust-cache@v2 - with: - workspaces: packages/desktop/src-tauri - key: aarch64-apple-darwin - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Install Apple Certificate - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - KEYCHAIN_PASSWORD=$(openssl rand -base64 32) - - security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 - security import $RUNNER_TEMP/certificate.p12 \ - -P "$APPLE_CERTIFICATE_PASSWORD" \ - -A -t cert -f pkcs12 \ - -k "$KEYCHAIN_PATH" - - security list-keychain -d user -s "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - - name: Set up notarization credentials - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - run: | - if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then - echo "Error: Missing Apple notarization credentials" - exit 1 - fi - - xcrun notarytool store-credentials "openchamber-notarize" \ - --apple-id "$APPLE_ID" \ - --team-id "$APPLE_TEAM_ID" \ - --password "$APPLE_PASSWORD" - - - name: Build UI package - run: bun run --cwd packages/ui build - - - name: Build Desktop app (arm64) - run: bun run --cwd packages/desktop build && bun run --cwd packages/desktop tauri build --target aarch64-apple-darwin - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - - - name: Prepare DMG artifact - run: | - set -euo pipefail - mkdir -p artifacts - DMG_PATH="packages/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg" - if ls $DMG_PATH 1> /dev/null 2>&1; then - DMG_FILE=$(ls $DMG_PATH | head -n 1) - DMG_NAME="OpenChamber_${{ inputs.macos_version }}_arm64.dmg" - cp "$DMG_FILE" "artifacts/$DMG_NAME" - else - echo "Error: DMG file not found at $DMG_PATH" - exit 1 - fi - - - name: Upload DMG artifact - uses: actions/upload-artifact@v4 - with: - name: dmg-${{ inputs.macos_version }}-arm64 - path: artifacts/*.dmg - retention-days: 7 - - build-macos-dmg-arm64-electron: - name: Build Electron DMG (arm64, ${{ inputs.macos_version }}) - runs-on: ${{ inputs.macos_version }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref || github.ref }} - - - name: Setup bun - uses: oven-sh/setup-bun@v2 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Install Apple Certificate - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db - KEYCHAIN_PASSWORD=$(openssl rand -base64 32) - - security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 - security import $RUNNER_TEMP/certificate.p12 \ - -P "$APPLE_CERTIFICATE_PASSWORD" \ - -A -t cert -f pkcs12 \ - -k "$KEYCHAIN_PATH" - - security list-keychain -d user -s "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - - name: Build Electron app (arm64) - working-directory: packages/electron - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - ELECTRON_BUILDER_ARCH: arm64 - run: | - bun run build:web-assets - bun run bundle:main - bun run rebuild:native - ./node_modules/.bin/electron-builder --mac --arm64 --publish=never - - - name: Prepare DMG artifact - run: | - set -euo pipefail - mkdir -p artifacts - DMG_PATH="packages/electron/dist/*.dmg" - if ls $DMG_PATH 1> /dev/null 2>&1; then - DMG_FILE=$(ls $DMG_PATH | head -n 1) - DMG_NAME="OpenChamber_Electron_${{ inputs.macos_version }}_arm64.dmg" - cp "$DMG_FILE" "artifacts/$DMG_NAME" - else - echo "Error: DMG file not found at $DMG_PATH" - exit 1 - fi - - - name: Upload DMG artifact - uses: actions/upload-artifact@v4 - with: - name: dmg-electron-${{ inputs.macos_version }}-arm64 - path: artifacts/*.dmg - retention-days: 7 diff --git a/src/.github/workflows/docs-source.yml b/src/.github/workflows/docs-source.yml deleted file mode 100644 index 05feca6..0000000 --- a/src/.github/workflows/docs-source.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Docs Source - -on: - push: - branches: [main] - paths: - - "packages/docs/**" - - "scripts/docs/**" - - "package.json" - release: - types: [published] - workflow_dispatch: - inputs: - release_tag: - description: "Optional existing tag to upload docs source archive" - required: false - type: string - -permissions: - contents: write - -jobs: - validate-and-package: - runs-on: ubuntu-latest - outputs: - archive_name: ${{ steps.archive.outputs.archive_name }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup bun - uses: oven-sh/setup-bun@v2 - - - name: Validate docs source - run: bun run docs:validate - - - name: Build docs source archive - id: archive - run: | - mkdir -p artifacts - ARCHIVE_NAME="openchamber-docs-source-${GITHUB_SHA::8}.tar.gz" - tar -czf "artifacts/${ARCHIVE_NAME}" -C packages/docs . - echo "archive_name=${ARCHIVE_NAME}" >> "$GITHUB_OUTPUT" - - - name: Upload workflow artifact - uses: actions/upload-artifact@v4 - with: - name: docs-source - path: artifacts/${{ steps.archive.outputs.archive_name }} - retention-days: 14 - - - name: Upload archive to release tag - if: ${{ github.event_name == 'release' || github.event.inputs.release_tag != '' }} - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag }} - files: artifacts/${{ steps.archive.outputs.archive_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Trigger openchamber-website docs sync (optional) - if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }} - env: - WEBSITE_REPO: openchamber/openchamber-website - WEBSITE_TOKEN: ${{ secrets.OPENCHAMBER_WEBSITE_REPO_TOKEN }} - SOURCE_REF: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref_name }} - run: | - if [ -z "$WEBSITE_TOKEN" ]; then - echo "OPENCHAMBER_WEBSITE_REPO_TOKEN not set; skip dispatch." - exit 0 - fi - - curl -sS -X POST \ - -H "Authorization: Bearer $WEBSITE_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/$WEBSITE_REPO/dispatches \ - -d @- <> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == refs/tags/* ]]; then - echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - else - echo "version=0.0.0-dev" >> $GITHUB_OUTPUT - fi - - - name: Extract changelog for release - env: - VERSION: ${{ steps.get_version.outputs.version }} - run: | - node - <<'NODE' - const fs = require('fs'); - const version = process.env.VERSION; - const changelogPath = 'CHANGELOG.md'; - if (!fs.existsSync(changelogPath)) { - throw new Error('CHANGELOG.md not found; add it before releasing.'); - } - const changelog = fs.readFileSync(changelogPath, 'utf8'); - const sections = changelog.split(/^## /m); - const section = sections.find(s => s.startsWith('[' + version + ']')); - if (!section) { - throw new Error('Changelog section [' + version + '] not found. Add a section like "## [' + version + '] - YYYY-MM-DD".'); - } - const content = ('## ' + section).trim(); - fs.mkdirSync('artifacts', { recursive: true }); - fs.writeFileSync('artifacts/release-notes.md', content + '\n'); - NODE - - - name: Create GitHub Release - id: create_release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.get_version.outputs.version }} - draft: true - generate_release_notes: false - body_path: artifacts/release-notes.md - name: OpenChamber v${{ steps.get_version.outputs.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - build-desktop-macos: - needs: create-release - runs-on: macos-26 - strategy: - fail-fast: false - matrix: - target: [aarch64-apple-darwin, x86_64-apple-darwin] - include: - - target: aarch64-apple-darwin - arch: aarch64 - platform: darwin-aarch64 - - target: x86_64-apple-darwin - arch: x86_64 - platform: darwin-x86_64 - outputs: - version: ${{ needs.create-release.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Setup bun - uses: oven-sh/setup-bun@v2 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Rust cache - uses: swatinem/rust-cache@v2 - with: - workspaces: packages/desktop/src-tauri - key: ${{ matrix.target }} - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Install Apple Certificate - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - # Create temporary keychain - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - KEYCHAIN_PASSWORD=$(openssl rand -base64 32) - - security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - # Import certificate - echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 - security import $RUNNER_TEMP/certificate.p12 \ - -P "$APPLE_CERTIFICATE_PASSWORD" \ - -A -t cert -f pkcs12 \ - -k "$KEYCHAIN_PATH" - - security list-keychain -d user -s "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - - name: Set up notarization credentials - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - run: | - # Validate secrets are set - if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then - echo "Error: Missing Apple notarization credentials" - exit 1 - fi - - xcrun notarytool store-credentials "openchamber-notarize" \ - --apple-id "$APPLE_ID" \ - --team-id "$APPLE_TEAM_ID" \ - --password "$APPLE_PASSWORD" - - - name: Build UI package - run: bun run --cwd packages/ui build - - - name: Build Desktop app - # Note: We use inline commands instead of desktop:build to pass architecture-specific --target flag - # This enables cross-compilation for both arm64 and x86_64 from the same runner - run: | - export TAURI_ENV_TARGET_TRIPLE=${{ matrix.target }} - bun run --cwd packages/desktop build - bun run --cwd packages/desktop tauri build --target ${{ matrix.target }} - env: - TAURI_ENV_TARGET_TRIPLE: ${{ matrix.target }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - - - name: Verify binary architectures - run: | - set -euo pipefail - - BUNDLE_DIR="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos" - - if [ ! -d "$BUNDLE_DIR" ]; then - echo "❌ Error: bundle directory not found: $BUNDLE_DIR" - exit 1 - fi - - APP_PATH=$(find "$BUNDLE_DIR" -maxdepth 2 -name "*.app" -print -quit) - if [ -z "$APP_PATH" ]; then - echo "❌ Error: .app bundle not found under $BUNDLE_DIR" - exit 1 - fi - - echo "🔍 Verifying binary architectures in $APP_PATH" - - # Extract raw architecture names (macOS file command reports ARM as "arm64") - MAIN_ARCH_RAW=$(file "$APP_PATH/Contents/MacOS/openchamber-desktop" | grep -oE 'arm64|x86_64|aarch64' | head -1) - SIDEARCH_ARCH_RAW=$(file "$APP_PATH/Contents/MacOS/openchamber-server" | grep -oE 'arm64|x86_64|aarch64' | head -1) - - # Normalize architecture names (arm64 -> aarch64 for consistency with Rust/Tauri) - normalize_arch() { - case "$1" in - arm64) echo "aarch64" ;; - aarch64|x86_64) echo "$1" ;; - *) echo "unknown" ;; - esac - } - - MAIN_ARCH=$(normalize_arch "$MAIN_ARCH_RAW") - SIDEARCH_ARCH=$(normalize_arch "$SIDEARCH_ARCH_RAW") - EXPECTED_ARCH=$(echo "${{ matrix.target }}" | grep -oE 'aarch64|x86_64' | head -1) - - echo " Main: $MAIN_ARCH_RAW → $MAIN_ARCH" - echo " Sidecar: $SIDEARCH_ARCH_RAW → $SIDEARCH_ARCH" - echo " Expected: $EXPECTED_ARCH" - - if [ "$MAIN_ARCH" != "$EXPECTED_ARCH" ]; then - echo "❌ ERROR: Main binary architecture mismatch!" - echo " Expected: $EXPECTED_ARCH" - echo " Got: $MAIN_ARCH (raw: $MAIN_ARCH_RAW)" - exit 1 - fi - - if [ "$SIDEARCH_ARCH" != "$EXPECTED_ARCH" ]; then - echo "❌ ERROR: Sidecar binary architecture mismatch!" - echo " Expected: $EXPECTED_ARCH" - echo " Got: $SIDEARCH_ARCH (raw: $SIDEARCH_ARCH_RAW)" - exit 1 - fi - - echo "✅ Architecture verification passed: both binaries match $EXPECTED_ARCH" - - - name: Verify macOS entitlements - run: | - set -euo pipefail - - BUNDLE_DIR="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos" - - if [ ! -d "$BUNDLE_DIR" ]; then - echo "Error: bundle directory not found: $BUNDLE_DIR" - exit 1 - fi - - APP_PATH=$(find "$BUNDLE_DIR" -maxdepth 2 -name "*.app" -print -quit) - if [ -z "$APP_PATH" ]; then - echo "Error: .app bundle not found under $BUNDLE_DIR" - echo "Contents:"; ls -la "$BUNDLE_DIR" - exit 1 - fi - - echo "Verifying app bundle: $APP_PATH" - codesign -vv "$APP_PATH" - - ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true) - echo "$ENTITLEMENTS" - - if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then - echo "Error: app sandbox entitlement is present" - exit 1 - fi - - for key in \ - com.apple.security.cs.allow-jit \ - com.apple.security.cs.allow-unsigned-executable-memory \ - com.apple.security.cs.disable-executable-page-protection \ - com.apple.security.cs.disable-library-validation - do - if ! echo "$ENTITLEMENTS" | grep -q "$key"; then - echo "Error: required entitlement missing: $key" - exit 1 - fi - done - - - name: Prepare release artifacts - run: | - mkdir -p artifacts - VERSION="${{ needs.create-release.outputs.version }}" - - # Copy DMG (Tauri names it with the target triple in the path) - DMG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg" - if ls $DMG_PATH 1> /dev/null 2>&1; then - DMG_FILE=$(ls $DMG_PATH | head -n 1) - DMG_NAME="OpenChamber_${VERSION}_${{ matrix.platform }}.dmg" - cp "$DMG_FILE" "artifacts/$DMG_NAME" - else - echo "Error: DMG file not found at $DMG_PATH" - exit 1 - fi - - # Copy tar.gz and signature for updater - TAR_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz" - SIG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig" - - if ls $TAR_PATH 1> /dev/null 2>&1; then - TAR_FILE=$(ls $TAR_PATH | head -n 1) - TAR_BASE=$(basename "$TAR_FILE") - TAR_NAME="${TAR_BASE%.tar.gz}-${{ matrix.platform }}.tar.gz" - cp "$TAR_FILE" "artifacts/$TAR_NAME" - else - echo "Error: tar.gz file not found at $TAR_PATH" - exit 1 - fi - - if ls $SIG_PATH 1> /dev/null 2>&1; then - SIG_FILE=$(ls $SIG_PATH | head -n 1) - SIG_BASE=$(basename "$SIG_FILE") - SIG_NAME="${SIG_BASE%.tar.gz.sig}-${{ matrix.platform }}.tar.gz.sig" - cp "$SIG_FILE" "artifacts/$SIG_NAME" - else - echo "Error: signature file not found at $SIG_PATH" - exit 1 - fi - - echo "Successfully prepared artifacts:" - ls -lh artifacts/ - - - name: Generate update manifest - run: | - VERSION="${{ needs.create-release.outputs.version }}" - - # Find the signature file for this platform - SIG_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz.sig" | head -1) - if [ -f "$SIG_FILE" ]; then - SIGNATURE=$(cat "$SIG_FILE") - else - SIGNATURE="" - fi - - # Find the tar.gz file name for this platform - TAR_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz" ! -name "*.sig" | head -1) - TAR_NAME=$(basename "$TAR_FILE" 2>/dev/null || echo "OpenChamber-${{ matrix.platform }}.app.tar.gz") - - cat > artifacts/latest-${{ matrix.platform }}.json << EOF - { - "version": "${VERSION}", - "notes": "See release notes at https://github.com/${{ github.repository }}/releases/tag/v${VERSION}", - "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", - "platforms": { - "${{ matrix.platform }}": { - "signature": "${SIGNATURE}", - "url": "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${TAR_NAME}" - } - } - } - EOF - - echo "Generated latest-${{ matrix.platform }}.json:" - cat artifacts/latest-${{ matrix.platform }}.json - - - name: Upload release assets - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - files: | - artifacts/*.dmg - artifacts/*.tar.gz - artifacts/*.tar.gz.sig - artifacts/latest-${{ matrix.platform }}.json - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload manifest as artifact - uses: actions/upload-artifact@v4 - with: - name: manifest-${{ matrix.platform }} - path: artifacts/latest-${{ matrix.platform }}.json - retention-days: 1 - - publish-npm: - needs: create-release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup bun - uses: oven-sh/setup-bun@v2 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - registry-url: 'https://registry.npmjs.org' - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Build packages - run: bun run build - - - name: Create npm tarball - working-directory: packages/web - run: npm pack - - - name: Upload npm tarball to release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - files: packages/web/*.tgz - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish to npm - if: ${{ github.event.inputs.dry_run != 'true' }} - working-directory: packages/web - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - combine-manifests: - needs: [create-release, build-desktop-macos] - runs-on: ubuntu-latest - steps: - - name: Download aarch64 manifest - uses: actions/download-artifact@v4 - with: - name: manifest-darwin-aarch64 - path: artifacts - - - name: Download x86_64 manifest - uses: actions/download-artifact@v4 - with: - name: manifest-darwin-x86_64 - path: artifacts - - - name: Combine manifests - run: | - VERSION="${{ needs.create-release.outputs.version }}" - PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - REPO="${{ github.repository }}" - - # Validate that both manifest files exist and are valid JSON - if [ ! -f artifacts/latest-darwin-aarch64.json ]; then - echo "Error: aarch64 manifest not found" - exit 1 - fi - - if [ ! -f artifacts/latest-darwin-x86_64.json ]; then - echo "Error: x86_64 manifest not found" - exit 1 - fi - - # Validate JSON structure - if ! jq empty artifacts/latest-darwin-aarch64.json 2>/dev/null; then - echo "Error: aarch64 manifest is not valid JSON" - exit 1 - fi - - if ! jq empty artifacts/latest-darwin-x86_64.json 2>/dev/null; then - echo "Error: x86_64 manifest is not valid JSON" - exit 1 - fi - - # Validate platform data exists in manifests - if ! jq -e '.platforms["darwin-aarch64"]' artifacts/latest-darwin-aarch64.json > /dev/null; then - echo "Error: darwin-aarch64 platform data not found in manifest" - exit 1 - fi - - if ! jq -e '.platforms["darwin-x86_64"]' artifacts/latest-darwin-x86_64.json > /dev/null; then - echo "Error: darwin-x86_64 platform data not found in manifest" - exit 1 - fi - - # Use jq to properly combine the manifests - jq -n \ - --arg version "$VERSION" \ - --arg notes "See release notes at https://github.com/${REPO}/releases/tag/v${VERSION}" \ - --arg pub_date "$PUB_DATE" \ - --slurpfile aarch64 artifacts/latest-darwin-aarch64.json \ - --slurpfile x86_64 artifacts/latest-darwin-x86_64.json \ - '{ - version: $version, - notes: $notes, - pub_date: $pub_date, - platforms: { - "darwin-aarch64": $aarch64[0].platforms["darwin-aarch64"], - "darwin-x86_64": $x86_64[0].platforms["darwin-x86_64"] - } - }' > artifacts/latest.json - - echo "Generated combined latest.json:" - cat artifacts/latest.json - - - name: Upload combined manifest - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - files: artifacts/latest.json - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - build-desktop-electron-macos: - needs: create-release - runs-on: macos-26 - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - arch: arm64 - platform: darwin-aarch64 - - target: x86_64-apple-darwin - arch: x64 - platform: darwin-x86_64 - steps: - - uses: actions/checkout@v4 - - - name: Setup bun - uses: oven-sh/setup-bun@v2 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Install Apple Certificate - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db - KEYCHAIN_PASSWORD=$(openssl rand -base64 32) - - security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 - security import $RUNNER_TEMP/certificate.p12 \ - -P "$APPLE_CERTIFICATE_PASSWORD" \ - -A -t cert -f pkcs12 \ - -k "$KEYCHAIN_PATH" - - security list-keychain -d user -s "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - - name: Build Electron app - working-directory: packages/electron - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - # rebuild-native.mjs reads this to target the right arch when - # cross-building (runner is arm64; x64 matrix needs the hint). - ELECTRON_BUILDER_ARCH: ${{ matrix.arch }} - run: | - bun run build:web-assets - bun run bundle:main - # npmRebuild=false in package.json, so electron-builder won't - # recompile native deps on its own — we must rebuild against the - # target Electron ABI before packaging, otherwise better-sqlite3/ - # node-pty/bun-pty crash on require inside the packaged app. - bun run rebuild:native - ./node_modules/.bin/electron-builder --mac --${{ matrix.arch }} --publish=never - - - name: Verify signature + entitlements + notarization - run: | - set -euo pipefail - - APP_DIR="packages/electron/dist/mac" - [ -d "packages/electron/dist/mac-arm64" ] && APP_DIR="packages/electron/dist/mac-arm64" - - APP_PATH=$(find "$APP_DIR" -maxdepth 2 -name "*.app" -print -quit) - if [ -z "$APP_PATH" ]; then - echo "Error: .app not found under packages/electron/dist/mac*" - ls -la packages/electron/dist/ - exit 1 - fi - - echo "Verifying $APP_PATH" - codesign -vv --deep --strict "$APP_PATH" - - # Require hardened runtime - CS_INFO=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1) - echo "$CS_INFO" - if ! echo "$CS_INFO" | grep -q "flags=.*runtime"; then - echo "Error: hardened runtime flag missing" - exit 1 - fi - - # Require notary ticket stapled - xcrun stapler validate "$APP_PATH" - - ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true) - if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then - echo "Error: app sandbox entitlement is present" - exit 1 - fi - for key in \ - com.apple.security.cs.allow-jit \ - com.apple.security.cs.allow-unsigned-executable-memory \ - com.apple.security.cs.disable-library-validation - do - if ! echo "$ENTITLEMENTS" | grep -q "$key"; then - echo "Error: required entitlement missing: $key" - exit 1 - fi - done - - - name: Upload DMG / ZIP / blockmaps to release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - files: | - packages/electron/dist/*.dmg - packages/electron/dist/*.zip - packages/electron/dist/*.blockmap - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload per-arch latest-mac.yml for merge - uses: actions/upload-artifact@v4 - with: - name: latest-yml-${{ matrix.target }} - path: packages/electron/dist/latest-mac.yml - retention-days: 1 - - combine-electron-manifests: - needs: [create-release, build-desktop-electron-macos] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Download per-arch latest-mac.yml - uses: actions/download-artifact@v4 - with: - pattern: latest-yml-*-apple-darwin - path: artifacts - - - name: Finalize combined latest-mac.yml - env: - LATEST_YML_DIR: ${{ github.workspace }}/artifacts - GH_REPO: ${{ github.repository }} - OPENCHAMBER_VERSION: ${{ needs.create-release.outputs.version }} - run: node packages/electron/scripts/finalize-latest-yml.mjs - - - name: Upload combined latest-mac.yml to release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - files: ${{ runner.temp }}/latest-mac.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - finalize-release: - needs: [create-release, build-desktop-macos, build-desktop-electron-macos, publish-npm, combine-manifests, combine-electron-manifests] - runs-on: ubuntu-latest - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - DISCORD_UPDATE_ROLE_ID: ${{ secrets.DISCORD_UPDATE_ROLE_ID }} - steps: - - name: Publish release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - draft: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Send release to Discord - if: ${{ env.DISCORD_WEBHOOK_URL != '' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ needs.create-release.outputs.version }} - REPOSITORY: ${{ github.repository }} - UPDATE_ROLE_ID: ${{ env.DISCORD_UPDATE_ROLE_ID }} - run: | - node - <<'NODE' - (async () => { - const tag = `v${process.env.VERSION}`; - const repo = process.env.REPOSITORY; - const rawRoleId = (process.env.UPDATE_ROLE_ID || '').trim(); - const updateRoleId = /^\d+$/.test(rawRoleId) ? rawRoleId : ''; - - const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, { - headers: { - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - Accept: 'application/vnd.github+json', - }, - }); - - if (!releaseRes.ok) { - const body = await releaseRes.text(); - throw new Error(`Failed to fetch release ${tag}: ${releaseRes.status} ${body}`); - } - - const release = await releaseRes.json(); - const description = (release.body || `OpenChamber ${tag} released.`).slice(0, 4096); - const mention = updateRoleId ? `<@&${updateRoleId}>` : ''; - - const payload = { - username: 'OpenChamber Releases', - ...(mention ? { content: mention } : {}), - ...(updateRoleId - ? { - allowed_mentions: { - roles: [updateRoleId], - }, - } - : {}), - embeds: [ - { - title: release.name || `OpenChamber ${tag}`, - url: release.html_url, - description, - color: 2105893, - footer: { text: 'OpenChamber Changelog' }, - }, - ], - }; - - const discordRes = await fetch(process.env.DISCORD_WEBHOOK_URL, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(payload), - }); - - if (!discordRes.ok) { - const body = await discordRes.text(); - throw new Error(`Failed to send Discord release: ${discordRes.status} ${body}`); - } - })().catch((error) => { - console.error(error); - process.exit(1); - }); - NODE - - - name: Trigger openchamber-website site refresh (optional) - env: - WEBSITE_REPO: openchamber/openchamber-website - WEBSITE_TOKEN: ${{ secrets.OPENCHAMBER_WEBSITE_REPO_TOKEN }} - VERSION: ${{ needs.create-release.outputs.version }} - run: | - if [ -z "$WEBSITE_TOKEN" ]; then - echo "OPENCHAMBER_WEBSITE_REPO_TOKEN not set; skip site refresh dispatch." - exit 0 - fi - - curl --fail-with-body -sS -X POST \ - -H "Authorization: Bearer $WEBSITE_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/$WEBSITE_REPO/dispatches \ - -d @- </dev/null || git rev-list --max-parents=0 HEAD); echo "Base: $BASE"; echo "Commits since base: $(git rev-list --count "$BASE"..HEAD)"; echo "Diff stats: $(git diff --shortstat "$BASE"..HEAD)"; echo; echo "=== Top 30 commits ==="; git log --oneline -30 "$BASE"..HEAD; echo; echo "=== Changed files ==="; git diff --stat "$BASE"..HEAD` - -Additional hints (optional, use only if needed): -- If there are breaking changes or user-visible behavior changes, call them out first. -- If changes are mostly internal refactors, summarize them as reliability/performance improvements. - -Now: -1) Propose the new `[Unreleased]` bullet list for the main @CHANGELOG.md. -2) Propose the VS Code-specific `[Unreleased]` list for @packages/vscode/CHANGELOG.md. -3) Edit both files to update their respective `[Unreleased]` sections. diff --git a/src/.opencode/package-lock.json b/src/.opencode/package-lock.json deleted file mode 100644 index fc32819..0000000 --- a/src/.opencode/package-lock.json +++ /dev/null @@ -1,376 +0,0 @@ -{ - "name": ".opencode", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@opencode-ai/plugin": "1.4.10" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@opencode-ai/plugin": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.10.tgz", - "integrity": "sha512-35Za2LT2oNWnBoonmPjN1Z9PB4+ir2a6GbZ3nIZQQL/96mqzTRkT1FqUkQc3bdMmfT1R1rqOd5aMzkIXMqC7dA==", - "license": "MIT", - "dependencies": { - "@opencode-ai/sdk": "1.4.10", - "effect": "4.0.0-beta.48", - "zod": "4.1.8" - }, - "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } - } - }, - "node_modules/@opencode-ai/sdk": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.10.tgz", - "integrity": "sha512-Yaddcs/COp0hwiCxgobSZyDUN0nHgkEFL4bG0BQxwd52SGAysOr6A6L0ihfkuhVx0kbi9eXWgZk4ydNOrnur5w==", - "license": "MIT", - "dependencies": { - "cross-spawn": "7.0.6" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/effect": { - "version": "4.0.0-beta.48", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", - "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "fast-check": "^4.6.0", - "find-my-way-ts": "^0.1.6", - "ini": "^6.0.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^1.11.9", - "multipasta": "^0.2.7", - "toml": "^4.1.1", - "uuid": "^13.0.0", - "yaml": "^2.8.3" - } - }, - "node_modules/fast-check": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz", - "integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^8.0.0" - }, - "engines": { - "node": ">=12.17.0" - } - }, - "node_modules/find-my-way-ts": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", - "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", - "license": "MIT" - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", - "license": "Apache-2.0" - }, - "node_modules/msgpackr": { - "version": "1.11.9", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz", - "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/multipasta": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", - "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pure-rand": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", - "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/toml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", - "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zod": { - "version": "4.1.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/src/.opencode/skills/clack-cli-patterns/SKILL.md b/src/.opencode/skills/clack-cli-patterns/SKILL.md deleted file mode 100644 index 23e9468..0000000 --- a/src/.opencode/skills/clack-cli-patterns/SKILL.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -name: clack-cli-patterns -description: Use when creating or modifying terminal CLI commands, prompts, or output formatting in OpenChamber. Enforces Clack UX standards with strict parity and safety across TTY/non-TTY, --quiet, and --json modes. -license: MIT -compatibility: opencode ---- - -## Overview - -OpenChamber terminal CLI uses `@clack/prompts` for interactive UX, but command policy and validation must be mode-agnostic. - -**Core principle:** policy-first, UX-second. Clack is presentation, not enforcement. - -## Scope - -Use this skill for terminal CLI work only (for example `packages/web/bin/*`). - -Do not use this skill for web UI or VS Code webview styling work. - -## Mandatory Rules - -1. **Validation first** - - Safety and correctness checks must run in all modes. - - Prompts may help collect input, but cannot be the only guard. - -2. **Mode parity is required** - - Behavior must be equivalent in: - - Interactive TTY - - Non-interactive shells - - `--quiet` - - `--json` - - Fully pre-specified flags - - Invalid operations must fail deterministically with non-zero exit code. - -3. **Prompt guard contract** - - Only prompt when all are true: - - stdout is TTY - - not `--quiet` - - not `--json` - - not automated/non-interactive context - -4. **Output contract** - - `--json`: machine-readable output only. - - `--quiet`: suppress non-essential output only. - - Neither mode weakens policy enforcement. - -5. **Cancellation contract** - - Handle prompt cancellation with `isCancel` + `cancel(...)`. - - Handle SIGINT cleanly and use consistent exit semantics. - -## Clack Primitive Standard - -- **Flow framing:** `intro`, `outro`, `cancel` -- **Status lines:** `log.info`, `log.success`, `log.warn`, `log.error`, `log.step` -- **Guidance blocks:** - - default: `note` - - high-severity warnings only: `box` -- **Prompts:** `select`, `confirm`, `text`, `password` -- **Long-running feedback:** - - unknown duration: `spinner` - - known duration: `progress` - - multi-stage: `tasks` - -## Preferred Pattern - -Centralize Clack imports and formatting helpers in one adapter module (for example `cli-output.js`) so command logic stays focused on behavior and policy. - -### Thin framework (recommended) - -Use a small shared helper surface rather than command-specific formatting logic. - -- `isJsonMode(options)` -- `isQuietMode(options)` -- `shouldRenderHumanOutput(options)` -- `canPrompt(options)` -- `createSpinner(options)` -- `createProgress(options, config)` -- `printJson(payload)` - -Keep this layer minimal. Do not hide core validation or command semantics inside output helpers. - -## Output Contracts by Mode - -### `--quiet` contract - -`--quiet` should still return essential result data. - -- Read/list commands: emit concise machine-friendly lines (not framed Clack blocks). -- Action commands: emit one minimal success line and concise errors. -- Do not suppress required outcomes entirely. - -Quiet output should still be complete enough for scripts and quick human scanning. - -- Status-like commands should list all active items, not only `running`/`ok`. -- Prefer compact stable key tokens in quiet lines (for example `port 3000 pass:yes`). - -### `--json` contract (strict) - -- Output must be JSON only (no extra text before/after payload). -- Warnings/info should be represented in JSON fields (for example `status`, `messages`). -- Preserve non-zero exit codes for failures. - -## Human UX Consistency - -### Framing completeness - -- If human flow uses `intro`, close with `outro` (or `outro('')` when you want structure without text). -- Avoid orphan frame/spinner artifacts (prefer `spinner.clear()` when a trailing spinner line is not wanted). -- If a structured summary section immediately follows a spinner, prefer `spinner.clear()` to avoid duplicate success lines. - -### Progress feedback for visible operations - -- For operations users wait on (start/stop/restart/tunnel lifecycle), show in-progress spinner in interactive mode. -- Resolve each spinner explicitly to done/error so users can see completion state at the same visual location. -- Keep quiet/json modes non-animated. - -### Prompt flow design - -- Ask required inputs in dependency order (for example hostname before token when token depends on chosen host/mode context). -- When offering save-vs-run flows, ask intent before collecting optional metadata (for example profile name only if user chooses save). -- Prefill editable values with `initialValue` (not only `placeholder`) so users can accept or edit quickly. -- Reuse latest relevant values when safe (for example last managed-local config path, last managed-remote hostname). - -### Readability on narrow terminals - -- Prefer short lines. -- Split long guidance into multiple detail lines. -- Use warning/info codes (`[CODE]`) when the message has follow-up docs or repeat use. - -### Guidance tone - -- Use `Optional Tips` for non-required next actions. -- Avoid wording that implies mandatory follow-up unless it is truly required. - -### Guidance rendering style (preferred) - -- Prefer structured status lines for reusable hints: - - `logStatus('info', '[CODE]', '')` -- Use short, stable codes (for example `[START_PROFILE]`, `[PORT_MISMATCH]`) so users can quickly scan and recognize repeated guidance. -- Prefer this style over boxed notes for routine follow-up actions. -- Reserve `note`/boxed callouts for rare, high-context guidance where a long paragraph is truly necessary. - -## Parity Verification Matrix - -For each command/subcommand, manually verify: - -1. default interactive TTY output -2. `--quiet` output (minimal but informative) -3. `--json` output (JSON-only) -4. non-TTY behavior (e.g. piped) -5. error path in both human and json modes - -## Copy/Paste Snippets - -### Prompt Guard - -```js -if (canPrompt(options)) { - const value = await select({ - message: 'Choose an option', - options: [{ value: 'a', label: 'Option A' }], - }); - if (isCancel(value)) { - cancel('Operation cancelled.'); - return; - } -} -``` - -### Non-Interactive Fallback - -```js -if (!resolvedValue) { - if (canPrompt(options)) { - // prompt path - } else { - throw new Error('Missing required value. Provide --flag .'); - } -} -``` - -### Spinner Guard - -```js -const spin = createSpinner(options); -spin?.start('Running operation...'); -// ...work... -spin?.stop('Done'); -``` - -### JSON vs Human Output - -```js -if (options.json) { - printJson({ ok: true, data }); - return; -} - -intro('Operation'); -log.success('Completed'); -outro('done'); -``` - -## Implementation Checklist - -1. Add or update core validators first. -2. Ensure validators execute in all modes. -3. Add interactive Clack UX only as enhancement. -4. Verify parity between interactive and non-interactive flows. -5. Ensure script-safe deterministic failure behavior. - -## References - -- Policy source: `AGENTS.md` (CLI Parity and Safety Policy) -- Terminal CLI precedent: `packages/web/bin/cli.js` -- Output adapter precedent: `packages/web/bin/cli-output.js` diff --git a/src/.opencode/skills/locale-ui-patterns/SKILL.md b/src/.opencode/skills/locale-ui-patterns/SKILL.md deleted file mode 100644 index ff57255..0000000 --- a/src/.opencode/skills/locale-ui-patterns/SKILL.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -name: locale-ui-patterns -description: Use when creating or modifying OpenChamber UI text, labels, buttons, placeholders, aria labels, empty states, toasts, dialogs, settings copy, navigation labels, or any user-facing strings. ---- - -# Locale UI Patterns - -## Core Rule - -User-facing UI text must go through `@/lib/i18n`; do not hardcode English strings in components. - -Use this skill for any React UI change that adds or edits visible text, accessible labels, placeholders, tooltips, toasts, dialogs, settings labels, navigation labels, or empty/error states. - -## Required Flow - -1. Add or reuse a key in `packages/ui/src/lib/i18n/messages/en.ts`. -2. Add the same key to every non-English dictionary in `packages/ui/src/lib/i18n/messages/`. -3. In components, call `const { t } = useI18n()` from `@/lib/i18n` and render `t('key')`. -4. For locale names or language picker labels, use `label(locale)` from `useI18n()`. -5. Keep locale state in `packages/ui/src/lib/i18n/*`; do not add locale fields to broad stores like `useUIStore`. -6. Do not remount the app to update language. Components must re-render through `useI18n()`. - -## Component Usage Rules - -- Import from `@/lib/i18n`, not deep files. -- Keep `t(...)` calls inside React render/hook scope so locale changes re-render text. -- Do not resolve translated text at module scope. -- For static option arrays, store `labelKey` / `descriptionKey`; resolve with `t(...)` inside the component. -- For non-React helpers, pass translated strings in from the component or pass `t` explicitly. - -## Key Style - -Use stable semantic keys, not English text as keys. - -Keys should describe location + UI role + meaning. They should not encode current copy wording. - -Use existing nearby naming when extending a surface. If no nearby pattern exists, choose a short path that mirrors the UI ownership. - -Namespaces like `layout.*`, `settings.*`, `chat.*`, `git.*`, `session.*`, `toast.*`, and `dialog.*` are examples, not a fixed exhaustive list. - -Good: -```ts -'settings.appearance.language.label': 'Language' -'layout.mainTab.chat': 'Chat' -'chat.input.placeholder': 'Ask OpenChamber...' -``` - -Bad: -```ts -'Language': 'Language' -'chatLabel': 'Chat' -'askOpenChamberDotDotDot': 'Ask OpenChamber...' -``` - -Avoid overly generic keys unless the text is truly global and context-independent. Prefer specific keys when button meaning can vary by surface. - -## Parameters - -Use `{name}` placeholders for dynamic values. - -```ts -'toast.language.changed': 'Language changed to {language}' -``` - -```tsx -t('toast.language.changed', { language: label(locale) }) -``` - -Do not pass grammar fragments as params. Never use params like `{suffix}`, `{plural}`, `{article}`, `{prefix}`, `{dateSuffix}`, or pieces of words/sentences. - -Bad: -```tsx -t('dialog.delete.description', { count, suffix: count === 1 ? '' : 's' }) -``` - -Good: -```tsx -count === 1 - ? t('dialog.delete.descriptionSingle', { count }) - : t('dialog.delete.descriptionPlural', { count }) -``` - -Plural/count-dependent text must use separate complete-message keys unless all supported locales can use one identical complete sentence. Placeholders are only for real values (`{count}`, `{name}`, `{path}`), not grammar. - -Optional clauses must also be complete-message keys. Do not build a sentence by injecting a translated phrase into another translated sentence. - -Bad: -```tsx -t('dialog.delete.description', { - dateLabel: date ? t('dialog.delete.dateSuffix', { date }) : '', -}) -``` - -Good: -```tsx -date - ? t('dialog.delete.descriptionWithDate', { count, date }) - : t('dialog.delete.description', { count }) -``` - -## What Counts As UI Text - -- Button and menu labels -- Settings labels and descriptions -- Placeholder text -- Tooltip content -- Dialog titles/descriptions/actions -- Toast title/description/action labels -- Empty/error/loading states -- `aria-label`, `title`, image `alt` text when user-facing - -## Exceptions - -Do not translate: - -- Product names: `OpenChamber`, `OpenCode`, `GitHub` -- Protocol/tool acronyms: `MCP`, `SSE`, `WebSocket`, `API` -- Model/provider names -- File paths, command names, environment variables -- User/generated content - -## Review Checklist - -- No new hardcoded user-facing English in changed UI files. -- Every new key exists in all dictionaries. -- No locale state added to broad/shared stores. -- No full app remount for locale changes. -- Locale switch preserves current UI state. diff --git a/src/.opencode/skills/settings-ui-patterns/SKILL.md b/src/.opencode/skills/settings-ui-patterns/SKILL.md deleted file mode 100644 index 0fc9276..0000000 --- a/src/.opencode/skills/settings-ui-patterns/SKILL.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -name: settings-ui-patterns -description: Use when creating or modifying UI components, styling, or visual elements related to Settings in OpenChamber. -license: MIT -compatibility: opencode ---- - -# Settings UI Patterns Skill - -## Purpose -This skill provides instructions for creating or redesigning Settings pages, informational panels, and configuration interfaces within the OpenChamber application. - -## Current Canonical Look (2026) -Use this as source of truth for new settings UI work. - -- **Flat hierarchy first**: Prefer spacing + typography hierarchy over boxed backgrounds. -- **No unnecessary wrappers**: Avoid extra section wrappers that mix unrelated controls. -- **No redundant section titles**: Do not add headers like `Theme Preferences` or `Scaling & Layout` when controls are already self-explanatory. -- **Compact controls**: Option chips and radio rows should be dense, not tall. -- **Left-leading state icon**: Radio/checkbox state icon appears before text. -- **Subtle state contrast**: Inactive radio labels should be visibly dimmer than active labels. -- **Minimal row chrome**: Avoid row hover/background highlighting by default; keep only where explicitly needed. - -## Typography Guidelines -Always utilize the standard OpenChamber typography classes defined in `packages/ui/src/lib/typography.ts`. - -- **Page Title**: Use `typography-ui-header font-semibold text-foreground` for the top-most title of a settings page/dialog. -- **Section Header**: Use `typography-ui-header font-medium text-foreground` for settings sections (e.g. `Notification Events`, `Session Defaults`). -- **Control Group Header**: Use `typography-ui-header font-medium text-foreground` (or `font-normal` if it reads too loud) for grouped controls inside a section (e.g. `Default Tool Output`, `Diff Layout`). -- **Values / Primary Text**: Use `typography-ui-label text-foreground`. Add `tabular-nums` if displaying numbers or stats to ensure vertical alignment. -- **Option Labels**: Use non-bold label text in compact option controls (`font-normal` when needed to override). -- **Meta / Helper Text**: Use `typography-meta text-muted-foreground` or `typography-small text-muted-foreground` for supplemental text. - -## Layout and Spacing Patterns - -### 1. Main Backgrounds -Main wrappers should generally use `bg-background` or `bg-[var(--surface-background)]`. Ensure adequate padding (e.g., `px-5 py-6` or `p-6`). - -### 2. Subsection Grouping -Group related controls with vertical spacing, not mandatory cards. - -- Use `space-y-3` between logical subsections. -- Use `p-2` for subsection internal padding. -- Avoid adding `bg-[var(--surface-elevated)]` unless there is a clear reason. -- Avoid extra row decorations (`rounded-md`, hover fills) unless there is explicit UX value. - -### 3. Header-to-Content Hierarchy (critical) -When removing cards/background wrappers, spacing must be rebalanced so header ownership stays clear. - -- Keep **section-to-section spacing larger** than **header-to-own-content spacing**. -- Typical pattern: - - header wrapper `mb-1 px-1` - - content wrapper `pt-0 pb-2 px-2` - - outer section spacing `mb-8` -- Do not leave legacy `mb-3` style gaps after flattening a section; it makes headers look detached. - -### 4. Headerless Blocks (when context is obvious) -If the page title already provides enough context, remove redundant local headers and place controls directly below the title. - -- Example: project page identity controls can sit directly under project name/path. -- Tighten top gap for this pattern (e.g. top header `mb-4` instead of larger section spacing). - -```tsx -
-
...
-
...
-
-``` - -## Structural Patterns - -### 1. Segmented Option Buttons (compact) -Use for short option sets where button-style segmented choice reads best (e.g. Default Tool Output). - -```tsx -
- - Collapsed - -
-``` - -### 2. Radio Option Lists (compact rows) -Use for mutually exclusive mode/layout settings (e.g. Diff Layout, Diff View Mode). - -- Use shared `Radio` component from `@/components/ui/radio`. -- Icon first, label second. -- Row container compact: `py-0.5`. -- Inactive label can use `text-foreground/50`. - -```tsx -
-
- - Dynamic -
-
-``` - -### 3. Checkbox Setting Rows -Use shared `Checkbox` component from `@/components/ui/checkbox` for boolean toggles. - -- Icon first, text immediately after (`gap-2`). -- Typical row spacing for checkbox rows: `py-1.5`. -- Keep row click and keyboard toggle support. -- Prefer checkbox over binary show/hide button pairs for pure boolean state. - -```tsx -
- - Show Dotfiles -
-``` - -### 4. Invisible Two-Column Alignment -Use consistent label/control columns across settings rows so controls align on a shared vertical line. - -- Desktop row pattern: `flex items-center gap-8` -- Label column width: `w-56 shrink-0` -- Control cluster: `w-fit` - -```tsx -
- Interface Font Size -
...
-
-``` - -#### Disabled control rule -If a control is unavailable, disable the control only. Do not dim the label row by default. - -#### Width-matching rule -When matching visual widths across different rows, compare full row footprint (control + adjacent action buttons), not just input width. - -### 5. Theme Row Composition -For theme controls in Appearance: - -- `Color Mode` header on first line; option chips below it. -- `Light Theme` and `Dark Theme` on one row where possible, wrapping on small widths. -- Keep selectors near labels and aligned to existing column rhythm. -- Replace persistent helper text with an info tooltip icon near the related action. - -```tsx -
-
Light Theme ...
-
Dark Theme ...
-
-``` - -### 6. Numeric Controls in Settings -Use compact stepper input (`- value +`) plus reset button. - -- Prefer shared `NumberInput` stepper style over slider + numeric combo in dense settings pages. -- Keep reset button adjacent to control (`gap-2`). -- Avoid using Tailwind `overflow-hidden` on mobile for controls; `packages/ui/src/styles/mobile.css` forces `.overflow-hidden { overflow-y: auto !important; }`. - Use `overflow-x-hidden overflow-y-hidden` if you truly need clipping. -- Touch devices: `packages/ui/src/styles/mobile.css` enforces `min-height: 36px` on `button`. If you build custom segmented controls with `