Complete rebuild of voice input on a server-authoritative streaming architecture, replacing the legacy Web Speech / whole-blob / WASM engines and the dead voice-agent layer (~4k lines removed). Speech-to-text (dictation): - Client streams 16 kHz mono PCM16 chunks over /api/dictation/ws with seq/ack ordering; buffered audio is retained and replayed on reconnect - Server transcribes and streams live partial transcripts back; segments auto-commit every ~15s with silence suppression and adaptive finalization timeouts - Local provider (default, zero config): sherpa-onnx models in a forked worker process — auto-download with progress, staged extraction with verification, corrupt-model auto-recovery, idle shutdown after 5 min - Model catalog with settings picker (accuracy/speed ratings, sizes, download/delete): Parakeet TDT v2 (English) and v3 (25 European languages, auto-detected), Whisper base and tiny (multilingual, light) - OpenAI-compatible provider for any Whisper endpoint - Composer overlay with live transcript, volume meter, timer, and cancel / insert / insert-and-send actions; failed transcriptions keep their audio for retry or accepting the partial text as-is - Configurable keyboard shortcut (default mod+alt+v) toggles dictation; Enter confirms and Escape cancels while recording - Overlay is pixel-aligned with the composer (measured footer height, matching paddings/typography/gaps) — no layout shift when toggling Text-to-speech: - Local Kokoro provider (English, 11 voices) synthesized in the same worker via /api/dictation/tts/speak, managed by the shared model pipeline; sentence-pipelined playback keeps time-to-first-audio at ~1 sentence regardless of message length, and stop cancels in-flight synthesis - Sanitizer keeps inline-code content (strips backticks only), reads interword slashes aloud, and removes only absolute file paths Settings: - Voice page unified: a single read-aloud toggle owns all playback options (the confusing "Enable Voice Mode" is gone); a new "Enable voice input" toggle (default on, persisted to settings.json) hides the composer mic entirely when disabled Mobile and transport: - iOS/Android microphone permissions added (dictation was previously impossible on mobile) - Fixed Android WebSocket upgrades: the Capacitor WebView origin (https://localhost) was missing from the packaged-client allowlist, 403-ing every WS connection — root cause of the old mobile SSE lock, which is now removed for all transports Security and conventions: - All HTTP routes sit behind the global /api auth gate; the WS upgrade explicitly validates the UI session and origin, with oc_url_token narrowly allowlisted and covered by tests; the dictation socket mints a fresh URL token before connecting - Routes register before the generic OpenCode proxy; the client goes through runtimeFetch/getRuntimeUrlResolver, and runtime switches reset the dictation socket - VS Code deliberately reports dictation as unavailable (no server process in that runtime) CI: workflow Node bumped 20 -> 22 to match the repo engines and fix better-sqlite3 installs broken by node-gyp@latest on Node 20. New dependency: sherpa-onnx-node (prebuilt N-API; macOS/Linux x64+arm64, Windows x64 — Windows-on-ARM falls back to the OpenAI-compatible provider)
269 lines
9.6 KiB
YAML
269 lines
9.6 KiB
YAML
name: Desktop Release Build Smoke
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
repository:
|
|
description: Repository to checkout, for example openchamber/openchamber or daveotero/openchamber
|
|
required: false
|
|
default: openchamber/openchamber
|
|
type: string
|
|
ref:
|
|
description: Git ref to build (branch, tag, or sha)
|
|
required: true
|
|
default: feat/windows-desktop-app
|
|
type: string
|
|
build_macos:
|
|
description: Build signed/notarized macOS Electron artifacts
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
build_windows:
|
|
description: Build Windows Electron installer artifacts
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
retention_days:
|
|
description: Artifact retention days
|
|
required: false
|
|
default: "7"
|
|
type: choice
|
|
options:
|
|
- "1"
|
|
- "3"
|
|
- "7"
|
|
- "14"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build-macos-electron:
|
|
if: ${{ inputs.build_macos }}
|
|
name: Build macOS Electron (${{ matrix.arch }})
|
|
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:
|
|
- name: Checkout selected ref
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
repository: ${{ inputs.repository || github.repository }}
|
|
ref: ${{ inputs.ref || github.ref }}
|
|
|
|
- name: Setup bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Get bundled OpenCode CLI version
|
|
id: opencode_cli_version
|
|
run: |
|
|
VERSION=$(node -p "require('./package.json').dependencies['@opencode-ai/sdk']")
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cache bundled OpenCode CLI artifact
|
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
|
with:
|
|
path: packages/electron/.cache/opencode-cli
|
|
key: opencode-cli-${{ runner.os }}-${{ matrix.arch }}-${{ steps.opencode_cli_version.outputs.version }}
|
|
restore-keys: |
|
|
opencode-cli-${{ runner.os }}-${{ matrix.arch }}-
|
|
|
|
- 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 }}
|
|
ELECTRON_BUILDER_ARCH: ${{ matrix.arch }}
|
|
run: |
|
|
bun run build:web-assets
|
|
bun run prepare:opencode-cli
|
|
bun run verify:opencode-cli
|
|
bun run bundle:main
|
|
# npmRebuild=false in package.json, so electron-builder won't
|
|
# recompile native deps on its own. Rebuild against the target
|
|
# Electron ABI before packaging, matching the release workflow.
|
|
bun run rebuild:native
|
|
bunx electron-builder --mac --${{ matrix.arch }} --publish=never
|
|
bun run verify:opencode-cli:packaged
|
|
|
|
- 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"
|
|
|
|
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
|
|
|
|
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>$key</key>"; then
|
|
echo "Error: required entitlement missing: $key"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- name: Upload macOS installable artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: desktop-release-smoke-macos-${{ matrix.arch }}
|
|
path: |
|
|
packages/electron/dist/*.dmg
|
|
packages/electron/dist/*.zip
|
|
packages/electron/dist/*.blockmap
|
|
packages/electron/dist/latest-mac.yml
|
|
if-no-files-found: error
|
|
retention-days: ${{ fromJSON(inputs.retention_days) }}
|
|
|
|
build-windows-electron:
|
|
if: ${{ inputs.build_windows }}
|
|
name: Build Windows Electron (x64)
|
|
# Match the production release workflow. windows-latest currently resolves
|
|
# to a runner with Visual Studio 18, which this Electron/node-gyp stack does
|
|
# not detect correctly.
|
|
runs-on: windows-2022
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- arch: x64
|
|
target: x86_64-pc-windows-msvc
|
|
platform: win32-x64
|
|
steps:
|
|
- name: Checkout selected ref
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
repository: ${{ inputs.repository || github.repository }}
|
|
ref: ${{ inputs.ref || github.ref }}
|
|
|
|
- name: Setup bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Get bundled OpenCode CLI version
|
|
id: opencode_cli_version
|
|
shell: bash
|
|
run: |
|
|
VERSION=$(node -p "require('./package.json').dependencies['@opencode-ai/sdk']")
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cache bundled OpenCode CLI artifact
|
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
|
with:
|
|
path: packages/electron/.cache/opencode-cli
|
|
key: opencode-cli-${{ runner.os }}-${{ matrix.arch }}-${{ steps.opencode_cli_version.outputs.version }}
|
|
restore-keys: |
|
|
opencode-cli-${{ runner.os }}-${{ matrix.arch }}-
|
|
|
|
- name: Build web assets
|
|
working-directory: packages/electron
|
|
run: bun run build:web-assets
|
|
|
|
- name: Prepare bundled OpenCode CLI
|
|
working-directory: packages/electron
|
|
shell: bash
|
|
run: |
|
|
bun run prepare:opencode-cli
|
|
bun run verify:opencode-cli
|
|
|
|
- name: Bundle main process
|
|
working-directory: packages/electron
|
|
run: bun run bundle:main
|
|
|
|
- name: Rebuild native modules
|
|
working-directory: packages/electron
|
|
shell: bash
|
|
# npmRebuild=false in package.json, so electron-builder won't
|
|
# recompile native deps on its own. Rebuild against the target
|
|
# Electron ABI before packaging, matching the release workflow.
|
|
run: node ./scripts/rebuild-native.mjs
|
|
|
|
- name: Build Windows app
|
|
working-directory: packages/electron
|
|
shell: bash
|
|
run: |
|
|
node ./scripts/package.mjs --win --${{ matrix.arch }} --publish=never
|
|
bun run verify:opencode-cli:packaged
|
|
|
|
- name: Upload Windows installable artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: desktop-release-smoke-windows-${{ matrix.arch }}
|
|
path: |
|
|
packages/electron/dist/*.exe
|
|
packages/electron/dist/*.blockmap
|
|
packages/electron/dist/latest.yml
|
|
if-no-files-found: error
|
|
retention-days: ${{ fromJSON(inputs.retention_days) }}
|