From d4510c3ca80e3a212d5639dcd66df802ef1b50e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 06:20:03 +0000 Subject: [PATCH 1/5] CI: build and test on macOS 27 with Xcode 27 as well Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ihz5z81AZ27T33PMCKkaL --- .github/workflows/ci.yml | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0224e81..916dbb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,17 +14,34 @@ concurrency: jobs: swift: - name: Swift build + tests (macOS) - runs-on: macos-15 + name: Swift build + tests (${{ matrix.name }}) + runs-on: ${{ matrix.runner }} timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - name: macOS 15, Xcode 26.2 + slug: macos15 + runner: macos-15 + xcode: "26.2" + # The OS most users are on: build with its SDK and run the tests on + # it. GitHub names this image after its Xcode; it runs macOS 27. + - name: macOS 27, Xcode 27 + slug: macos27 + runner: xcode-27 + xcode: "27" steps: - name: Checkout uses: actions/checkout@v4 - - name: Select Xcode + - name: Select Xcode ${{ matrix.xcode }} run: | - sudo xcode-select -s /Applications/Xcode_26.2.app 2>/dev/null \ - || sudo xcode-select -s "$(ls -d /Applications/Xcode_2*.app | sort -V | tail -1)" + sw_vers + ls -d /Applications/Xcode*.app + # The newest release of this Xcode, not a beta. + XCODE="$(ls -d /Applications/Xcode_${{ matrix.xcode }}*.app | grep -iv beta | sort -V | tail -1)" + sudo xcode-select -s "$XCODE" xcodebuild -version - name: Install XcodeGen @@ -37,8 +54,8 @@ jobs: uses: actions/cache@v4 with: path: build/SourcePackages - key: spm-${{ runner.os }}-${{ hashFiles('project.yml') }} - restore-keys: spm-${{ runner.os }}- + key: spm-${{ matrix.slug }}-${{ hashFiles('project.yml') }} + restore-keys: spm-${{ matrix.slug }}- - name: Resolve Swift package dependencies run: | @@ -122,7 +139,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: xcodebuild-logs + name: xcodebuild-logs-${{ matrix.slug }} path: | build/xcodebuild-build.log build/xcodebuild-test.log @@ -133,7 +150,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-${{ matrix.slug }} path: build/TestResults.xcresult if-no-files-found: ignore From d7f56b930839baa797e6c1aa49143a648a6019db Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 06:20:48 +0000 Subject: [PATCH 2/5] Release: build the Apple silicon DMG with Xcode 27 on macOS 27 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ihz5z81AZ27T33PMCKkaL --- .github/workflows/release.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53ce1c4..c0db027 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,23 +43,35 @@ jobs: # One DMG per architecture rather than a universal app: each download is # half the size, and the Intel build says plainly that it cannot run the # local MLX engines. + # + # Apple silicon is built with Xcode 27 on macOS 27, so the app is linked + # against the macOS 27 SDK and gets that system's behavior rather than a + # compatibility mode. macOS 27 does not run on Intel Macs, so the Intel + # build stays on the toolchain of the last system they run. build: name: Build DMG (${{ matrix.arch }}) needs: [version, test] - runs-on: macos-15 + runs-on: ${{ matrix.runner }} strategy: matrix: - arch: [arm64, x86_64] + include: + - arch: arm64 + runner: xcode-27 + xcode: "27" + - arch: x86_64 + runner: macos-15 + xcode: "26.2" env: VERSION: ${{ needs.version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Select Xcode + - name: Select Xcode ${{ matrix.xcode }} run: | - sudo xcode-select -s /Applications/Xcode_26.2.app 2>/dev/null \ - || sudo xcode-select -s "$(ls -d /Applications/Xcode_2*.app | sort -V | tail -1)" + # The newest release of this Xcode, not a beta. + XCODE="$(ls -d /Applications/Xcode_${{ matrix.xcode }}*.app | grep -iv beta | sort -V | tail -1)" + sudo xcode-select -s "$XCODE" xcodebuild -version - name: Install XcodeGen From fcda4157c559aada96382b424498ddc0b7c1d99a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 06:26:25 +0000 Subject: [PATCH 3/5] Pick a released Xcode, not a beta; clear the concurrency warnings; 0.16.7 On the xcode-27 runner Xcode_27.2.app is a link to the 27.2 beta, so filtering names for "beta" still picked it. Each path is now resolved before the filter, and the step fails if no release is left. SentenceBlock is Sendable when its lines are, so the transcript and live rows' nonisolated `==` can read it; Xcode 27 warned on both (an error in Swift 6 mode). Version 0.16.7 (build 25). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ihz5z81AZ27T33PMCKkaL --- .github/workflows/ci.yml | 14 +++++++++++--- .github/workflows/release.yml | 13 +++++++++++-- Core/Pipeline/TranscriptBuffer.swift | 4 ++-- Features/Shared/SessionPresentation.swift | 3 +++ project.yml | 4 ++-- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 916dbb3..db50694 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,9 +38,17 @@ jobs: - name: Select Xcode ${{ matrix.xcode }} run: | sw_vers - ls -d /Applications/Xcode*.app - # The newest release of this Xcode, not a beta. - XCODE="$(ls -d /Applications/Xcode_${{ matrix.xcode }}*.app | grep -iv beta | sort -V | tail -1)" + # The newest release of this Xcode, not a beta. A version-named path + # can be a link to a beta (Xcode_27.2.app -> Xcode_27.2_beta.app), + # so judge each by where it really points. + XCODE="$(for app in /Applications/Xcode_${{ matrix.xcode }}*.app; do realpath "$app"; done \ + | grep -viE 'beta|release_candidate|_rc' | sort -uV | tail -1)" + if [ -z "$XCODE" ]; then + echo "::error::no release of Xcode ${{ matrix.xcode }} on this runner" + ls -l /Applications | grep Xcode + exit 1 + fi + echo "Using $XCODE" sudo xcode-select -s "$XCODE" xcodebuild -version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0db027..2aa5f78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,8 +69,17 @@ jobs: - name: Select Xcode ${{ matrix.xcode }} run: | - # The newest release of this Xcode, not a beta. - XCODE="$(ls -d /Applications/Xcode_${{ matrix.xcode }}*.app | grep -iv beta | sort -V | tail -1)" + # The newest release of this Xcode, not a beta. A version-named path + # can be a link to a beta (Xcode_27.2.app -> Xcode_27.2_beta.app), + # so judge each by where it really points. + XCODE="$(for app in /Applications/Xcode_${{ matrix.xcode }}*.app; do realpath "$app"; done \ + | grep -viE 'beta|release_candidate|_rc' | sort -uV | tail -1)" + if [ -z "$XCODE" ]; then + echo "::error::no release of Xcode ${{ matrix.xcode }} on this runner" + ls -l /Applications | grep Xcode + exit 1 + fi + echo "Using $XCODE" sudo xcode-select -s "$XCODE" xcodebuild -version diff --git a/Core/Pipeline/TranscriptBuffer.swift b/Core/Pipeline/TranscriptBuffer.swift index 4c35797..77f387e 100644 --- a/Core/Pipeline/TranscriptBuffer.swift +++ b/Core/Pipeline/TranscriptBuffer.swift @@ -94,7 +94,7 @@ final class TranscriptBuffer: ObservableObject { /// the only signal, so a line whose translation failed, came back empty, was /// covered by a later T3PO translation or was never sent (translation off) /// showed as translating forever. -enum LiveTranslationState: Hashable { +enum LiveTranslationState: Hashable, Sendable { /// Not sent: translation was off, or the line's sentence has not ended. case notSent case pending @@ -105,7 +105,7 @@ enum LiveTranslationState: Hashable { case merged } -struct LiveSegment: Identifiable, Hashable { +struct LiveSegment: Identifiable, Hashable, Sendable { let id: Int64 // = rowId let rowId: Int64 let startMs: Int64 diff --git a/Features/Shared/SessionPresentation.swift b/Features/Shared/SessionPresentation.swift index 6e8d220..698dc0a 100644 --- a/Features/Shared/SessionPresentation.swift +++ b/Features/Shared/SessionPresentation.swift @@ -102,6 +102,9 @@ struct SentenceBlock: Identifiable { var id: Line.ID { lines[0].id } } +/// So the rows' nonisolated `==` can read a block off a main-actor view. +extension SentenceBlock: Sendable where Line: Sendable {} + extension Array where Element == Segment { var sentenceBlocks: [SentenceBlock] { SentenceGroups.group(self).map { SentenceBlock(lines: $0) } diff --git a/project.yml b/project.yml index 49d92d5..2290365 100644 --- a/project.yml +++ b/project.yml @@ -15,8 +15,8 @@ settings: SWIFT_STRICT_CONCURRENCY: complete SWIFT_TREAT_WARNINGS_AS_ERRORS: "NO" ENABLE_USER_SCRIPT_SANDBOXING: NO - MARKETING_VERSION: "0.16.6" - CURRENT_PROJECT_VERSION: "24" + MARKETING_VERSION: "0.16.7" + CURRENT_PROJECT_VERSION: "25" DEAD_CODE_STRIPPING: YES packages: From bef2d8466cd96d9dbe612b793ccf0d0f5ddcb877 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 06:32:09 +0000 Subject: [PATCH 4/5] TEMP: probe the release DMG builds on this branch Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ihz5z81AZ27T33PMCKkaL --- .github/workflows/release-dmg-probe.yml | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/release-dmg-probe.yml diff --git a/.github/workflows/release-dmg-probe.yml b/.github/workflows/release-dmg-probe.yml new file mode 100644 index 0000000..bcf1c84 --- /dev/null +++ b/.github/workflows/release-dmg-probe.yml @@ -0,0 +1,59 @@ +name: Release DMG probe (temporary) +'on': + push: + branches: + - claude/sweet-dijkstra-lbh7a4 +jobs: + build: + name: Build DMG (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + matrix: + include: + - arch: arm64 + runner: xcode-27 + xcode: '27' + - arch: x86_64 + runner: macos-15 + xcode: '26.2' + env: + VERSION: 0.16.7-probe + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Select Xcode ${{ matrix.xcode }} + run: "# The newest release of this Xcode, not a beta. A version-named path\n# can be a link to a beta (Xcode_27.2.app -> Xcode_27.2_beta.app),\n# so judge each by where it really points.\nXCODE=\"\ + $(for app in /Applications/Xcode_${{ matrix.xcode }}*.app; do realpath \"$app\"; done \\\n | grep -viE 'beta|release_candidate|_rc' | sort -uV | tail -1)\"\nif [ -z \"$XCODE\" ]; then\n echo \"\ + ::error::no release of Xcode ${{ matrix.xcode }} on this runner\"\n ls -l /Applications | grep Xcode\n exit 1\nfi\necho \"Using $XCODE\"\nsudo xcode-select -s \"$XCODE\"\nxcodebuild -version\n" + - name: Install XcodeGen + run: brew install xcodegen + - name: Generate Xcode project + run: xcodegen generate + - name: Resolve Swift package dependencies + run: "xcodebuild -resolvePackageDependencies \\\n -project ClassNote.xcodeproj \\\n -scheme ClassNote\n" + - name: Build Release (${{ matrix.arch }}) + run: "xcodebuild -project ClassNote.xcodeproj \\\n -scheme ClassNote \\\n -configuration Release \\\n -destination 'generic/platform=macOS' \\\n -derivedDataPath\ + \ build/DerivedData \\\n -skipMacroValidation \\\n ARCHS=${{ matrix.arch }} \\\n ONLY_ACTIVE_ARCH=NO \\\n build\n" + - name: Check the binary is ${{ matrix.arch }} only + run: "set -euo pipefail\nBINARY=\"build/DerivedData/Build/Products/Release/ClassNote.app/Contents/MacOS/ClassNote\"\nARCHS=\"$(lipo -archs \"$BINARY\")\"\necho \"ClassNote binary: $ARCHS\"\nif [[\ + \ \"$ARCHS\" != \"${{ matrix.arch }}\" ]]; then\n echo \"::error::expected only ${{ matrix.arch }}, got: $ARCHS\"\n exit 1\nfi\n" + - name: 'Probe: SDK and LaTeX in the Release app' + run: 'set -euo pipefail + + APP="build/DerivedData/Build/Products/Release/ClassNote.app" + + otool -l "$APP/Contents/MacOS/ClassNote" | grep -A4 LC_BUILD_VERSION + + ls "$APP/Contents/Resources" | grep SwiftMath + + ' + - name: Package DMG + run: "set -euo pipefail\nAPP_PATH=\"build/DerivedData/Build/Products/Release/ClassNote.app\"\nif [[ ! -d \"$APP_PATH\" ]]; then\n echo \"\u2717 ClassNote.app not found at $APP_PATH\"\n exit 1\n\ + fi\n\nSTAGE_DIR=\"build/dmg-stage\"\nrm -rf \"$STAGE_DIR\"\nmkdir -p \"$STAGE_DIR\"\ncp -R \"$APP_PATH\" \"$STAGE_DIR/\"\nln -s /Applications \"$STAGE_DIR/Applications\"\n\nDMG_NAME=\"ClassNote-${VERSION}-${{\ + \ matrix.arch }}.dmg\"\nhdiutil create -volname \"ClassNote\" \\\n -srcfolder \"$STAGE_DIR\" \\\n -ov -format UDZO \\\n \"$DMG_NAME\"\n\necho \"DMG_NAME=$DMG_NAME\"\ + \ >> \"$GITHUB_ENV\"\n" + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ClassNote-dmg-${{ matrix.arch }} + path: ${{ env.DMG_NAME }} From 45b1d3f8f12028fe7323d0bc2548de323d2fe735 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 06:39:34 +0000 Subject: [PATCH 5/5] Remove the temporary release DMG probe Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ihz5z81AZ27T33PMCKkaL --- .github/workflows/release-dmg-probe.yml | 59 ------------------------- 1 file changed, 59 deletions(-) delete mode 100644 .github/workflows/release-dmg-probe.yml diff --git a/.github/workflows/release-dmg-probe.yml b/.github/workflows/release-dmg-probe.yml deleted file mode 100644 index bcf1c84..0000000 --- a/.github/workflows/release-dmg-probe.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release DMG probe (temporary) -'on': - push: - branches: - - claude/sweet-dijkstra-lbh7a4 -jobs: - build: - name: Build DMG (${{ matrix.arch }}) - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - arch: arm64 - runner: xcode-27 - xcode: '27' - - arch: x86_64 - runner: macos-15 - xcode: '26.2' - env: - VERSION: 0.16.7-probe - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Select Xcode ${{ matrix.xcode }} - run: "# The newest release of this Xcode, not a beta. A version-named path\n# can be a link to a beta (Xcode_27.2.app -> Xcode_27.2_beta.app),\n# so judge each by where it really points.\nXCODE=\"\ - $(for app in /Applications/Xcode_${{ matrix.xcode }}*.app; do realpath \"$app\"; done \\\n | grep -viE 'beta|release_candidate|_rc' | sort -uV | tail -1)\"\nif [ -z \"$XCODE\" ]; then\n echo \"\ - ::error::no release of Xcode ${{ matrix.xcode }} on this runner\"\n ls -l /Applications | grep Xcode\n exit 1\nfi\necho \"Using $XCODE\"\nsudo xcode-select -s \"$XCODE\"\nxcodebuild -version\n" - - name: Install XcodeGen - run: brew install xcodegen - - name: Generate Xcode project - run: xcodegen generate - - name: Resolve Swift package dependencies - run: "xcodebuild -resolvePackageDependencies \\\n -project ClassNote.xcodeproj \\\n -scheme ClassNote\n" - - name: Build Release (${{ matrix.arch }}) - run: "xcodebuild -project ClassNote.xcodeproj \\\n -scheme ClassNote \\\n -configuration Release \\\n -destination 'generic/platform=macOS' \\\n -derivedDataPath\ - \ build/DerivedData \\\n -skipMacroValidation \\\n ARCHS=${{ matrix.arch }} \\\n ONLY_ACTIVE_ARCH=NO \\\n build\n" - - name: Check the binary is ${{ matrix.arch }} only - run: "set -euo pipefail\nBINARY=\"build/DerivedData/Build/Products/Release/ClassNote.app/Contents/MacOS/ClassNote\"\nARCHS=\"$(lipo -archs \"$BINARY\")\"\necho \"ClassNote binary: $ARCHS\"\nif [[\ - \ \"$ARCHS\" != \"${{ matrix.arch }}\" ]]; then\n echo \"::error::expected only ${{ matrix.arch }}, got: $ARCHS\"\n exit 1\nfi\n" - - name: 'Probe: SDK and LaTeX in the Release app' - run: 'set -euo pipefail - - APP="build/DerivedData/Build/Products/Release/ClassNote.app" - - otool -l "$APP/Contents/MacOS/ClassNote" | grep -A4 LC_BUILD_VERSION - - ls "$APP/Contents/Resources" | grep SwiftMath - - ' - - name: Package DMG - run: "set -euo pipefail\nAPP_PATH=\"build/DerivedData/Build/Products/Release/ClassNote.app\"\nif [[ ! -d \"$APP_PATH\" ]]; then\n echo \"\u2717 ClassNote.app not found at $APP_PATH\"\n exit 1\n\ - fi\n\nSTAGE_DIR=\"build/dmg-stage\"\nrm -rf \"$STAGE_DIR\"\nmkdir -p \"$STAGE_DIR\"\ncp -R \"$APP_PATH\" \"$STAGE_DIR/\"\nln -s /Applications \"$STAGE_DIR/Applications\"\n\nDMG_NAME=\"ClassNote-${VERSION}-${{\ - \ matrix.arch }}.dmg\"\nhdiutil create -volname \"ClassNote\" \\\n -srcfolder \"$STAGE_DIR\" \\\n -ov -format UDZO \\\n \"$DMG_NAME\"\n\necho \"DMG_NAME=$DMG_NAME\"\ - \ >> \"$GITHUB_ENV\"\n" - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: ClassNote-dmg-${{ matrix.arch }} - path: ${{ env.DMG_NAME }}