Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
cb26fb6
feat(web-shell): add native Live Voice
LaZzyMan Jul 27, 2026
c9d8712
Merge remote-tracking branch 'origin/main' into lazzy/web-shell-live-…
LaZzyMan Jul 27, 2026
c941066
Merge branch 'main' into lazzy/web-shell-live-voice
wenshao Jul 27, 2026
105dba9
Merge branch 'main' into lazzy/web-shell-live-voice
wenshao Jul 27, 2026
e083779
fix(web-shell): address review feedback for Live Voice PR (#7859)
Jul 27, 2026
6b6b171
fix(cli): register realtime_voice in docs contract and env guard (#7859)
Jul 27, 2026
9199fa6
fix(web-shell): address review feedback for Live Voice PR (#7859)
Jul 27, 2026
85165f1
fix(cli): discard orphaned isolated dir when parent restore fails (#7…
Jul 27, 2026
85c4801
Merge branch 'main' into lazzy/web-shell-live-voice
qwen-code-ci-bot Jul 28, 2026
7110bec
fix(web-shell): address review feedback for Live Voice PR (#7859)
qwen-code-ci-bot Jul 28, 2026
67cb233
fix(serve): harden live turn recovery
LaZzyMan Jul 28, 2026
57f282a
fix(desktop): restore Live Host native build
LaZzyMan Jul 28, 2026
d49e314
Merge remote-tracking branch 'origin/main' into lazzy/web-shell-live-…
LaZzyMan Jul 28, 2026
e653a6f
fix(live): align native host and session isolation
LaZzyMan Jul 28, 2026
7a4df25
Merge remote-tracking branch 'origin/main' into lazzy/web-shell-live-…
LaZzyMan Jul 28, 2026
3d7b7dc
fix(acp): preserve live worker continuation lineage
LaZzyMan Jul 28, 2026
bc72f81
fix(live): classify provider close reasons
LaZzyMan Jul 28, 2026
86803ea
fix(serve): discard unused recovered conversation dirs
LaZzyMan Jul 28, 2026
4185f3e
fix(live): isolate authorized realtime responses
LaZzyMan Jul 28, 2026
ac2b3db
fix(live): preserve realtime response authority
LaZzyMan Jul 28, 2026
56c1127
feat(web-shell): complete Live Voice onboarding
LaZzyMan Aug 1, 2026
1bdd2e8
fix(live): persist realtime-owned dialogue
LaZzyMan Aug 1, 2026
078f252
Merge remote-tracking branch 'origin/main' into lazzy/web-shell-live-…
LaZzyMan Aug 1, 2026
96a2c22
fix(live): preserve final speech while stopping
LaZzyMan Aug 1, 2026
411e9a2
Revert "fix(web-shell): address review feedback for Live Voice PR (#7…
LaZzyMan Aug 2, 2026
ea3c083
Revert "fix(cli): discard orphaned isolated dir when parent restore f…
LaZzyMan Aug 2, 2026
d440763
Revert "fix(web-shell): address review feedback for Live Voice PR (#7…
LaZzyMan Aug 2, 2026
0a3548c
Revert "fix(cli): register realtime_voice in docs contract and env gu…
LaZzyMan Aug 2, 2026
b27d5d5
Revert "fix(web-shell): address review feedback for Live Voice PR (#7…
LaZzyMan Aug 2, 2026
522eb29
revert(live): remove remaining takeover behavior
LaZzyMan Aug 2, 2026
7d85e9c
revert(live): restore pre-rollback implementation
LaZzyMan Aug 2, 2026
aae449f
Merge origin/main into lazzy/web-shell-live-voice
LaZzyMan Aug 2, 2026
f3335bb
test(cli): align Live diagnostics env guard
LaZzyMan Aug 2, 2026
22e6d09
test(release): cover Live Host publication
LaZzyMan Aug 2, 2026
2185661
fix(ci): re-sign Live Host package before verification
LaZzyMan Aug 2, 2026
5f63a39
fix(serve): scope sent completion notifications to Live
LaZzyMan Aug 2, 2026
de64483
Merge branch 'main' into lazzy/web-shell-live-voice
LaZzyMan Aug 3, 2026
83cc16a
fix(web-shell): preserve live setup errors
LaZzyMan Aug 3, 2026
765f95f
fix(live): align realtime backend speech lifecycle
LaZzyMan Aug 4, 2026
cf1939d
ci(live): publish Live Host independently
LaZzyMan Aug 4, 2026
30b5ed0
Merge remote-tracking branch 'lazzy/lazzy/web-shell-live-voice' into …
LaZzyMan Aug 4, 2026
a8f94a2
Merge remote-tracking branch 'origin/main' into lazzy/web-shell-live-…
LaZzyMan Aug 4, 2026
b8d8965
test(cli): mock Live speech bridge handler
LaZzyMan Aug 4, 2026
28dfc42
test(release): align Live Host workflow contract
LaZzyMan Aug 4, 2026
5a1046d
Merge origin/main into lazzy/web-shell-live-voice
LaZzyMan Aug 4, 2026
e84799c
fix(live): address release and lifecycle review findings
LaZzyMan Aug 5, 2026
4e7761c
Merge remote-tracking branch 'origin/main' into lazzy/web-shell-live-…
LaZzyMan Aug 5, 2026
dfd0b3e
fix(live): release completed call tracking
LaZzyMan Aug 5, 2026
4c0811d
Merge remote-tracking branch 'origin/main' into lazzy/web-shell-live-…
LaZzyMan Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ concurrency:
cancel-in-progress: false

env:
BUN_VERSION: '1.3.9'
NODE_VERSION: '22.20.0'
DESKTOP_FEED_TAG: 'desktop-latest'

Expand Down
322 changes: 322 additions & 0 deletions .github/workflows/live-host-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
name: 'Qwen Live Host Release'

run-name: "Qwen Live Host release ${{ inputs.version || format('PR #{0} dry run', github.event.pull_request.number) }}"

on:
pull_request:
paths:
- '.github/workflows/live-host-release.yml'
- 'packages/desktop/apps/live-host/**'
- 'packages/desktop/bun.lock'
- 'packages/desktop/package.json'
- 'packages/desktop/scripts/bump-live-host-version.ts'
workflow_dispatch:
inputs:
version:
description: 'Live Host version, for example 0.1.0 or v0.1.0.'
required: true
type: 'string'
dry_run:
description: 'Build unsigned packages without publishing.'
required: true
default: true
type: 'boolean'
draft:
description: 'Create a draft GitHub release.'
required: true
default: true
type: 'boolean'
prerelease:
description: 'Mark the release as a prerelease.'
required: true
default: false
type: 'boolean'
clobber:
description: 'Replace same-named assets in an existing release.'
required: true
default: false
type: 'boolean'

permissions:
contents: 'read'

concurrency:
group: 'live-host-release-${{ github.event.pull_request.number || inputs.version }}'
cancel-in-progress: false

env:
BUN_VERSION: '1.3.9'
NODE_VERSION: '22.20.0'
LIVE_HOST_FEED_TAG: 'live-host-latest'

jobs:
prepare:
name: 'Prepare Live Host release'
runs-on: 'ubuntu-latest'
timeout-minutes: 10
outputs:
tag: '${{ steps.version.outputs.tag }}'
version: '${{ steps.version.outputs.version }}'
steps:
- name: 'Resolve version'
id: 'version'
shell: 'bash'
env:
INPUT_VERSION: '${{ inputs.version }}'
PR_NUMBER: '${{ github.event.pull_request.number }}'
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" = 'pull_request' ]; then
version="0.0.$PR_NUMBER"
else
version="${INPUT_VERSION#v}"
fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Live Host version must be valid SemVer: $INPUT_VERSION"
exit 1
fi
if [[ "$version" == *+* ]]; then
echo '::error::Live Host releases do not support SemVer build metadata.'
exit 1
fi
if [ "$GITHUB_EVENT_NAME" = 'workflow_dispatch' ] && [ "${{ inputs.dry_run }}" = 'false' ] && [ "$GITHUB_REF_NAME" != 'main' ]; then
echo '::error::Published Live Host releases must run from main.'
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=live-host-v$version" >> "$GITHUB_OUTPUT"

build:
name: 'Build Qwen Live Host'
needs: 'prepare'
runs-on: 'macos-latest'
timeout-minutes: 60
steps:
- uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3

- uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version: '${{ env.NODE_VERSION }}'

- uses: 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' # v2
with:
bun-version: '${{ env.BUN_VERSION }}'

- name: 'Install dependencies'
working-directory: 'packages/desktop'
run: 'bun install --frozen-lockfile'

- name: 'Set Live Host version'
working-directory: 'packages/desktop'
run: 'bun run bump-live-host-version "${{ needs.prepare.outputs.version }}"'

- name: 'Test Live Host'
working-directory: 'packages/desktop'
run: 'bun run live-host:typecheck && bun run live-host:test'

- name: 'Import macOS certificate'
if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}"
shell: 'bash'
env:
APPLE_CERTIFICATE: '${{ secrets.APPLE_CERTIFICATE }}'
APPLE_CERTIFICATE_PASSWORD: '${{ secrets.APPLE_CERTIFICATE_PASSWORD }}'
KEYCHAIN_PASSWORD: '${{ secrets.APPLE_KEYCHAIN_PASSWORD }}'
run: |
set -euo pipefail
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD KEYCHAIN_PASSWORD; do
if [ -z "${!name}" ]; then echo "::error::$name is required for published Qwen Live Host releases."; exit 1; fi
done
certificate="$RUNNER_TEMP/qwen-live-host.p12"
keychain="$RUNNER_TEMP/qwen-live-host.keychain-db"
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$certificate"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain"
security import "$certificate" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain"
security list-keychains -d user -s "$keychain" login.keychain-db
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$keychain"
identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)"
if [ -z "$identity" ]; then echo '::error::Developer ID Application identity was not found.'; exit 1; fi
{
echo "CSC_NAME=$identity"
echo 'CSC_IDENTITY_AUTO_DISCOVERY=true'
} >> "$GITHUB_ENV"

- name: 'Configure notarization'
if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}"
shell: 'bash'
env:
APPLE_API_ISSUER: '${{ secrets.APPLE_API_ISSUER }}'
APPLE_API_KEY_ID: '${{ secrets.APPLE_API_KEY }}'
APPLE_API_KEY_P8: '${{ secrets.APPLE_API_KEY_P8 }}'
APPLE_TEAM_ID: '${{ secrets.APPLE_TEAM_ID }}'
run: |
set -euo pipefail
for name in APPLE_API_ISSUER APPLE_API_KEY_ID APPLE_API_KEY_P8 APPLE_TEAM_ID; do
if [ -z "${!name}" ]; then echo "::error::$name is required for Qwen Live Host notarization."; exit 1; fi
done
key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf '%s' "$APPLE_API_KEY_P8" > "$key_path"
{
echo "APPLE_API_KEY=$key_path"
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID"
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER"
echo "APPLE_TEAM_ID=$APPLE_TEAM_ID"
} >> "$GITHUB_ENV"

- name: 'Build packages'
working-directory: 'packages/desktop'
env:
CSC_IDENTITY_AUTO_DISCOVERY: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}"
run: 'bun run live-host:dist:mac:no-publish'

- name: 'Verify release assets'
working-directory: 'packages/desktop/apps/live-host'
env:
RELEASE_VERSION: '${{ needs.prepare.outputs.version }}'
shell: 'bash'
run: |
set -euo pipefail
for asset in \
Qwen-Live-Host-arm64.dmg \
Qwen-Live-Host-x64.dmg \
Qwen-Live-Host-arm64.zip \
Qwen-Live-Host-x64.zip \
Qwen-Live-Host-manifest.json; do
test -f "release/$asset"
done
node --input-type=module -e '
import { createHash } from "node:crypto";
import { readFileSync, statSync } from "node:fs";
const manifest = JSON.parse(readFileSync("release/Qwen-Live-Host-manifest.json", "utf8"));
if (manifest.version !== process.env.RELEASE_VERSION) {
throw new Error("Manifest version " + manifest.version + " does not match " + process.env.RELEASE_VERSION + ".");
}
for (const architecture of ["arm64", "x64"]) {
const name = "Qwen-Live-Host-" + architecture + ".zip";
const path = "release/" + name;
const bytes = readFileSync(path);
const asset = manifest.assets?.[architecture];
if (asset?.name !== name || asset.size !== statSync(path).size || asset.sha256 !== createHash("sha256").update(bytes).digest("hex")) {
throw new Error("Manifest asset verification failed for " + architecture + ".");
}
}
'

- name: 'Verify signing and notarization'
if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}"
working-directory: 'packages/desktop'
shell: 'bash'
run: |
set -euo pipefail
app_count=0
while IFS= read -r -d '' app; do
app_count=$((app_count + 1))
codesign --verify --deep --strict --verbose=2 "$app"
signature="$(codesign -dv --verbose=4 "$app" 2>&1)"
if ! grep -q '^Authority=Developer ID Application:' <<<"$signature" || ! grep -qx 'TeamIdentifier=NF4574S59H' <<<"$signature"; then
echo '::error::Qwen Live Host was not signed by the expected Developer ID team.'
exit 1
fi
spctl -a -vv -t exec "$app"
xcrun stapler validate "$app"
done < <(find apps/live-host/release -mindepth 2 -maxdepth 2 -type d -name '*.app' -print0)
if [ "$app_count" -eq 0 ]; then echo '::error::No packaged Qwen Live Host application was found.'; exit 1; fi
dmg_count=0
while IFS= read -r -d '' dmg; do
dmg_count=$((dmg_count + 1))
hdiutil verify "$dmg"
done < <(find apps/live-host/release -type f -name '*.dmg' -print0)
if [ "$dmg_count" -eq 0 ]; then echo '::error::No Qwen Live Host disk image was found.'; exit 1; fi

- uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
with:
name: 'qwen-live-host-macos'
path: |
packages/desktop/apps/live-host/release/*.dmg
packages/desktop/apps/live-host/release/*-manifest.json
packages/desktop/apps/live-host/release/*.zip
if-no-files-found: 'error'
retention-days: 14

publish:
name: 'Publish Qwen Live Host release'
if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false && github.repository == 'QwenLM/qwen-code' }}"
needs:
- 'prepare'
- 'build'
runs-on: 'ubuntu-latest'
timeout-minutes: 20
permissions:
contents: 'write'
env:
GH_REPO: '${{ github.repository }}'
steps:
- uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1
with:
path: 'release-assets'
merge-multiple: true

- name: 'Generate checksums'
working-directory: 'release-assets'
run: 'sha256sum -- * > SHA256SUMS.txt'

- name: 'Create GitHub release'
id: 'release'
env:
GH_TOKEN: '${{ github.token }}'
RELEASE_TAG: '${{ needs.prepare.outputs.tag }}'
RELEASE_VERSION: '${{ needs.prepare.outputs.version }}'
RELEASE_DRAFT: '${{ inputs.draft }}'
RELEASE_PRERELEASE: '${{ inputs.prerelease }}'
RELEASE_CLOBBER: '${{ inputs.clobber }}'
run: |
set -euo pipefail
args=("$RELEASE_TAG" release-assets/* --target "$GITHUB_SHA" --title "Qwen Live Host v$RELEASE_VERSION" --generate-notes --latest=false)
if [ "$RELEASE_DRAFT" = 'true' ]; then args+=(--draft); fi
if [ "$RELEASE_PRERELEASE" = 'true' ]; then args+=(--prerelease); fi
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
if [ "$RELEASE_CLOBBER" != 'true' ]; then
echo "::error::Release $RELEASE_TAG already exists. Re-run with clobber=true to replace its assets."
exit 1
fi
gh release upload "$RELEASE_TAG" release-assets/* --clobber
edit_args=()
if [ "$RELEASE_DRAFT" = 'true' ]; then edit_args+=(--draft); else edit_args+=(--draft=false); fi
if [ "$RELEASE_PRERELEASE" = 'true' ]; then edit_args+=(--prerelease); else edit_args+=(--prerelease=false); fi
gh release edit "$RELEASE_TAG" "${edit_args[@]}"
release_url="$(gh release view "$RELEASE_TAG" --json url --jq '.url')"
else
release_url="$(gh release create "${args[@]}")"
fi
echo "url=$release_url" >> "$GITHUB_OUTPUT"

- name: 'Update stable Live Host feed'
if: '${{ inputs.draft == false && inputs.prerelease == false }}'
env:
GH_TOKEN: '${{ github.token }}'
FEED_TAG: '${{ env.LIVE_HOST_FEED_TAG }}'
run: |
set -euo pipefail
stable_assets=(
release-assets/Qwen-Live-Host-manifest.json
release-assets/Qwen-Live-Host-arm64.zip
release-assets/Qwen-Live-Host-x64.zip
)
if gh release view "$FEED_TAG" >/dev/null 2>&1; then
gh release upload "$FEED_TAG" "${stable_assets[@]}" --clobber
else
gh release create "$FEED_TAG" "${stable_assets[@]}" --title 'Qwen Live Host latest' --notes 'Stable Qwen Live Host installer feed.' --latest=false
fi

- name: 'Publish release summary'
env:
RELEASE_URL: '${{ steps.release.outputs.url }}'
RELEASE_VERSION: '${{ needs.prepare.outputs.version }}'
run: |
{
echo '## Qwen Live Host release'
echo
echo "Version: $RELEASE_VERSION"
echo "Release: $RELEASE_URL"
} >> "$GITHUB_STEP_SUMMARY"
Loading
Loading