diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json
new file mode 100644
index 00000000..89a45abb
--- /dev/null
+++ b/.cursor/worktrees.json
@@ -0,0 +1,3 @@
+{
+ "setup-worktree": ["pnpm run setup"]
+}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..5d883bce
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,101 @@
+name: Build and Release
+
+on:
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+ inputs:
+ release_tag:
+ description: "Release tag (e.g. v0.14.5). Leave empty to skip release creation."
+ required: false
+ default: ""
+
+jobs:
+ build:
+ runs-on: macos-latest
+ permissions:
+ contents: write
+ strategy:
+ fail-fast: false
+ matrix:
+ target:
+ - aarch64-apple-darwin
+ - x86_64-apple-darwin
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ lfs: true
+ - name: Install Rust toolchain
+ uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: stable
+ cache: true
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+ - name: Build for ${{ matrix.target }}
+ run: |
+ rustup target add ${{ matrix.target }}
+ pnpm tauri build --target ${{ matrix.target }} --config '{"bundle":{"createUpdaterArtifacts":false}}'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Zip .app bundle
+ run: |
+ cd src-tauri/target/${{ matrix.target }}/release/bundle/macos
+ zip -r Chorus.app.zip Chorus.app
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: bundle-${{ matrix.target }}
+ path: |
+ src-tauri/target/${{ matrix.target }}/release/bundle/macos/Chorus.app.zip
+ src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
+
+ release:
+ needs: build
+ if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != ''
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+ - name: Rename artifacts for clarity
+ run: |
+ mkdir -p release-assets
+ # aarch64 (Apple Silicon)
+ cp artifacts/bundle-aarch64-apple-darwin/macos/Chorus.app.zip release-assets/Chorus-aarch64.app.zip
+ cp artifacts/bundle-aarch64-apple-darwin/dmg/*.dmg release-assets/ 2>/dev/null && \
+ mv release-assets/Chorus_*.dmg release-assets/Chorus-aarch64.dmg || true
+ # x86_64 (Intel)
+ cp artifacts/bundle-x86_64-apple-darwin/macos/Chorus.app.zip release-assets/Chorus-x86_64.app.zip
+ cp artifacts/bundle-x86_64-apple-darwin/dmg/*.dmg release-assets/ 2>/dev/null && \
+ mv release-assets/Chorus_*.dmg release-assets/Chorus-x86_64.dmg || true
+ ls -la release-assets/
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ github.event.inputs.release_tag }}
+ name: ${{ github.event.inputs.release_tag }}
+ body: |
+ ## Chorus ${{ github.event.inputs.release_tag }}
+
+ ### Downloads
+ - **Apple Silicon (M1/M2/M3):** `Chorus-aarch64.app.zip` or `Chorus-aarch64.dmg`
+ - **Intel:** `Chorus-x86_64.app.zip` or `Chorus-x86_64.dmg`
+
+ > **Note:** This app is not code-signed. On first launch, right-click the app and select "Open" to bypass Gatekeeper.
+ files: release-assets/*
+ draft: false
+ prerelease: false
diff --git a/BUILD.md b/BUILD.md
new file mode 100644
index 00000000..52ab48fa
--- /dev/null
+++ b/BUILD.md
@@ -0,0 +1,36 @@
+# Building Chorus
+
+Chorus is built using Tauri, React, TypeScript, and Rust. To build the application yourself, follow these steps.
+
+## Prerequisites
+
+- [Node.js](https://nodejs.org/) (version >= 22.0.0)
+- [pnpm](https://pnpm.io/)
+- [Rust](https://www.rust-lang.org/) and Cargo
+- [Git LFS](https://git-lfs.com/)
+
+## Installation
+
+1. Clone the repository and navigate to the directory.
+2. Initialize Git LFS:
+
+ ```bash
+ git lfs install --force
+ git lfs pull
+ ```
+
+3. Install dependencies:
+
+ ```bash
+ pnpm install
+ ```
+
+## Building the App
+
+To build the production app for your platform, run:
+
+```bash
+pnpm tauri build
+```
+
+This will generate the application bundle (e.g., `.app` for macOS) in `src-tauri/target/release/bundle/`.
diff --git a/README.md b/README.md
index e682b6ea..fe279845 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,19 @@
+ Configure defaults applied when you create a new chat. These + are read once at creation time and do not change existing + chats. +
++ Automatically injected into new regular chats. +
+ ++ Single model for new chats when{" "} + + Default Chat Models + {" "} + is cleared. Takes priority over your current multi-model + (⌘J) list. For recovery when a chat's models are no + longer visible, the same order applies after filtering. +
+ + {staleFallback && ( ++ Previously selected model is no longer in your visible + models. +
+ )} ++ When set, the fallback model must belong to this + profile. Only profiles that include the selected + fallback model are shown. +
+ + {fallbackProfileIncompatible && ( ++ The saved profile no longer includes the + selected fallback model. Clear it to avoid + conflicts. +
+ )} ++ Model used for ambient chat. Only visible models that accept + image input are listed. +
+ + {visionVisibleModels.length === 0 && ( ++ No vision-capable models available. Enable a vision + model in{" "} + + . +
+ )} + {staleAmbient && ( ++ Previously selected model is no longer available as a + visible vision model. +
+ )} ++ Optional explicit list: every new regular chat starts with + exactly these models (in order). When cleared, new chats use{" "} + + Default Fallback Model + {" "} + if set, otherwise your ⌘J multi-model list, then the first + visible model. +
+ {staleChatModelIds.length > 0 && ( ++ Some saved defaults are no longer visible and will be + skipped. +
+ )} ++ No visible models. Configure them in{" "} + + . +
+ ) : ( + providerGroups.map(([provider, models]) => { + const label = PROVIDER_LABELS[provider] ?? provider; + return ( ++ Start an ambient chat with{" "} + + {quickChatShortcut} + +
++ Enter the shortcut you want to use to start an + ambient chat. +
++ No visible models available. Go to "Visible Models" to enable + models first. +
+ ); + } + + return ( ++ Create named sets of models to quickly switch between them + in chat. Profiles draw from your visible models — configure + which models are visible in the "Visible Models" tab. +
++ {p.modelConfigIds.length} models +
++ Applied to new chats in this project, + overriding the global default. +
++ Prompt profiles inject a persona or role into your chats. + Select a profile from the chat input toolbar to activate it. +
++ {p.systemPrompt} +
++ Model used to auto-generate chat titles. + Defaults to the ambient chat model. +
+ +- Start an ambient chat with{" "} - - {typeof quickChatShortcut === - "string" - ? quickChatShortcut - : "Alt+Space"} - -
-- Enter the shortcut you want to use to - start an ambient chat. -
-+ Fetch and choose which models appear in the chat model + picker and in your model profiles. +
++ {isFetchable + ? 'No models loaded yet. Click "Fetch Models" to load the model list.' + : "No models available."} +
+ ) : ( +