# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Git Worktrees Worktree directory: `.claude/worktrees/` (gitignored) ## Project Overview Sokuji is a real-time AI-powered translation application available as both an Electron desktop app and a browser extension. It provides live speech translation through Kizuna AI (managed Soniox), free on-device inference, Google Gemini, Doubao AST 2.0, OpenAI Realtime, OpenAI Translate, OpenAI Live, Soniox, Qwen LiveTranslate and Palabra AI, with modern audio processing capabilities. Local Native, the Electron sidecar, still runs on the old provider path until kizuna-ai-lab/sokuji#578. ## Development Commands ### Running the Application ```bash # Run Electron app in development mode npm run electron:dev # Run React app only (for browser extension development) npm run dev # Build Electron app for production npm run electron:build # Run tests npm run test # Run tests with UI npm run test:ui # Run specific test npm run test -- path/to/test ``` ### Building and Packaging ```bash # Build React app npm run build # Package Electron app npm run package # Create distributable packages npm run make ``` ### Version Update Process **All five version sites must land in a single `chore(release): vX.Y.Z` commit BEFORE the tag is created.** Earlier releases split root and extension version bumps into two separate commits with the tag on the root-only commit; the tag checkout then built the extension with the previous version. The release workflow checks out the tag verbatim, so every version-affecting file must be at the new version at the tagged commit. 1. Update all five files in one go: - `package.json` - `extension/package.json` - `extension/manifest.json` - `package-lock.json` (run `npm install` at the root to regenerate) - `extension/package-lock.json` (run `npm install` inside `extension/` to regenerate) 2. Commit all five together: `git commit -m "chore(release): vX.Y.Z"` 3. Create annotated tag on that commit: `git tag -a vX.Y.Z -m "Release vX.Y.Z"` 4. Push: `git push origin main --follow-tags` **Sanity check before pushing the tag:** ```bash git show vX.Y.Z:package.json | grep '"version"' git show vX.Y.Z:extension/package.json | grep '"version"' git show vX.Y.Z:extension/manifest.json | grep '"version"' ``` All three must print the same new version. If they don't, `git tag -d vX.Y.Z` and re-tag on the correct commit before pushing. ## Architecture Overview ### Dual Platform Architecture The codebase supports both Electron desktop app and Chrome/Edge browser extension from a shared React codebase: - **Shared code**: `src/` directory contains all React components and business logic - **Electron-specific**: `electron/` directory, virtual audio device management (Linux only) - **Extension-specific**: `extension/` directory, manifest.json, background scripts ### Key Architectural Components 1. **Service Layer Pattern** - `ServiceFactory` hands out the one settings service (`SettingsService`, implementing `ISettingsService`), cached as a singleton; audio has no service object (5. below) - Platform detection via `src/utils/environment.ts` utilities 2. **Provider Architecture** (spec: `docs/superpowers/specs/2026-09-22-client-contract-design.md`) - One folder per provider, `src/providers//`: `provider.ts`, the definition (settings, credentials, languages, `check`, `session`, its `Settings` view; typed by `src/lib/provider/types.ts`), and `adapter.ts`, the L0 client of one leg: it speaks the provider's wire and emits segments, audio and lifecycle through `AdapterEvents` (`src/lib/contract/adapter.ts`); every adapter passes `src/lib/contract/conformance.ts` - `src/providers/registry.ts` lists them in UI order and compiles the `fake` providers into development builds only (D24); `src/lib/provider/presence.ts` (`isPresent`) decides what a build and platform offer: a definition's `platforms`, a flagged one by `VITE_ENABLED_PROVIDERS` (every one in a development build, or by its tester switch), the managed one by `VITE_ENABLE_KIZUNA_AI` (on in every development build) - `src/stores/providerStore.ts` holds each provider's settings and credentials, stored under `settings..*` (older keys through the definition's `legacyKeys` and `migrate`), and one language pair for every provider under `settings.common.sourceLanguage/targetLanguage` that only a pick writes (each provider shows it within what it offers); a load never writes - The runner (`src/lib/session/`, `src/app/`) runs the legs; L1 (`src/lib/conversation/Conversation.ts`) folds each leg's events into segments - Local Native is the exception until kizuna-ai-lab/sokuji#578: `LocalNativeClient` implements the old `IClient` (`src/services/interfaces/IClient.ts`), registered as `LocalNativeProviderConfig` in `ProviderConfigFactory`, built through `ClientFactory`, and set up in the old settings shell (`ProviderSection`, `LanguageSection`, `ProviderSpecificSettings`), which nothing mounts; `src/providers/oldPath.consistency.test.ts` keeps every other file off that path. Its folders (`src/services/{clients,providers,interfaces}/`) also hold four shared leaves the new code takes — `ProviderConfig` (`LanguageOption`, `VoiceOption`), `astGuard`, `tutorialUrls` and `ISettingsService`, the guard's `SHARED` set — which stay or move when #578 lands; they are not Local Native's to delete 3. **Audio Processing Pipeline** ``` Source (microphone, system audio, a tab) → runner → provider's adapter → L1 → playback → outputs ``` - `ModernAudioRecorder` (`src/lib/modern-audio/`): captures the microphone with echo cancellation and noise suppression, AudioWorklet with a ScriptProcessor fallback - Playback (`src/lib/audio/playback.ts`): a clip queue per leg and one for replay, the routes kept live from the routing settings, the passthrough stream; replay and preview play on the outlet of their row - `src/lib/audio/appAudio.ts`: one graph per page, and the virtual output per platform (Electron's virtual speaker, the extension's tabs, nothing on the web) 4. **State Management** - **Zustand stores** in `src/stores/` for primary application state: - `settingsStore.ts`: common settings (UI mode, text-only, segmentation, display modes) and Local Native's old slice - `providerStore.ts`: each provider's settings and credentials, and the one global language pair (`settings.common.*`, written only by a pick) - `sessionStore.ts`: the locked mode the kept old settings shell reads; nothing writes it - `audioStore.ts`: Audio device selection and playback state - `logStore.ts`: Application logs and diagnostics - React Context for specific features: UserProfileContext, PostHogContext - Zustand's `subscribeWithSelector` middleware for efficient re-renders - Backend-managed API key integration for authenticated providers 5. **Audio** (`src/lib/audio/`) - The runner's sources (`capture/`): the microphone (`mic.ts`, over `ModernAudioRecorder`) and system audio, each following its device or source during a run; a tab, fixed when it opens - The page's playback (`appAudio.ts`, `playback.ts`), the routing read live from `audioStore`, `routingStore`, `settingsStore` and `turnModeStore` through `speechFromStores` (`src/lib/session/appShape.ts`): who hears what. The system-audio source tells the audio store when an application capture widened to the whole system (`participantCaptureWidened`); `heardFromStores` reads it, so Translation I hear falls silent live instead of being recaptured. - `src/lib/audio/outlets.ts`: the outlets — `other` (face-to-face's other person), `me` (I hear it too), `them` (Translation I hear) — each a stored device and channel (`audio.outlet..*`) resolved to a sink; `graph.ts` keeps one `