Adds a microphone button beside the composer. Click to record, click to stop; the audio is resampled to 16 kHz mono PCM16 WAV, posted to the local server and transcribed by a SenseVoice worker process, and the text lands in the draft without being sent. If the draft changed or an IME is composing, the result is held behind an insert button instead of overwriting the user's text.
Server: a small provider registry behind /api/voice/* (catalog, preferences, prepare/cancel/status/remove, transcribe). The engine and model are downloaded at runtime to <config>/cc-haha/voice with pinned sha256/sha512, HuggingFace plus hf-mirror and npm plus npmmirror, HTTP Range resume, automatic retry after interruptions, and a partial file kept across cancels. Recognition runs in a separate worker process (sidecar --voice-worker), started on demand and reclaimed when idle.
Desktop: an independent Voice input settings tab with enable switch, model download progress and resume, language, microphone selection and a transcription test with a live waveform. Uses the shared Dropdown/Card/Button components; adds a danger-ghost Button variant. Preferences live in desktop-ui.json (schemaVersion 6); the microphone device id stays in localStorage.
Electron: main-window media permission handler limited to app pages, main frame and audio only, plus the audio-input entitlement and NSMicrophoneUsageDescription.
Scope: Electron desktop only. Not verified on Windows or Linux, with a real microphone, or in a signed and notarized package.
The NSIS installer spawned by quitAndInstall() inherits the app process
environment, including the app-managed CLAUDE_CONFIG_DIR /
CC_HAHA_APP_PORTABLE_DIR pair that applyStartupPortableMode() derives
from app-mode.json. The installer's recovery helper then re-validated
that snapshot against the persisted mode it reads via its own APPDATA
and blocked the whole upgrade on any disagreement (mode switched without
a restart, APPDATA differing from the app's known-folder view), even
though an active data directory outside every install directory cannot
be touched by removing the old version. Manually launched setups never
saw the variables, which is why they kept working.
- clear the app-managed portable env before handing off to the spawned
installer (shared with the existing app.relaunch() cleanup), so
built-in updates present the same clean environment as a manual setup
- downgrade the recovery helper's managed-mode consistency check from
fail-closed to treating the directory as externally managed; the
install-contained legacy data guard below it still refuses unsafe
removals, and matching persisted modes behave exactly as before
- mac.notarize=true + hardenedRuntime + entitlements so a signed CI release
actually notarizes (gatekeeper smoke + Squirrel.Mac auto-update need it)
- entitlements grant disable-library-validation for the Bun sidecar/node-pty
- local unsigned build passes -c.mac.notarize=false so electron:package still
works without an Apple account
- release signing-preflight now hard-requires only the Apple secrets; Windows
cert is optional (unsigned NSIS still auto-updates, just SmartScreen warning)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>