This file provides context and instructions for AI coding agents. For human contributors, see README.md and CONTRIBUTING.md.
Important
All changes must meet industrial-grade quality. Enforce DRY (extract composables/utilities over duplication), strict TypeScript (no any, justify every as cast), structured error handling, and full verification (vue-tsc + tests pass) before completion.
| Layer | Stack |
|---|---|
| Frontend | Vue 3 Composition API + Pinia + Naive UI + TypeScript |
| Backend | Rust (Tauri 2) + aria2 sidecar |
| Build | Vite (frontend) + Cargo (backend) |
| Package Manager | pnpm (version pinned via packageManager field in package.json) |
| Testing | Vitest (frontend), cargo test (backend) |
src/
├── api/ # Aria2 JSON-RPC client
├── components/preference/ # Settings UI (Basic.vue, Advanced.vue, UpdateDialog.vue)
├── composables/ # Vue composables — business logic extracted from components
├── layouts/ # Page-level layouts (MainLayout.vue)
├── shared/
│ ├── types.ts # All TypeScript interfaces (AppConfig, TauriUpdate, etc.)
│ ├── constants.ts # DEFAULT_APP_CONFIG, proxy scopes, tracker URLs, timing constants
│ ├── configKeys.ts # Config key lists (userKeys, systemKeys, needRestartKeys)
│ ├── locales/ # 26 locale directories (see Section D)
│ └── utils/
│ ├── configMigration.ts # Config schema migration engine (see Section C′)
│ ├── config.ts # Config key-value transform utilities
│ └── tracker.ts # BT tracker fetching with proxy support
├── stores/ # Pinia stores (app.ts, preference.ts)
├── views/ # Page-level route views
└── main.ts # App entry, auto-update check
src-tauri/
├── src/
│ ├── lib.rs # Tauri builder, plugin registration, invoke_handler
│ ├── main.rs # Tauri entry point
│ ├── commands/
│ │ ├── mod.rs # Command module re-exports
│ │ ├── config.rs # Config CRUD, session, factory reset commands
│ │ ├── engine.rs # Engine start/stop/restart commands
│ │ ├── ui.rs # Tray, menu, dock, progress bar commands
│ │ ├── tracker.rs # Tracker probing and protocol classification
│ │ ├── fs.rs # File system ops, diagnostics, platform code
│ │ ├── updater.rs # check_for_update, download_update, apply_update, cancel_update
│ │ └── upnp.rs # UPnP port mapping commands
│ ├── engine/
│ │ ├── mod.rs # Module re-exports
│ │ ├── lifecycle.rs # aria2 sidecar start/stop/restart
│ │ ├── args.rs # aria2 command-line argument builder
│ │ ├── cleanup.rs # Engine cleanup utilities
│ │ └── state.rs # Engine state management
│ ├── error.rs # AppError enum (Store, Engine, Io, NotFound, Updater, Upnp)
│ ├── menu.rs # Native menu builder (macOS only, cfg-gated)
│ ├── tray.rs # System tray setup
│ └── upnp.rs # UPnP/IGD port mapping with renewal loop
├── Cargo.toml # VERSION SOURCE OF TRUTH
└── tauri.conf.json # Tauri config (no version field — reads from Cargo.toml)
.github/
├── ISSUE_TEMPLATE/ # Bug report (YAML form) + feature request templates
├── PULL_REQUEST_TEMPLATE.md # PR template with TypeScript + Rust checklist
└── workflows/
├── ci.yml # Lint + type check + test (frontend & backend parallel jobs)
└── release.yml # Build + sign + upload for 6 platforms + updater JSON
src-tauri/Cargo.toml is the single source of truth. The version field in package.json must stay in sync.
Always use the provided script:
./scripts/bump-version.sh 1.4.0This atomically updates both Cargo.toml and package.json.
Cargo.toml— Tauri reads this at build time; the About panel reads it viagetVersion()at runtime.package.json— pnpm/action-setup and npm tooling reference this; CI workflows use thepackageManagerfield.tauri.conf.json— intentionally omitsversionso Tauri falls back toCargo.toml.
Never manually edit version strings. Always use
bump-version.sh.
Follow this exact checklist:
src/shared/types.ts— Add the field to theAppConfiginterface with proper typingsrc/shared/configKeys.ts— Add the key name (kebab-case) touserKeysorsystemKeysarray. Without this, the value will NOT persist across restarts- UI binding — For Basic settings: add to
buildForm()initializer +watchSyncEffectsave inBasic.vue. For Advanced settings: add tobuildAdvancedForm()+buildAdvancedSystemConfig()inuseAdvancedPreference.ts - All 26 locale files — Add i18n label keys. Must use batch Python script (see Section D)
- If modifying an existing field's format or default — Add a migration in
configMigration.ts(see Section C′)
src/shared/utils/configMigration.ts implements versioned schema migration (same pattern as electron-store). On each app launch, loadPreference() runs pending migrations before merging saved config into defaults.
configVersion(integer) is stored inconfig.jsonalongside user preferencesCONFIG_VERSIONconstant defines the current schema versionmigrations[]array holds ordered migration functions (index 0 = v0→v1, etc.)- Migrations run only when
stored version < CONFIG_VERSION, then persist
- Append a function to the
migrationsarray inconfigMigration.ts - Increment
CONFIG_VERSIONto match the new array length - Update
DEFAULT_APP_CONFIG.configVersioninconstants.tsto match - Add tests in
configMigration.test.ts
- Migrations mutate the config object in place
- Migrations must be idempotent — safe to re-run on already-migrated data
- Migrations must not delete user data without logging
tauri_plugin_sql manages versioned SQL migrations for sqlite:history.db. Migrations run automatically on app launch when the stored version is behind the latest.
This is separate from Config Schema Migration (Section C′). Config migrations handle
config.json(JSON key-value preferences) in the frontend. DB migrations handlehistory.db(SQLite relational tables) in the backend. They manage different data stores in different runtimes — merging them is not practical.
- SQL migration files live in
src-tauri/migrations/withNNN_description.sqlnaming - Each migration is registered as a
tauri_plugin_sql::Migrationstruct in the.add_migrations()call inlib.rs - The plugin tracks executed versions in an internal
_sqlite_migrationstable - Old users receive new migrations transparently on upgrade — no manual action needed
- Create
src-tauri/migrations/NNN_description.sqlwith the SQL statements - Append a
Migrationstruct to thevec![]inlib.rs:tauri_plugin_sql::Migration { version: N, description: "short description", sql: include_str!("../migrations/NNN_description.sql"), kind: tauri_plugin_sql::MigrationKind::Up, },
- If the migration adds/renames columns used by the frontend, update
HistoryRecordinsrc/shared/types.ts - Update relevant SQL queries in
src/stores/history.ts - Run
cargo checkto verify the Rust compiles
- Migrations must be additive — never DROP columns that old code may still reference
- Use
ALTER TABLE ... ADD COLUMNwith defaults for backward compatibility - Use
COALESCEin queries to handle NULL values from old rows gracefully - Test with both a fresh DB AND an existing DB to verify both paths work
Both migration systems show upgrade toasts on the UI, but with distinct messages:
| System | i18n Key | Example (en-US) | Toast Type |
|---|---|---|---|
| Config (C′) | app.migration-success |
"User settings schema upgraded to v2" | success (green) |
| DB (C″) | app.db-upgraded |
"Database schema upgraded to v2" | info (blue) |
- NEVER edit locale files manually one by one. Always use a Python batch script.
- Strings containing
'must be escaped as\'in JS source files. - English (
en-US) keys serve as the fallback — always verify this locale first.
ar bg ca de el en-US es fa fr hu id it ja ko nb nl pl pt-BR ro ru th tr uk vi zh-CN zh-TW
#!/usr/bin/env python3
"""Batch-update locale files with native translations."""
import os, re
LOCALES_DIR = "src/shared/locales"
TRANSLATIONS = {
"ar": ("Arabic text",),
"bg": ("Bulgarian text",),
# ... all 26 locales with native translations ...
"en-US": ("English text",),
"zh-CN": ("Chinese Simplified text",),
"zh-TW": ("Chinese Traditional text",),
}
def update_locale(locale_dir, values):
filepath = os.path.join(LOCALES_DIR, locale_dir, "preferences.js")
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# Use regex or string replacement to insert/update keys
# Escape single quotes in values: value.replace("'", "\\'")
# Write back
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
for locale, vals in sorted(TRANSLATIONS.items()):
update_locale(locale, vals)Critical: After running, verify with
npx vite build— locale parse errors will surface here.
The release workflow (.github/workflows/release.yml) is triggered by on: release: types: [published].
| Channel | Tag Pattern | JSON Generated | Example |
|---|---|---|---|
| Stable | v1.4.0 |
latest.json |
v1.3.1 |
| Beta | v1.4.0-beta.N |
beta.json |
v1.4.0-beta.1 |
| RC | v1.4.0-rc.N |
beta.json |
v1.4.0-rc.1 |
Both latest.json and beta.json are uploaded to a permanent updater Release tag:
https://github.com/AnInsomniacy/motrix-next/releases/download/updater/latest.json
https://github.com/AnInsomniacy/motrix-next/releases/download/updater/beta.json
The CI creates this Release automatically if it doesn't exist, and uses --clobber to overwrite on each release.
The Tauri JS check() API does not support runtime endpoint override. Channel switching is implemented via Rust commands:
check_for_update(channel, proxy)→ dynamically builds updater with correct endpointdownload_update(channel, proxy)→ downloads update binary, emits progress eventsapply_update(channel)→ stops engine, installs downloaded updatecancel_update()→ cancels in-progress download
The user's channel preference is stored as updateChannel in the preference store.
All code changes must be finalized before starting. Execute these three steps in strict order:
-
Bump the version:
# Stable ./scripts/bump-version.sh 1.4.0 # Beta ./scripts/bump-version.sh 1.4.0-beta.1
Do not modify code after this step. This updates
Cargo.toml+package.json. -
Release:
./scripts/release.sh
This formats code, commits all changes, creates an annotated tag
v{VERSION}, and pushes to origin. The script outputs a color-coded channel indicator (yellow = pre-release, green = stable). -
Generate Release Title and Notes:
Based on the commits included in this release, generate an English title and release notes following the Release Notes Conventions below. Output them in two separate markdown code blocks — one for the title, one for the body — so the user can copy-paste each directly into the GitHub Release page.
-
User publishes on GitHub — CI automatically builds for all 6 platforms and uploads the updater JSON.
- Channel detection — CI checks the tag name: tags containing
-beta,-alpha, or-rc→beta.json; everything else →latest.json - Single fixed host — Both JSON files live in a permanent
updaterRelease tag (auto-created by CI on first publish). Each publish overwrites the previous JSON via--clobber - Tag = immutable pointer — A git tag points to a fixed commit. If a build fails, you must delete both the tag and the Release, then re-publish to pick up the fixed code
- CI trigger — Only
on: release: [published]triggers builds. Pushing a tag alone does not trigger the workflow
# 1. Fix the code, commit and push
git add -A && git commit -m "fix: resolve build issue" && git push
# 2. Delete the remote tag
git push origin --delete v2.1.1
# 3. Delete the local tag
git tag -d v2.1.1
# 4. Delete the failed Release on GitHub (Releases → click → Delete this release)
# 5. Re-run bump-version.sh with the same version to re-create the tag
./scripts/bump-version.sh 2.1.1
git push && git push --tags
# 6. Re-create the Release in the GitHub UI selecting the tagTitle format: v{VERSION} — {Short Description}
Examples: v2.0.0 — Stability & Quality Release, v2.0.1 — Bug Fixes, v2.1.0 — Proxy Support
Body template:
> [!CAUTION]
> **Breaking change notice** (only if applicable)
## What's Changed
One-paragraph summary of the release scope and significance.
### ✨ New Features
- **Feature name** — short description
- **Feature name** — short description
### 🛠 Improvements
- Description of improvement
- Description of improvement
### 🐛 Bug Fixes
- Fixed specific issue
### 📦 Downloads
| Platform | Architecture | File |
| -------- | --------------------- | ------------------ |
| macOS | Apple Silicon · Intel | `.dmg` |
| Windows | x64 · ARM64 | `-setup.exe` |
| Linux | x64 · ARM64 | `.AppImage` `.deb` |Guidelines:
- Use
> [!CAUTION]GitHub Alert only for breaking changes or manual action required - Omit empty sections — e.g. no Bug Fixes section if there are none
- Patch releases: keep concise, only list what changed
- Major releases: include a summary paragraph explaining the scope
Two parallel jobs:
| Job | Steps |
|---|---|
frontend |
pnpm install → eslint → prettier --check → vue-tsc --noEmit → vitest run → vite build |
backend |
cargo fmt --check → cargo clippy → cargo check --all-targets → cargo test |
- Build job — Matrix:
macos-latest(aarch64),macos-15-intel(x86_64),windows-latest(×2: x64 + aarch64 cross-compile),ubuntu-22.04(GLIBC 2.35 compat),ubuntu-24.04-arm - merge-updater-json job — Detects channel from tag name → generates
latest.jsonorbeta.jsonwith 6 platform keys → uploads toupdatertag
- Strict mode enabled in
tsconfig.json <script setup lang="ts">for all components- Path aliases:
@/→src/,@shared/→src/shared/ - Imports: named imports from
naive-ui, destructured Tauri APIs - State management: Pinia stores with Composition API style (
setupfunction) - Formatting: Prettier with project config (
.prettierrc)
- Error handling: All commands return
Result<T, AppError>, never rawStringerrors AppErrorenum inerror.rswith variants:Store,Engine,Io,NotFound,Updater,Upnp- Async commands: Use
#[tauri::command]withasyncfor I/O operations - Plugin usage: Tauri plugin traits (e.g.,
UpdaterExt,StoreExt) imported in command modules
- Custom properties for all design tokens (colors, timing, easing)
- No utility frameworks — vanilla CSS with component-scoped styles
- Motion: Material Design 3 asymmetric timing and emphasized easing curves
Run these before committing changes:
# Frontend
pnpm format # Auto-format all source files with Prettier
pnpm format:check # Verify formatting (CI runs this)
pnpm test # Vitest unit tests
npx vue-tsc --noEmit # TypeScript type checking
# Backend
cargo check # Fast compilation check
cargo test # Rust unit tests
# Version (when bumping)
./scripts/bump-version.sh <version>Every commit MUST pass
pnpm format:check. If you edit any.ts,.vue,.css, or.jsonfile, runpnpm formatbefore committing. The husky pre-commit hook runs lint-staged automatically, but it only formats staged files — so always verify withpnpm format:checkif unsure.
Note:
npx vite buildis slow and should only be run when validating production output or debugging locale/bundling issues — not on every change.
All fast checks must pass with zero errors before any PR or release.
DO NOT use browser tools (Playwright, browser subagent, etc.) to test this app. Tauri renders in a native webview —
localhost:1420in a browser lacks IPC, tray, and sidecar access. Use CLI checks (vue-tsc,pnpm test,cargo test) or ask the user to verify UI viapnpm tauri dev.