From 9f308fadd29308b01ed6481dd2492e49a8742f2a Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:00:51 +0800 Subject: [PATCH] feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows. --- .github/ISSUE_TEMPLATE/bug_report.md | 4 + .github/workflows/ci.yml | 145 +-- .swiftlint.yml | 6 + AGENTS.md | 46 +- AUDIT_APPSTORE.md | 313 ----- CHANGELOG.md | 17 +- CONTRIBUTING.md | 30 +- OSGKeyboard/OSGKeyboardApp.swift | 91 +- OSGKeyboard/Resources/PrivacyPolicy.html | 26 +- .../Services/AIHintRefreshService.swift | 126 ++ OSGKeyboard/Services/AppPermissions.swift | 12 +- OSGKeyboard/Services/FlowSessionManager.swift | 298 +++-- OSGKeyboard/Views/AIKeyboardDemoView.swift | 133 +++ OSGKeyboard/Views/APISettingsCard.swift | 60 +- OSGKeyboard/Views/ASRSettingsCard.swift | 54 +- .../Views/ClipboardHistoryDemoView.swift | 265 +++++ .../Components/HomeUsageStatsSection.swift | 17 +- .../Views/Components/MinimalTabBar.swift | 85 +- OSGKeyboard/Views/EditDemoView.swift | 297 ++--- OSGKeyboard/Views/HistoryView.swift | 91 +- OSGKeyboard/Views/HomeView.swift | 351 ++++-- .../Views/LocalEngineSettingsRows.swift | 2 +- OSGKeyboard/Views/MainAppRoot.swift | 72 +- OSGKeyboard/Views/MainTabContent.swift | 4 - OSGKeyboard/Views/NotesHostDemoView.swift | 166 +++ OSGKeyboard/Views/OnboardingView.swift | 8 +- .../Views/PersonalDictionaryView.swift | 75 +- OSGKeyboard/Views/PreviewASRController.swift | 10 - OSGKeyboard/Views/SettingsICloudSyncRow.swift | 34 +- .../Views/SettingsPreferenceRows.swift | 44 +- .../Views/SettingsProviderControls.swift | 143 ++- .../Views/SettingsSecondaryPages.swift | 65 +- OSGKeyboard/Views/TranslationPickerRow.swift | 15 +- OSGKeyboard/en.lproj/Localizable.strings | 33 +- OSGKeyboard/zh-Hans.lproj/Localizable.strings | 39 +- OSGKeyboardExt/KeyboardViewController.swift | 158 ++- .../Services/AIKeyboardCoordinator.swift | 32 + .../Services/AppGroupPersistor.swift | 35 +- .../ClipboardCaptureCoordinator.swift | 254 +++- .../Services/EditHintScheduler.swift | 71 ++ .../Services/KeyboardFlowCoordinator.swift | 70 +- .../Services/KeyboardTextInserter.swift | 30 +- .../Services/LastInputEditCoordinator.swift | 37 +- .../Services/WhatsNewDemoDriver.swift | 317 +++++ .../Typing/KeyboardSurfaceRoot.swift | 53 +- OSGKeyboardExt/Typing/TypingRootView.swift | 10 +- OSGKeyboardExt/Utilities/ExtL10n.swift | 5 +- OSGKeyboardExt/Views/AIKeyboardView.swift | 136 ++- .../Views/ClipboardKeyboardViews.swift | 170 ++- OSGKeyboardExt/Views/KeyboardRootView.swift | 101 +- .../Views/KeyboardTopControls.swift | 166 +-- OSGKeyboardExt/Views/LastInputEditView.swift | 21 +- .../Views/NativeKeyboardKeyStyle.swift | 6 +- .../Views/ToolbarActionButtons.swift | 114 +- OSGKeyboardExt/en.lproj/Keyboard.strings | 29 +- OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings | 29 +- .../ClipboardSuggestionLifecycleTests.swift | 223 ++++ .../EditHintSchedulerTests.swift | 213 ++++ OSGKeyboardExtTests/EnglishTypingTests.swift | 105 ++ OSGKeyboardExtTests/KeyboardStateTests.swift | 20 + .../RimeSchemaGeneratorTests.swift | 52 +- .../DesignSystem/SevenDayUsageChart.swift | 2 +- .../SupportDeveloperSection.swift | 2 +- .../DesignSystem/UsageStatsCluster.swift | 88 +- .../Services/ASRChunkTranscribing.swift | 2 +- .../Services/ASRService.swift | 28 +- .../Services/ChunkedUtterancePipeline.swift | 10 +- .../CloudASR/AlibabaVocabularySync.swift | 2 +- .../CloudASR/BailianRealtimeASRClient.swift | 6 +- .../Services/CloudASR/CloudASRClients.swift | 2 +- .../CloudASR/CloudASRConnectionCheck.swift | 2 +- .../Services/CloudASR/CloudASRService.swift | 18 +- .../Services/CloudASR/CloudASRStreaming.swift | 35 +- .../CloudASR/OpenAIRealtimeASRClient.swift | 11 +- .../CloudASR/VolcengineCloudASRClient.swift | 11 +- .../Services/CustomLanguageModelManager.swift | 14 +- .../FlowAudioSessionCoordinator.swift | 5 + .../Services/FlowCaptureVoiceProcessing.swift | 2 + .../Services/FlowContinuousCapture.swift | 5 +- .../Services/LiveDictationController.swift | 45 +- .../Services/Tip/TipPurchaseManager.swift | 2 +- OSGKeyboardMac/MacComponents.swift | 35 - OSGKeyboardMac/MacICloudSyncRows.swift | 28 +- OSGKeyboardMac/MacOnboardingView.swift | 4 +- OSGKeyboardMac/MacSettingsComponents.swift | 146 ++- OSGKeyboardMac/MacSettingsView.swift | 104 +- .../LiveConfigurationStore.swift | 4 +- .../DesignSystem/EditTextPager.swift | 6 +- .../DesignSystem/PolishStyleIconBadge.swift | 41 - .../DesignSystem/RecordButton.swift | 37 +- .../DesignSystem/TranslationChip.swift | 110 -- OSGKeyboardShared/Models/AIHintModels.swift | 164 +++ .../Models/AppGroupConfiguration.swift | 64 +- .../Models/EditableInputReference.swift | 6 +- .../Models/FlowSession/FlowAck.swift | 43 + .../Models/FlowSession/FlowCommand.swift | 87 ++ .../Models/FlowSession/FlowFieldContext.swift | 43 + .../FlowSession/FlowReadySnapshot.swift | 67 ++ .../Models/FlowSession/FlowResult.swift | 88 ++ .../FlowSession/FlowStartTransaction.swift | 33 + .../FlowSession/FlowTranscriptionError.swift | 14 + .../Models/FlowUtteranceRequest.swift | 14 +- OSGKeyboardShared/Models/LLMRequest.swift | 5 +- .../Models/LocalASRCapabilities.swift | 11 +- .../Models/LocalASRModelCatalog.swift | 4 +- .../Models/PersonalDictionary.swift | 11 +- OSGKeyboardShared/Models/ProviderConfig.swift | 17 +- .../Models/SyncedAppSettingsV2.swift | 74 +- .../Models/TranslationLanguage.swift | 7 +- .../Models/TypingInputConfiguration.swift | 110 +- .../Models/VolcengineASRFields.swift | 4 +- .../Services/AIClipboardPrompt.swift | 69 ++ .../Services/AIHintKeywordCompressor.swift | 193 +++ .../Services/AIHintLocalCatalog.swift | 173 +++ OSGKeyboardShared/Services/AIHintPool.swift | 84 ++ OSGKeyboardShared/Services/AIHintStore.swift | 108 ++ .../Services/AIQuestionService.swift | 2 + .../Services/AnthropicLLMClient.swift | 7 + .../Services/AppGroupStore.swift | 9 +- .../Services/ClipboardHistoryPolicy.swift | 247 +++- .../Services/ClipboardHistoryStore.swift | 26 +- .../EditLastInputPromptComposer.swift | 15 +- .../Services/EditTransactionStore.swift | 6 + .../Services/FlowSessionBridge.swift | 1032 +---------------- .../FlowSessionBridge+Lifecycle.swift | 393 +++++++ .../FlowSessionBridge+Mailbox.swift | 167 +++ .../FlowSessionBridge+Transcription.swift | 152 +++ .../ICloudSync/SettingsCloudSync.swift | 29 +- .../Services/KeyboardState.swift | 67 +- OSGKeyboardShared/Services/Keychain.swift | 463 +++++++- OSGKeyboardShared/Services/LLMClient.swift | 47 +- OSGKeyboardShared/Services/LLMStreaming.swift | 24 +- .../Services/LocalASRModelInstallState.swift | 3 + .../Services/LocalASRModelManager.swift | 6 +- .../Services/PolishPromptComposer.swift | 11 +- .../Services/PolishingService.swift | 14 +- .../Services/ProviderModelService.swift | 2 + .../Services/ProviderToolRunnerState.swift | 109 +- .../Services/ResponsesAPILLMClient.swift | 11 +- .../Services/SearchAugmentedChatClient.swift | 11 +- .../Services/TranscriptPostProcessor.swift | 15 +- .../Services/WhatsNewDemoScenario.swift | 111 ++ .../RimePersonalDictionaryExporter.swift | 2 +- .../Typing/RimePinyinAnnotator.swift | 13 +- .../Typing/TypingSessionController.swift | 158 ++- .../Utilities/DictationTextComposer.swift | 42 +- OSGKeyboardShared/Utilities/FlowTrace.swift | 16 +- OSGKeyboardShared/Utilities/HanScript.swift | 19 + ...essiveDictationTranscriptAccumulator.swift | 4 +- .../Utilities/PromptXMLEscaping.swift | 17 + .../TranscriptLanguageDetector.swift | 11 +- .../TranscriptOverlapUtilities.swift | 28 + .../UtteranceTranscriptStitcher.swift | 30 +- OSGKeyboardShared/Views/FlowDebugPanel.swift | 169 --- OSGKeyboardShared/en.lproj/Shared.strings | 5 +- .../zh-Hans.lproj/Shared.strings | 5 +- OSGKeyboardTests/AIClipboardPromptTests.swift | 83 ++ OSGKeyboardTests/AIHintPoolTests.swift | 127 ++ .../ClipboardHistoryPolicyTests.swift | 102 +- .../ClipboardHistoryStoreTests.swift | 95 ++ OSGKeyboardTests/CloudASRTests.swift | 81 ++ .../EditLastInputPromptTests.swift | 17 +- OSGKeyboardTests/FlowSessionBridgeTests.swift | 789 ++++++++++++- OSGKeyboardTests/IntelligentPolishTests.swift | 7 +- OSGKeyboardTests/KeychainTests.swift | 138 +++ OSGKeyboardTests/LLMClientTests.swift | 18 +- .../PolishPromptComposerQuestionTests.swift | 11 +- ...eDictationTranscriptAccumulatorTests.swift | 9 + OSGKeyboardTests/SettingsCloudSyncTests.swift | 127 +- .../TranscriptLanguageDetectorTests.swift | 21 + .../UtteranceTranscriptStitcherTests.swift | 15 + README.en.md | 25 +- README.md | 20 +- Scripts/Package.resolved.lock | 294 +++++ Scripts/check_sensitive_logs.py | 245 ++++ Scripts/ensure-mlx-audio-swift.sh | 48 +- Scripts/generate-xcodeproj.sh | 9 + Scripts/install-xcodegen-ci.sh | 21 + Scripts/resolve_test_suite.py | 7 + Scripts/run-tests.sh | 1 + Scripts/sensitive_logs_safe.fixture.swift | 12 + Scripts/sensitive_logs_unsafe.fixture.swift | 25 + TYPEWHISPER_FLOW_MIGRATION_TRACKER.md | 270 ----- .../ProviderToolRequestCoordinatorTests.swift | 187 +++ Tests/suite-manifest.json | 31 +- docs/APPSTORE_METADATA.md | 424 +++---- docs/assets/whats-new/ai-keyboard-en.mp4 | Bin 0 -> 93097 bytes docs/assets/whats-new/ai-keyboard-zh.mp4 | Bin 0 -> 127161 bytes .../assets/whats-new/clipboard-history-en.mp4 | Bin 0 -> 101586 bytes .../assets/whats-new/clipboard-history-zh.mp4 | Bin 0 -> 80138 bytes docs/assets/whats-new/edit-last-input-en.mp4 | Bin 0 -> 113839 bytes docs/assets/whats-new/edit-last-input-zh.mp4 | Bin 0 -> 112052 bytes docs/assets/whats-new/edit-last-input.mp4 | Bin 59500 -> 0 bytes docs/index.html | 150 +-- docs/llms.txt | 11 +- docs/local-asr-architecture.md | 756 ++++-------- .../mac-qwen3-mlx-streaming-migration-plan.md | 589 ---------- docs/osgkeyboardversion.html | 106 +- docs/polish-style-packs-plan.md | 454 -------- docs/privacy.html | 20 +- docs/privacy/index.html | 42 +- project.yml | 40 +- 202 files changed, 10897 insertions(+), 5962 deletions(-) delete mode 100644 AUDIT_APPSTORE.md create mode 100644 OSGKeyboard/Services/AIHintRefreshService.swift create mode 100644 OSGKeyboard/Views/AIKeyboardDemoView.swift create mode 100644 OSGKeyboard/Views/ClipboardHistoryDemoView.swift create mode 100644 OSGKeyboard/Views/NotesHostDemoView.swift delete mode 100644 OSGKeyboard/Views/PreviewASRController.swift create mode 100644 OSGKeyboardExt/Services/EditHintScheduler.swift create mode 100644 OSGKeyboardExt/Services/WhatsNewDemoDriver.swift create mode 100644 OSGKeyboardExtTests/ClipboardSuggestionLifecycleTests.swift create mode 100644 OSGKeyboardExtTests/EditHintSchedulerTests.swift delete mode 100644 OSGKeyboardShared/DesignSystem/PolishStyleIconBadge.swift delete mode 100644 OSGKeyboardShared/DesignSystem/TranslationChip.swift create mode 100644 OSGKeyboardShared/Models/AIHintModels.swift create mode 100644 OSGKeyboardShared/Models/FlowSession/FlowAck.swift create mode 100644 OSGKeyboardShared/Models/FlowSession/FlowCommand.swift create mode 100644 OSGKeyboardShared/Models/FlowSession/FlowFieldContext.swift create mode 100644 OSGKeyboardShared/Models/FlowSession/FlowReadySnapshot.swift create mode 100644 OSGKeyboardShared/Models/FlowSession/FlowResult.swift create mode 100644 OSGKeyboardShared/Models/FlowSession/FlowStartTransaction.swift create mode 100644 OSGKeyboardShared/Models/FlowSession/FlowTranscriptionError.swift create mode 100644 OSGKeyboardShared/Services/AIClipboardPrompt.swift create mode 100644 OSGKeyboardShared/Services/AIHintKeywordCompressor.swift create mode 100644 OSGKeyboardShared/Services/AIHintLocalCatalog.swift create mode 100644 OSGKeyboardShared/Services/AIHintPool.swift create mode 100644 OSGKeyboardShared/Services/AIHintStore.swift create mode 100644 OSGKeyboardShared/Services/FlowSessionBridge/FlowSessionBridge+Lifecycle.swift create mode 100644 OSGKeyboardShared/Services/FlowSessionBridge/FlowSessionBridge+Mailbox.swift create mode 100644 OSGKeyboardShared/Services/FlowSessionBridge/FlowSessionBridge+Transcription.swift create mode 100644 OSGKeyboardShared/Services/WhatsNewDemoScenario.swift create mode 100644 OSGKeyboardShared/Utilities/HanScript.swift create mode 100644 OSGKeyboardShared/Utilities/PromptXMLEscaping.swift create mode 100644 OSGKeyboardShared/Utilities/TranscriptOverlapUtilities.swift delete mode 100644 OSGKeyboardShared/Views/FlowDebugPanel.swift create mode 100644 OSGKeyboardTests/AIClipboardPromptTests.swift create mode 100644 OSGKeyboardTests/AIHintPoolTests.swift create mode 100644 OSGKeyboardTests/ClipboardHistoryStoreTests.swift create mode 100644 Scripts/Package.resolved.lock create mode 100644 Scripts/check_sensitive_logs.py create mode 100755 Scripts/install-xcodegen-ci.sh create mode 100644 Scripts/sensitive_logs_safe.fixture.swift create mode 100644 Scripts/sensitive_logs_unsafe.fixture.swift delete mode 100644 TYPEWHISPER_FLOW_MIGRATION_TRACKER.md create mode 100644 Tests/ProviderToolRequestCoordinatorTests.swift create mode 100644 docs/assets/whats-new/ai-keyboard-en.mp4 create mode 100644 docs/assets/whats-new/ai-keyboard-zh.mp4 create mode 100644 docs/assets/whats-new/clipboard-history-en.mp4 create mode 100644 docs/assets/whats-new/clipboard-history-zh.mp4 create mode 100644 docs/assets/whats-new/edit-last-input-en.mp4 create mode 100644 docs/assets/whats-new/edit-last-input-zh.mp4 delete mode 100644 docs/assets/whats-new/edit-last-input.mp4 delete mode 100644 docs/mac-qwen3-mlx-streaming-migration-plan.md delete mode 100644 docs/polish-style-packs-plan.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1756eb8..8587ceb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -32,6 +32,10 @@ What you expected to happen. Paste the relevant Console.app output filtered to `OSGKeyboard`. Wrap in triple backticks. +Before uploading, remove DEBUG transcript/prompt/clipboard content, API keys, +authorization headers, request/response bodies, and any other credentials or +personal data. Do not attach unreviewed raw DEBUG logs. + ``` ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4cc887..bb420fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,93 +2,106 @@ name: CI on: push: - branches: [0.1, 0.2, develop] + branches: [main] pull_request: - branches: [0.1, 0.2, develop] + branches: [main] workflow_dispatch: +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - # ========================================================= - # SwiftLint (light config — fails only on severe issues) - # ========================================================= + validate: + name: Validate manifests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Validate test suite manifest + run: ./Scripts/run-tests.sh validate + - name: Check sensitive logs + run: | + python3 Scripts/check_sensitive_logs.py --self-test + python3 Scripts/check_sensitive_logs.py + lint: name: SwiftLint - # macos-14 runner ships Xcode 16.x (default 16.4 in 2026). We pin to - # 16.4 instead of hard-coding a path because the runner image is - # refreshed regularly and `/Applications/Xcode_16.0.app` is no - # longer pre-installed — that's the bug this PR fixes. - runs-on: macos-14 + runs-on: macos-26 steps: - - uses: actions/checkout@v4 - - name: Select Xcode 16.4 - run: sudo xcode-select -s /Applications/Xcode_16.4.app - - name: Install SwiftLint - run: brew install swiftlint - - name: Run SwiftLint - # Run twice: --quiet to fail on errors only, --strict to fail - # on warnings too. The current .swiftlint.yml is intentionally - # permissive (line_length, file_length, function_body_length, - # type_body_length are all disabled) so this should pass on - # the audit/appstore-prep branch. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Verify toolchain run: | - swiftlint lint --quiet - swiftlint lint --quiet --strict + set -euo pipefail + sudo xcode-select -s /Applications/Xcode_26.6.app + xcodebuild -version + test "$(xcodebuild -version | awk 'NR == 1 { print $2 }')" = "26.6" + test "$(xcrun --sdk iphonesimulator --show-sdk-version | cut -d. -f1)" = "26" + command -v swiftlint >/dev/null || brew install swiftlint + test "$(swiftlint version)" = "0.65.0" + - name: Run SwiftLint + run: swiftlint lint --quiet --strict - # ========================================================= - # Build the main app + keyboard extension - # ========================================================= - build: - name: Build (${{ matrix.destination }}) - needs: lint - runs-on: macos-14 - strategy: - fail-fast: false - matrix: - destination: - - generic/platform=iOS Simulator + ios: + name: iOS / Extension + needs: [validate, lint] + runs-on: macos-26 steps: - - uses: actions/checkout@v4 - - name: Select Xcode 16.4 - run: sudo xcode-select -s /Applications/Xcode_16.4.app - - name: Install XcodeGen - run: brew install xcodegen + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Verify toolchain + run: | + set -euo pipefail + sudo xcode-select -s /Applications/Xcode_26.6.app + xcodebuild -version + test "$(xcodebuild -version | awk 'NR == 1 { print $2 }')" = "26.6" + test "$(xcrun --sdk iphonesimulator --show-sdk-version | cut -d. -f1)" = "26" + - name: Install pinned XcodeGen + run: ./Scripts/install-xcodegen-ci.sh - name: Generate project run: ./Scripts/generate-xcodeproj.sh - - name: Build + - name: Run PR test preset + run: SKIP_GENERATE=1 ./Scripts/run-tests.sh pr + - name: Build Release run: | - set -o pipefail + set -euo pipefail xcodebuild \ -project OSGKeyboard.xcodeproj \ -scheme OSGKeyboard \ - -destination "${{ matrix.destination }}" \ - -configuration Debug \ - build \ - | xcpretty + -destination 'generic/platform=iOS Simulator' \ + -configuration Release \ + -onlyUsePackageVersionsFromResolvedFile \ + CODE_SIGNING_ALLOWED=NO \ + build - # ========================================================= - # Unit tests - # ========================================================= - test: - name: Unit tests - needs: lint - runs-on: macos-14 + mac: + name: macOS + needs: [validate, lint] + runs-on: macos-26 steps: - - uses: actions/checkout@v4 - - name: Select Xcode 16.4 - run: sudo xcode-select -s /Applications/Xcode_16.4.app - - name: Install XcodeGen - run: brew install xcodegen + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Verify toolchain + run: | + set -euo pipefail + sudo xcode-select -s /Applications/Xcode_26.6.app + xcodebuild -version + test "$(xcodebuild -version | awk 'NR == 1 { print $2 }')" = "26.6" + test "$(uname -m)" = "arm64" + - name: Install pinned XcodeGen + run: ./Scripts/install-xcodegen-ci.sh - name: Generate project run: ./Scripts/generate-xcodeproj.sh - - name: Validate test suite manifest - run: ./Scripts/run-tests.sh validate - - name: Run tests (pr preset) - # pr = critical-path groups including OSGKeyboardExtTests. - # See Tests/suite-manifest.json and docs/TESTING.md. + - name: Run macOS tests + run: SKIP_GENERATE=1 ./Scripts/run-tests.sh mac + - name: Build Release run: | - set -o pipefail - SKIP_GENERATE=1 ./Scripts/run-tests.sh pr + set -euo pipefail + xcodebuild \ + -project OSGKeyboard.xcodeproj \ + -scheme OSGKeyboardMac \ + -destination 'platform=macOS' \ + -configuration Release \ + -onlyUsePackageVersionsFromResolvedFile \ + CODE_SIGNING_ALLOWED=NO \ + build diff --git a/.swiftlint.yml b/.swiftlint.yml index 757abeb..dcae65d 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -24,7 +24,13 @@ included: - OSGKeyboard - OSGKeyboardExt - OSGKeyboardShared + - OSGKeyboardHostSupport + - OSGKeyboardMac - OSGKeyboardTests + - OSGKeyboardExtTests + - OSGKeyboardMacTests + - OSGKeyboardUITests + - Tests excluded: - build diff --git a/AGENTS.md b/AGENTS.md index d7ee915..293dc00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,27 +1,33 @@ # AGENTS.md +## License boundary + +OSGKeyboard is **source available, not open source**. `LICENSE` permits personal, +non-commercial local use and forbids unauthorized redistribution or public derivative +versions. Do not describe the project as MIT-licensed, open source, or freely forkable. + ## Versioning and releases OSGKeyboard uses **Conventional Commits** as the single source of truth for version bumps and `CHANGELOG.md` entries. Agents must follow this section whenever cutting a release or writing commit messages that will ship to users. -### Version format (0.x stage) +### Version format -While `MARKETING_VERSION` is `0.x.y`, treat the project as **pre-1.0**: +The current source-of-truth version is **1.7.0 (build 65)**. Releases use stable SemVer: | Field | File | Rule | |-------|------|------| -| Marketing version | `project.yml` → `MARKETING_VERSION` | `0.MINOR.PATCH` (SemVer) | +| Marketing version | `project.yml` → `MARKETING_VERSION` | `MAJOR.MINOR.PATCH` (SemVer) | | Build number | `project.yml` → `CURRENT_PROJECT_VERSION` | Monotonic integer; **+1 on every release cut**, never decrease | **Bump rules** (evaluate all commits since the last tagged/released version; take the **highest** bump): | Commit prefix | Version bump | Example | |---------------|--------------|---------| -| `feat:` | **MINOR** + reset PATCH → `0` | `0.3.6` → `0.4.0` | -| `fix:`, `perf:` (user-visible) | **PATCH** | `0.3.6` → `0.3.7` | -| `feat!:` or footer `BREAKING CHANGE:` | **MINOR** (pre-1.0; reserve `1.0.0` for a deliberate GA) | `0.3.6` → `0.4.0` | +| `feat:` | **MINOR** + reset PATCH → `0` | `1.7.0` → `1.8.0` | +| `fix:`, `perf:` (user-visible) | **PATCH** | `1.7.0` → `1.7.1` | +| `feat!:` or footer `BREAKING CHANGE:` | **MAJOR** | `1.7.0` → `2.0.0` | | `refactor:`, `style:`, `docs:`, `test:`, `chore:`, `ci:` | **no bump by itself** | group with user-facing commits or skip release | Pragmatic overrides (experienced-maintainer judgment, still objective): @@ -71,13 +77,13 @@ chore(lexicon): add offline SFCustomLanguageModelData export scripts **Example entry:** ```markdown -## [0.4.0] - 2026-07-06 +## [1.8.0] - 2026-09-01 ### Added - **Cursor navigation**: drag pad on the keyboard for precise caret movement. / **光标导航**:键盘拖动手势区,精确移动光标。 ### Fixed -- **API key handling**: move DeepSeek key into gitignored local file. / **API 密钥**:将 DeepSeek 密钥移至 gitignore 的本地文件。 +- **API key handling**: keep user-owned provider keys in Keychain. / **API 密钥**:将用户自备的服务商密钥保存在 Keychain。 ``` ### Release checklist (agent) @@ -87,7 +93,7 @@ When the user asks to release or bump version: 1. `git log` from last release tag/commit → classify commits → pick bump level. 2. Update `CHANGELOG.md` (`[Unreleased]` → `[X.Y.Z] - date`, bilingual bullets). 3. Update `project.yml`: - - `MARKETING_VERSION` → new `0.x.y` + - `MARKETING_VERSION` → new SemVer version - `CURRENT_PROJECT_VERSION` → previous build **+ 1** 4. Commit: `chore(release): bump version to X.Y.Z (build N)` — or include in the release PR. 5. Do **not** bump version for work that stays on a feature branch until it merges to `main`. @@ -106,20 +112,22 @@ When the user asks to release or bump version: ## Cursor Cloud specific instructions -### Platform reality: this is an iOS-only project on a Linux VM +### Platform reality: this is an Apple-platform project on a Linux VM -OSGKeyboard is a native **iOS 18+** app (main app + custom keyboard extension + shared -framework, all Swift 6 / SwiftUI). The Cursor Cloud VM is **Linux x86_64**. iOS development -is fundamentally macOS-only, so the following **cannot run in this environment**: +OSGKeyboard contains a native **iOS/iPadOS 26+** app and keyboard extension plus a +native **macOS 15+** menu-bar app (Swift 6 / SwiftUI). The iOS host links +`OSGKeyboardShared` and the host-only `OSGKeyboardHostSupport`; the Mac target reuses +shared/host-support sources and links MLX Audio for Qwen3 streaming ASR. The Cursor Cloud VM +is **Linux x86_64**. Apple-platform development is macOS-only, so the following **cannot run +in this environment**: - **Build** — needs `xcodebuild` + the iOS SDK (Xcode, macOS only). - **Run** — needs the iOS Simulator or a physical iPhone (macOS only). -- **Tests** — `OSGKeyboardTests` / `OSGKeyboardExtTests` run via `xcodebuild test` against the - iOS Simulator (macOS only). +- **Tests** — iOS/extension tests require an iOS Simulator; `OSGKeyboardMacTests` requires macOS. Nearly every source file imports iOS-only frameworks (`SwiftUI`, `UIKit`, `AVFoundation`, `Speech`, `Combine`), so there is no meaningful subset that compiles with Swift-for-Linux. -Do **not** attempt to build/run/test on the Linux VM — escalate to a macOS host with Xcode 16+ +Do **not** attempt to build/run/test on the Linux VM — escalate to a macOS host with Xcode 26+ (see `README.md` / `CONTRIBUTING.md` for the `xcodegen generate` + `xcodebuild` flow). ### What *does* work on Linux: SwiftLint @@ -144,9 +152,3 @@ Caveats: The `.xcodeproj` is **gitignored**; `project.yml` (XcodeGen) is the source of truth. On macOS run `xcodegen generate` before any `xcodebuild`. XcodeGen is macOS-oriented and is not installed on the Linux VM. - -### CI note - -`.github/workflows/ci.yml` runs on `macos-14` and currently fails at the `xcode-select -s -/Applications/Xcode_16.0.app` step because that Xcode version is absent from GitHub's current -`macos-14` image — this is a CI runner-image issue, unrelated to the code or this Linux setup. diff --git a/AUDIT_APPSTORE.md b/AUDIT_APPSTORE.md deleted file mode 100644 index 758aafa..0000000 --- a/AUDIT_APPSTORE.md +++ /dev/null @@ -1,313 +0,0 @@ -# App Store 上架前审计报告 — v0.1.2 - -**项目**: hkgood/OSGKeyboard -**基线**: main @ `642c97c` -**审计日期**: 2026-06-20 -**审计员**: 中书省 (zhongshu subagent) -**目标**: 满足 Apple App Store Review Guidelines + iOS 26 + Custom Keyboard Extension 审核要点 - ---- - -## TL;DR - -仓库代码质量整体良好(iOS 26 only / Swift 6 / 零依赖 / 隐私 manifest / onboarding 全套 / Flow 模型完整)。**阻塞上架的 P0 项集中在 CI 运维和 App Store Connect 元数据/资源**,**没有发现代码层面的功能缺陷或安全漏洞**。修完 6 项 P0 即可提交;P1/P2 建议在后续小版本里迭代。 - -### 优先级分布 - -| 优先级 | 数量 | 状态 | -|--------|------|------| -| **P0**(必须修,阻塞上架) | 6 | 本审计已全部落地 | -| **P1**(强烈建议) | 5 | 留给 v0.1.3 后续 | -| **P2**(可选优化) | 4 | 长期 roadmap | - ---- - -## P0 — 阻塞上架 - -### P0-1. CI workflow 引用已不存在的 Xcode 版本 ✅ 已修 - -**现状**: `.github/workflows/ci.yml` 三个 job(lint / build / test)都写死 `/Applications/Xcode_16.0.app`,但 GitHub Actions `macos-14` runner 默认已无此版本(Xcode 16.0 是 macos-14 runner 早期预装版本,已被替换为 Xcode 16.x 最新 stable)。 - -**影响**: -- lint job: `sudo xcode-select -s /Applications/Xcode_16.0.app` 直接失败 -- build job: matrix 写死 `xcode: "16.0"`,同样失败 -- test job: 同样失败 -- → **CI 一直红**,合并任何 PR 都看不出真错 - -**修复**: -- 升级 `macos-14` → `macos-15`(GitHub-hosted runner 自带 Xcode 26.x) -- 删除 matrix 中 `xcode: "16.0"` 字段,改为 `macos-15` 自带的 Xcode -- 改用 `sudo xcode-select -s /Applications/Xcode_16.4.app`(macos-15 预装的版本号;26 系列需要 macos-26 runner 上线后才稳定;先锁 16.4 是 CI 可复现的稳妥选择,并增加 `select-xcode` 步骤用 `xcversion` 或 `agvtool` 探测) - -> **实际修复策略**: macos-14 保留但改用 `maxim-lobanov/setup-xcode@v1` action 自动选最高稳定版,或者升级到 `macos-15`(更直接,已实施)。 - -**落地 commit**: 见 commit `chore(ci): pin Xcode 16.4 + macos-15 runner` - ---- - -### P0-2. release 配置残留 `print()` 与 `NSLog` ✅ 已评估并保留 #if DEBUG 包裹 - -**摸底结果**(与太子原话略有差异 — 已核实): - -| 位置 | 上下文 | 是否泄漏到 release | -|---|---|---| -| `OSGKeyboard/Services/FlowSessionManager.swift:465` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardShared/Models/ProviderConfig.swift:49` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardShared/Services/LLMClient.swift:103` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardShared/Services/LiveDictationController.swift:503` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardShared/Services/Keychain.swift:70` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardShared/Services/ASRService.swift:236` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardExt/KeyboardViewController.swift:697` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardExt/Services/AppGroupPersistor.swift:50` | `#if DEBUG ... print ... #endif` | ❌ 不泄漏 | -| `OSGKeyboardShared/Constants/AppGroup.swift:60` | `NSLog(...)` 写在 release fallback 路径 | ⚠️ **真泄漏** | - -**结论**: 8 个 `print()` 全部由 `#if DEBUG` 包裹,**Release 构建不会编译这些语句**。唯一 release-可见的是 `AppGroup.swift:60` 的 `NSLog`。 - -**App Store 审核对 `print` 的态度**: 严格说不查日志语句本身,App Review 只关心功能/隐私/UI。但苹果审核员在控制台中看到的 NSLog 输出,会被列为"non-issues"或"建议优化",**不会因此被拒**。但我们仍做 P1 优化(替换为 `os.Logger`,可在 release 下也输出但经 `OSLog` 隐私控制)。 - -**真 P0 问题**: `AppGroup.swift:60` 的 `NSLog` 会出现在 release 设备日志里、且文案暴露内部细节。已建议:换成 `os.Logger`,文案更克制。 - ---- - -### P0-3. Legacy 路径评估 ✅ 实际只 1 个文件可删 - -太子原话: "删除 `LiveDictationController / DictationBridge / AudioCaptureService`" - -**核实结果**(grep 全仓): - -| 文件 | 引用者 | 是否真 dead | 结论 | -|---|---|---|---| -| `OSGKeyboardShared/Services/AudioCaptureService.swift` | 仅 .swift 注释、.swift 源本身 | **✅ 真 dead** | **可删** | -| `OSGKeyboardShared/Services/DictationBridge.swift` | `KeyboardViewController.swift:594,601,639`(**实际消费 pending transcript**) | ❌ 在用 | **不可删** | -| `OSGKeyboardShared/Services/LiveDictationController.swift` | `PreviewASRController.swift`(typealias)、`DictationCaptureView.swift`、`KeyboardPreviewSheet.swift`、`PreviewASRControllerStateTests.swift` | ❌ 在用 | **不可删** | - -**结论**: "Legacy" 是相对概念。DictationBridge 和 LiveDictationController 实际上仍然在用,只是角色从"主路径"变成了"调试预览 + 一次性 handoff"。 - -**修复**: -- ✅ 删除 `AudioCaptureService.swift`(真正无人调用的死代码) -- ❌ 保留 `DictationBridge.swift`(KeyboardViewController 仍消费) -- ❌ 保留 `LiveDictationController.swift`(作为 PreviewASRController 仍在用) -- 文档化:在 `LiveDictationController.swift` 文件头补一段说明"虽然叫 LiveDictation 但已不是主路径,主路径是 FlowSessionManager/FlowContinuousCapture;本类服务于键盘内嵌预览场景"。 - -**落地 commit**: 见 commit `chore: remove dead AudioCaptureService and clarify LiveDictationController scope` - ---- - -### P0-4. `ITSAppUsesNonExemptEncryption=NO` 缺失 ✅ 已加 - -**现状**: `OSGKeyboard/Info.plist` 没有 `ITSAppUsesNonExemptEncryption` 键。 - -**影响**: App Store 上传时弹出"App Encryption"问卷,需要每次手填一遍"使用 HTTPS,不含非豁免加密"。最简方案是显式声明 `NO`,直接跳过问卷。 - -**修复**: 在主 App `Info.plist` 加 `ITSAppUsesNonExemptEncryption`。键盘扩展不需要此键(不是 app)。 - -**落地 commit**: 见 commit `chore: declare ITSAppUsesNonExemptEncryption=NO for App Store upload` - ---- - -### P0-5. App Store 截图缺失 ✅ 已生成占位 + 元数据 - -**现状**: -- `docs/assets/` 只有 `app-icon.png` (1024×1024) — 不是 App Store 截图 -- 缺 6.7" (iPhone 17 Pro Max) / 6.1" (iPhone 17 Pro) / 5.5" (iPhone 8 Plus) 三套 -- 缺 App Store Connect 元数据(description、promo text、keywords、release notes、support URL、marketing URL、privacy policy URL) - -**Apple 实际要求(2026)**: -- **6.7"** (1290×2796) — 必需 -- **6.1"** (1179×2556) — 必需(iPhone 17 Pro) -- 5.5" 已被苹果官方文档降级为"可选"(iPhone 8 Plus 等已停产机型) -- iPad 截图 — **现已必需**:项目自 TARGETED_DEVICE_FAMILY "1,2" 起支持 iPad,App Store Connect 要求提供 13″ iPad Pro 截图套组(本条为后续更新覆盖原「iPhone only 无需提供」的结论) -- 每套至少 3 张、最多 10 张 - -**本审计交付**: -- `docs/screenshots/` 目录 -- `docs/screenshots/README.md` — 截图规范说明 -- `docs/APPSTORE_METADATA.md` — 完整 App Store Connect 文案(含 description、promo text、keywords、release notes、whats new、support URL、marketing URL、privacy policy URL) -- 6.7" / 6.1" 各 5 张的占位 — **实际占位 PNG(用脚本生成纯色 + 文字标题的 1290×2796 / 1179×2556 PNG)**。真实 UI 截图需要人工在 Xcode Simulator 跑出再替换。 - -**落地 commit**: 见 commit `docs: App Store screenshots placeholder + APPSTORE_METADATA.md` - -> ⚠️ **真人事项**: 真实截图(带 UI 实际效果)需要太子或皇上在 Xcode Simulator (iPhone 17 Pro / Pro Max) 跑出 5+5 张并替换。已在 APPSTORE_METADATA.md 末尾给出截图拍摄脚本(xcrun simctl io booted screenshot ...)。 - ---- - -### P0-6. `v0.1.2` git tag 缺失 ✅ 已加 - -**现状**: `git tag -l` 空。 - -**影响**: App Store Connect 上传二进制时**不强制要 tag**,但 App Store Connect "Version" 字段对 tag 命名有强提示作用,且 CHANGELOG 里有 `## [0.1.2] - In Progress`,没 tag 看着像没发布。 - -**修复**: 创建 annotated tag `v0.1.2`,信息参考 CHANGELOG 的 0.1.2 段。 - -**落地 commit**: 见 commit `chore: tag v0.1.2 (App Store submission prep)` - -> ⚠️ 推送 tag = `git push origin v0.1.2`,需要 token。 - ---- - -## P1 — 强烈建议(v0.1.3 候选) - -### P1-1. SwiftLint strict 模式未启用 - -**摸底**: 当前 `.swiftlint.yml` 已 `disabled_rules` 去掉 `line_length` / `file_length` / `function_body_length` / `type_body_length` / `cyclomatic_complexity` 等大量 rule。**严格模式 (`--strict`) 把 warnings 升级为 errors**,如果启用会爆 134 warnings。 - -**未启用原因**: CI 第 47 行 `swiftlint lint --quiet --strict` 在 workflow 里**实际跑**(不是关掉),但因为没有 strict 模式触发条件所以通过。复现:在 strict 模式下,134 warnings → errors。 - -**建议修复**: -- 保持 `line_length` / `file_length` 禁用(不然误伤大) -- 启用 `sorted_imports` / `explicit_init` / `empty_count` / `first_where` 等已 opt_in 但实际报错的 rule -- 给 5 个超 400 行的文件(`SettingsView.swift 427行`、`OnboardingView.swift 581行`、`FlowSessionManager.swift 468行`、`KeyboardViewController.swift 700行`、`KeyboardRootView.swift 520行`、`LiveDictationController.swift 506行`)做拆分 task -- 短期:CI 不改 strict 模式,但开启 strict 后通过的 patch 进 v0.1.3 - -### P1-2. 测试覆盖率低 - -**现状**: 7 个测试文件 / 873 行 / 7 个 test class,未覆盖: -- `FlowSessionManager`(核心)— 0 测试 -- `FlowContinuousCapture`(核心)— 0 测试 -- `PolishingService` — 0 测试 -- `KeyboardViewController` — 0 测试 -- `ASRService` (SpeechAnalyzer 路径) — 仅 1 个 state 测试 - -**建议**: 至少补 1) `FlowSessionManager` start/stop 状态机 2) `PolishingService` 三种模式(off/transcribe/polish)3) `KeyboardViewController` 的 partial transcript 注入逻辑。 - -### P1-3. README 截图缺失 - -**现状**: `README.md` 没有截图段,全是 emoji + 链接。 - -**建议**: 加 `docs/screenshots/` 实际 UI 截图,README 顶部加 `` 段。 - -### P1-4. 键盘 UI 引导动图缺失 - -**现状**: 首次启用键盘需要 3 步(设置→键盘→添加→允许 Full Access),onboarding 是文字步骤,没动图。 - -**建议**: 用 QuickTime 录 30 秒 GIF,嵌到 `OSGKeyboard/OnboardingView.swift` 第 3 步下方。 - -### P1-5. 用 `os.Logger` 替换 `print` / `NSLog` - -**现状**: 见 P0-2。`AppGroup.swift:60` 的 `NSLog` 是唯一 release 可见日志。 - -**建议**: 引入 `OSGLog` enum(封装 `os.Logger`),全局替换。这样 release 下仍能在 Console.app / `log stream` 看到带 subsystem 的日志,但被 `OSLog` 隐私协议控制(不会把 API key 等敏感字段写出去)。 - ---- - -## P2 — 可选长期 - -### P2-1. Apple Foundation Models 本地润色 - -iOS 26 提供 `FoundationModels` 框架(设备端 LLM),可用作"polish 模式"的可选 backend,不消耗用户 API key、不外发。 - -**预估工作量**: 2-3 天(适配接口、prompt 工程、UI toggle、隐私政策更新)。 - -### P2-2. 新增 Anthropic / Gemini provider - -当前 6 个 LLM provider:OpenAI / DeepSeek / Qwen / Apple / Custom / Moonshot / Zhipu(实际 7 个)。Anthropic Claude / Google Gemini 都还没原生 provider。Claude 用 Anthropic Messages API(非 OpenAI 兼容),Gemini 用 Gemini API(也非 OpenAI 兼容)。 - -**预估工作量**: 4-5 天(含 token 估算、流式响应、错误处理、UI)。 - -### P2-3. 性能 profiling - -键盘 extension 内存预算 60 MB。当前 `FlowContinuousCapture` + `ASRService` + 音频 buffer 三者驻留。建议跑 Instruments → Allocations / Time Profiler,验证 peak memory < 40 MB。 - -### P2-4. App Store 内置评分请求 - -`SKStoreReviewController.requestReview(in:)` 在 Settings 加 "Rate OSGKeyboard" 按钮,不打断主流程。 - ---- - -## 跨领域观察(不在 P0/P1/P2 单项里的杂项) - -1. **键盘扩展无 `CFBundleURLTypes`(太子列为 P0-12)** — **核实后非问题**。键盘扩展**不能**通过 URL scheme 启动 host app;host app 的 `CFBundleURLTypes` 是为了从 Settings/外链唤起 App,与扩展无关。**已确认无需修**。 - -2. **App Group fallback 行为不一致(太子列为 P0-13)** — **实际是设计意图**。`AppGroup.swift:51-67` 注释明确说明:DEBUG `fatalError` 是为了"硬崩防止 desync 漏掉 bug";release `NSLog + 软 fallback` 是为了"App 不至于直接挂掉"。审核员不会因为 release 软 fallback 而拒。**已确认无需改 P0,归 P1-5 一起处理**。 - -3. **`FlowContinuousCapture` 依赖 `.playAndRecord` audio session** — **实际已处理**。`OSGKeyboardExt` 用的是 host App 的 `FlowSessionManager`(在主 App target 而非 ext),不冲突。键盘扩展内 AudioCaptureService 是死代码(P0-3 已删)。**已确认无问题**。 - -4. **`NSSupportsLiveTextFrequentUpdates`(太子列为 P0-11)** — **核实**。Apple 在 iOS 17 引入 `NSSupportsLiveText` / iOS 18 加 `NSSupportsLiveTextFrequentUpdates`,但这是**宿主 app 用于处理 Live Text 数据流**(相机/照片),与 custom keyboard extension 无关。键盘扩展加这个 key 不会被苹果拒,但也没用。**已确认无需加**。 - -5. **ASRService 仍引用 iOS 26 之前的 SFSpeechRecognizer fallback(太子列为 P0-14)** — **核实后非问题**。ASRService 的 ASR 后端**只有 SpeechAnalyzer**(iOS 26 only),但 `SettingsView` 和 `AppPermissions` 用 `SFSpeechRecognizer.supportedLocales()` / `authorizationStatus()` 等**元数据 API**(这些 API 在 iOS 26 仍存在且无 deprecation),完全合法。**已确认无需修**。 - -6. **测试运行 destination 是 iPhone 17(CI `ci.yml:79`)** — **核实**。Apple 2026 年 Simulator 列表中 iPhone 17 是 stable;本机是 iPhone 17 Pro。CI 用 `iPhone 17`(无 Pro),是 Apple 公开的标准 simulator 名,应该能跑。**已确认 OK**。 - -7. **Privacy manifest 完整度** — `OSGKeyboard/PrivacyInfo.xcprivacy` 和 `OSGKeyboardExt/PrivacyInfo.xcprivacy` 都声明了 `NSPrivacyAccessedAPICategoryUserDefaults` (CA92.1) 原因。**没有用磁盘 API / 系统启动时间 API**(除了 `containerURL`,它本身不需要声明 reason),所以不必加其他 API reason。**已确认合规**。 - -8. **AppIcon.appiconset 1024x1024 PNG** — ✅ 已确认 `Group 24.png` 1024×1024,App Store 上传最低要求。 - -9. **CHANGELOG 0.1.0 已有** — `## [0.1.0] - 2026-06-17`,v0.1.1 缺失(CHANGELOG 提到"v0.1.1 polish" 但没有 `## [0.1.1]` 段)。**归 P1**,下次发版前补。 - ---- - -## 落地操作(已 commit 到 `audit/appstore-prep` 分支) - -| Commit | 内容 | -|---|---| -| `chore(ci): pin Xcode 16.4 + macos-15 runner` | P0-1 | -| `chore(ci): enable strict lint with curated disabled rules` | P1-1 起步 | -| `chore: remove dead AudioCaptureService and clarify LiveDictationController scope` | P0-3 | -| `chore: declare ITSAppUsesNonExemptEncryption=NO for App Store upload` | P0-4 | -| `docs: App Store screenshots placeholder + APPSTORE_METADATA.md` | P0-5 | -| `chore: tag v0.1.2 (App Store submission prep)` | P0-6 | - -详细 diff 见 git log。 - ---- - -## 真人/外部事项(需皇上或太子手动处理) - -1. **App Store Connect 上传** — 需人工在 Xcode → Product → Archive → Distribute App,需要 Apple Developer Team 登录 -2. **真实截图替换** — `docs/screenshots/*.png` 当前是脚本生成占位;需在 iPhone 17 Pro / Pro Max 模拟器跑出真实 UI 截图 -3. **App Store Connect 元数据填写** — `docs/APPSTORE_METADATA.md` 含全部文案,需复制到 App Store Connect 后台 -4. **Privacy Nutrition Labels** — App Store Connect 隐私标签:勾选 "Data Not Collected",并在 "Health & Fitness / Data Not Used for Tracking" 等子项确认 -5. **Encryption 出口合规** — 加了 `ITSAppUsesNonExemptEncryption=NO` 后问卷会跳过;如出现,需要在 ITC 提交 self-classification report -6. **GitHub Release** — `git push origin v0.1.2` 后需到 GitHub Releases 页面写 release notes -7. **TestFlight** — 上传后建议先 internal TestFlight 跑一遍,确认 Flow / Onboarding / 键盘添加流程 - ---- - -## 附录 A:本地烟雾测试结果 - -### A.1 xcodebuild build (iPhone 17 Pro / iOS Simulator) - -``` -xcodebuild -project OSGKeyboard.xcodeproj -scheme OSGKeyboard \ - -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ - -configuration Debug -derivedDataPath ./.derivedData build \ - CODE_SIGNING_ALLOWED=NO - -** BUILD SUCCEEDED ** -``` - -产出 `.derivedData/Build/Products/Debug-iphonesimulator/OSGKeyboard.app/` -包含: -- `OSGKeyboard` (主 App, 39 KB 可执行) -- `OSGKeyboard.debug.dylib` (4.3 MB) -- `PlugIns/OSGKeyboardExt.appex` (键盘扩展) -- `Frameworks/OSGKeyboardShared.framework` (共享 framework) -- `Info.plist` 内 `ITSAppUsesNonExemptEncryption=0` 已生效 -- 资源 Assets.car / MaterialIcons-Regular.ttf / en.lproj / zh-Hans.lproj - -### A.2 xcodebuild test (iPhone 17 Pro / iOS Simulator) - -``` -xcodebuild test ... -only-testing:OSGKeyboardTests CODE_SIGNING_ALLOWED=NO - -Executed 29 tests, with 11 failures (0 unexpected) -``` - -**通过 20/29** — ASRConversion / DictationBridge / FlowSessionBridge / PreviewASRControllerState 全部 PASS。 - -**失败 9/29** — 全部为 `errSecMissingEntitlement (-34018)`: -- `KeychainTests` (8 tests) — 缺 `keychain-access-groups` entitlement 注入 -- `LLMClientTests` (3 tests) — 间接依赖 Keychain - -**根因**: `CODE_SIGNING_ALLOWED=NO` 时 xcodebuild 不注入 entitlement,但 Keychain tests 调用了 `kSecAttrAccessGroup` 共享 keychain。这是**测试工程问题**,不是代码 bug,**不影响上架**。 - -**修法(不本审计范围内,仅记录)**: -1. 在 CI 中允许临时 ad-hoc signing(`CODE_SIGN_IDENTITY=-`),保留 entitlement -2. 或在 `KeychainTests` 中使用 `URL(fileURLWithPath:)` mock Keychain,绕过真 keychain query -3. 或在 `OSGKeyboardTests/Info.plist` 同样加 `keychain-access-groups` 数组 - -推荐方案 1(最小改动;CI 加 `CODE_SIGN_IDENTITY: "-"` + `CODE_SIGN_STYLE: Manual`)。 - ---- - -**本审计到此结束。中书省。** diff --git a/CHANGELOG.md b/CHANGELOG.md index 2751967..8212011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,23 +8,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- **Clipboard history**: optional keyboard clipboard history (latest 15 plain-text items, App Group local), top-bar clipboard entry, history panel with whitespace token chips, and a suggestion strip across voice / typing / AI when enabled. / **剪贴板历史**:可选的键盘剪贴板历史(最近 15 条纯文本、App Group 本地),顶栏剪贴板入口、带空白分词芯片的历史面板,以及开启后在语音 / 打字 / AI 面显示的建议条。 +- **AI idle hint carousel**: AI mode empty state rotates one-line suggestions (local evergreen + optional remote hot topics); tap sends the card prompt to the LLM without the mic. Coexists with the clipboard suggestion strip in the top bar. / **AI 空闲建议轮播**:AI 模式空状态轮播单行建议(内置常驻 + 可选远程热点);点按即跳过麦克风把卡片 prompt 发给模型。与顶栏剪贴板建议条并存。 +- **Hint feed refresh**: main app silently fetches `https://key.osglab.com/hints` about every 12 hours, compresses titles with the user’s polish LLM, and writes App Group ready packs for the keyboard. / **建议源刷新**:主 App 约每 12 小时静默拉取 `https://key.osglab.com/hints`,用用户润色 LLM 压缩标题,写入 App Group 供键盘只读。 +- **Clipboard history**: optional keyboard clipboard history (latest 15 plain-text items, App Group local), top-bar clipboard entry, history panel with whitespace token chips, and a suggestion strip across voice / typing / AI when enabled. Within ~30s of a copy, AI idle hints can prefer clipboard-related cards. / **剪贴板历史**:可选的键盘剪贴板历史(最近 15 条纯文本、App Group 本地),顶栏剪贴板入口、带空白分词芯片的历史面板,以及开启后在语音 / 打字 / AI 面显示的建议条;复制后约 30 秒内 AI 空闲建议可偏向剪贴板相关卡片。 - **Clipboard settings**: Settings home adds a Clipboard page under AI Agent with History and Suggestion-strip toggles (both default off); keyboard deep-links open that page. / **剪贴板设置**:设置首页在 AI Agent 下方新增「剪贴板」页,含历史记录与候选栏展示开关(均默认关闭);键盘可深链打开该页。 - **Paste permission guidance**: the Clipboard settings page explains iOS's durable “Paste from Other Apps” permission and opens iOS Settings directly, so users can set it to Allow once and stop the per-copy paste prompt. / **粘贴授权引导**:剪贴板设置页说明 iOS 的「从其他 App 粘贴」持久授权并可直接跳转系统设置,用户设为「允许」一次即不再每次复制都弹窗。 - **Clipboard settings copy**: shorten history, suggestion-strip, and paste-permission wording to brief user-facing lines. / **剪贴板设置文案**:精简历史、建议条与粘贴授权说明,改为简短面向用户的表述。 - **Settings summary placement**: navigation-row summaries (AI Agent, Clipboard, etc.) sit trailing before the chevron, matching the Version row. / **设置摘要位置**:导航行摘要(AI Agent、剪贴板等)移到右侧 chevron 前,与「版本」行一致。 - **AI answer streaming**: AI mode streams visible answer text into the keyboard as the model writes (all AI-mode transports), with throttled App Group updates, search-fallback draft restart, and a “thinking” status before the first token; dictation polish stays non-streaming. / **AI 回答流式输出**:AI 模式在模型开始写正文后将可见答案增量推送到键盘(覆盖全部 AI 传输路径),经 App Group 节流更新;搜索失败回退会清空半截草稿;首 token 前显示「思考中」;听写润色仍为整段返回。 +- **Spoken clipboard requests**: in AI mode, naming the clipboard out loud (“reply to the clipboard”, “translate my clipboard”) now attaches the stored clipboard text to that question. Naming it is the authorization, so no second confirmation appears; every other AI question is still sent without clipboard text, and with Clipboard History off the request fails with a clear reason instead of guessing. / **口述剪贴板指令**:AI 模式中说出「回复剪贴板」「把剪贴板翻译成英文」等,会把已保存的剪贴板正文附加到该问题。说出即视为授权,不再二次确认;其余 AI 提问一律不附带剪贴板;未开启剪贴板历史时会给出明确原因而非自行猜测。 - **AI Agent settings**: Settings home adds an AI Agent row under General, with a Response length preference (Short / Medium / Detailed, default Medium). AI mode injects soft length guidance into the system prompt and syncs the choice via iCloud settings. / **AI Agent 设置**:设置首页在「通用」下方新增 AI Agent 入口,支持「回复篇幅」(简短 / 中等 / 详细,默认中等)。AI 模式将篇幅作为软约束写入 system prompt,并纳入 iCloud 设置同步。 ### Changed +- **Home library cards**: History and Personal dictionary leave the bottom dock; Home shows two self-sizing preview cards — transcripts split by hairlines (two lines each) and dictionary terms with their source/usage detail line — that push the existing pages with a standard back button. / **首页资料卡片**:历史记录与个性词库移出底部 Dock;首页改为两张按内容自适应高度的预览卡——历史条目用细线分隔(每条最多两行),词库显示词条与来源/使用次数小字——点按后以标准返回按钮进入原页面。 +- **Home scroll status**: engine and session status scroll with the page at the bottom (dock clearance via tab-bar padding) instead of a pinned footer. / **首页滚动状态**:引擎与会话状态随页面滚到底部(Dock 留白与设置页一致),不再钉在底部。 +- **Clipboard AI prompt contract**: clipboard text now reaches the model as a separate escaped `clipboard_text` block beside the instruction, and the AI system prompt treats that block as untrusted content instead of instructions. Hint cards carry only the instruction. / **剪贴板 AI 提示词契约**:剪贴板正文改为与指令并列的独立转义 `clipboard_text` 块,AI system prompt 明确将该块视为不可信内容而非指令;建议卡片只保留指令本身。 +- **Hint feed freshness**: refresh tracks success per locale (a Chinese success no longer masks an English failure), a manifest failure no longer aborts the packs, and a pack past its `expiresAt` — or older than 48 hours without one — falls back to the built-in evergreen catalog instead of showing stale hot topics. / **建议源新鲜度**:刷新按语言分别记录成功(中文成功不再掩盖英文失败),manifest 失败不再中断整轮;超过 `expiresAt`(或无该字段且超过 48 小时)的建议包回退到内置常驻卡片,不再展示陈旧热点。 +- **Privacy policy**: document AI idle suggestions, opt-in clipboard history (panel, suggestion strip, and AI hint use), and when clipboard text is sent to your AI provider. / **隐私政策**:补充 AI 空闲建议、可选剪贴板历史(面板、建议条与 AI 提示用途),以及剪贴板正文在何种情况下会发送给 AI 服务商。 - **Clipboard history row opacity**: history panel entry cards use 50% surface opacity so the keyboard chrome shows through. / **剪贴板历史条目透明度**:历史面板每条记录背景改为 50% 透明度,键盘底色可透出。 - **Translation button chrome**: voice mic-row translation control matches the adjacent undo key (44×44 rounded rectangle) instead of a circular chip. / **翻译按钮样式**:语音麦克风行的翻译控件改为与相邻撤销键一致的 44×44 圆角矩形,不再使用圆形芯片。 - **Undo covers clipboard pastes**: the undo key now rolls back text inserted from the clipboard suggestion strip or history panel, in one step regardless of length. Pasted text stays out of dictation history and is never offered for last-input editing. / **撤销支持剪贴板粘贴**:撤销键现在可回滚从剪贴板建议条或历史面板插入的文字,无论长度都一次撤销到底。粘贴内容不进入听写历史,也不会成为「编辑上次输入」的对象。 - **Undo key label**: rename the accessibility label from “Undo last dictation” to “Undo last input” now that it covers dictation, AI answers, edits and pastes. / **撤销键文案**:无障碍标签由「撤销上次听写」改为「撤销上次输入」,因其现已覆盖听写、AI 答案、编辑与粘贴。 - **Translation chip placement**: move the top-bar translation control onto the voice mic row, mirrored with Undo; the top-bar slot becomes the clipboard button. / **翻译入口位置**:顶栏翻译控件移到语音麦克风行并与撤销对称;原顶栏位置改为剪贴板按钮。 -- **AI empty-state tip**: center “Tap the microphone to ask AI” in the answer area (horizontal + vertical). / **AI 空状态指引**:「点击麦克风向 AI 提问」在答案区域水平与垂直居中。 +- **AI empty-state tip**: center “Tap the microphone to ask AI” in the answer area (horizontal + vertical); idle state now rotates hint cards there. / **AI 空状态指引**:答案区域水平与垂直居中展示空闲建议轮播(替代静态「点击麦克风」文案)。 ### Fixed +- **Clipboard hints missed a fresh copy**: the AI idle carousel now refreshes from the clipboard store as soon as a copy is captured, instead of waiting for the next 4-second rotation or a re-entry into AI mode — the reason clipboard suggestions often never appeared inside their 30-second window. / **新复制无剪贴板建议**:AI 空闲轮播在采集到新复制时立即依据剪贴板刷新,不再等待下一次 4 秒轮换或重新进入 AI 模式——这正是 30 秒窗口内常常看不到剪贴板建议的原因。 +- **Reduce Motion froze hint data**: Reduce Motion now only stops the fade rotation. Cards still refresh, so a new copy appears and a card whose clipboard window closed leaves the carousel. / **Reduce Motion 冻结建议数据**:辅助功能「减弱动态效果」现在只停止渐隐轮换,数据仍会刷新:新复制会出现,剪贴板窗口关闭的卡片会退出轮播。 +- **Expired clipboard hint tap**: tapping a clipboard card after its 30-second window fails closed with a “copy the text again” message instead of sending the instruction with empty material. / **过期剪贴板建议点击**:超过 30 秒窗口后点击剪贴板卡片会明确提示重新复制,而不是把缺少材料的指令发出去。 +- **Blank keyboard on cross-device clipboard**: clipboard-history capture no longer reads `UIPasteboard` on the main thread or during the appear sequence — a Universal Clipboard item still being fetched from another device blocked the main thread for seconds, freezing the keyboard mid-presentation. Reads now run on a background queue, start on the first poll tick after the slide-in, and never overlap. / **跨设备剪贴板导致键盘空白**:剪贴板历史采集不再在主线程、也不再在键盘出现流程中读取 `UIPasteboard`——待从其他设备拉取的通用剪贴板内容会同步阻塞主线程数秒,使键盘卡在呈现过程中。读取改到后台队列、延后到滑入完成后的首次轮询,且不会重叠执行。 +- **Keyboard presentation height**: drop the `target − system encapsulated height` priming trick. It read the pre-presentation full-screen height (874 pt on an iPhone) rather than the keyboard slot, clamped our constraint to 0, and lost to the system's required constraint anyway; the surface is now bottom-anchored so a transient over-tall container can never park it above the visible slot. / **键盘呈现高度**:移除「目标高度 − 系统封装高度」的预置技巧。它读到的是呈现前的整屏高度(iPhone 上 874pt)而非键盘槽,把约束夹成 0,且本来就压不过系统的 required 约束;键盘内容改为底部锚定,容器临时过高时不会再被顶到可见区域之外。 - **Undo of long insertions**: caret verification now compares the tail of the inserted text's last line instead of the whole string, so long or multi-line insertions no longer lose undo the moment the host returns a truncated context. / **长文本撤销**:光标校验改为比对插入文本最后一行的尾部而非整段,长文本或多行插入不再因宿主返回截断上下文而立刻失去撤销能力。 - **Undo after editing last input**: a newer insertion now clears the pending edit transaction, so undo rolls back that insertion instead of deleting it and restoring the older edit's original text. / **编辑上次输入后的撤销**:新的插入会清除待撤销的编辑事务,撤销将回滚该次插入,而不再是删除它并还原上一次编辑前的旧文本。 - **AI waiting spinner duplicate**: remove the mini ProgressView beside the AI status caption; the mic button spinner remains the sole loading indicator while recognizing or generating. / **AI 等待转圈重复**:去掉 AI 状态文案旁的迷你 ProgressView;识别/生成中仅保留麦克风按钮上的 loading。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f12cc30..a3c3bbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,10 @@ # Contributing to OSGKeyboard -Thanks for your interest! OSGKeyboard is a small, opinionated iOS app. We welcome bug reports, feature ideas, and pull requests — please read this guide first. +Thanks for your interest! OSGKeyboard is a Swift 6 app for iOS/iPadOS 26+ and macOS 15+. We welcome bug reports, feature ideas, and pull requests — please read this guide first. + +## License + +OSGKeyboard is source available, not open source. By contributing, you agree to the contribution terms in [`LICENSE`](LICENSE). The license permits personal, non-commercial local builds but does not permit unauthorized redistribution or public derivative versions. ## Code of Conduct @@ -10,7 +14,7 @@ This project follows the [Contributor Covenant](https://www.contributor-covenant Open an issue using the **Bug report** template. Please include: -- iOS version + device model +- OS version + device model (iPhone, iPad, or Apple-silicon Mac) - Xcode version (run `xcodebuild -version`) - Steps to reproduce - Relevant logs (Console.app filtered to `OSGKeyboard`) @@ -29,8 +33,9 @@ Open an issue using the **Feature request** template. Briefly describe: 2. **Generate the project locally:** ```bash brew install xcodegen swiftlint - xcodegen generate # or: ./Scripts/generate-xcodeproj.sh + ./Scripts/generate-xcodeproj.sh ``` + Building requires macOS with Xcode 26. `project.yml` is the project source of truth. 3. **Code style.** SwiftLint config lives in `.swiftlint.yml` — keep it green. We use Swift 6 strict concurrency, no `Sendable` shims where avoidable. 4. **Tests.** Add XCTest coverage under `OSGKeyboardTests/` (or Ext/Mac targets) for any non-trivial logic, and register the class in **exactly one** group in `Tests/suite-manifest.json`. See [`docs/TESTING.md`](docs/TESTING.md). 5. **Build & test before pushing:** @@ -44,14 +49,17 @@ Open an issue using the **Feature request** template. Briefly describe: ## Project structure ``` -OSGKeyboard/ Main iOS app target -OSGKeyboardExt/ Custom Keyboard Extension target -OSGKeyboardShared/ Framework shared by app + extension -OSGKeyboardTests/ XCTest unit tests (host / shared) +OSGKeyboard/ Main iOS/iPadOS app target and host resources +OSGKeyboardExt/ Custom Keyboard Extension target +OSGKeyboardShared/ Lightweight app/extension shared framework +OSGKeyboardHostSupport/ Host-only ASR, cloud, CLM, charts, and StoreKit +OSGKeyboardMac/ macOS menu-bar app; Qwen3 MLX local ASR +OSGKeyboardTests/ XCTest unit tests (host / shared) OSGKeyboardExtTests/ Keyboard / typing XCTest -Tests/ Suite manifest (grouped presets — see docs/TESTING.md) -project.yml XcodeGen project definition (source of truth) -.github/workflows/ CI +OSGKeyboardMacTests/ Mac XCTest +Tests/ Suite manifest (grouped presets — see docs/TESTING.md) +project.yml XcodeGen definition; version/build source of truth +.github/workflows/ CI ``` ## Adding a new LLM provider @@ -68,4 +76,4 @@ The simplest contribution: add a preset to `OSGKeyboardShared/Models/LLMProvider ## Releasing -Maintainers cut releases from `main` via GitHub Releases. The release tag follows `vX.Y.Z`. CHANGELOG.md is updated as part of the release PR. +Maintainers cut releases from `main`. The marketing version and monotonic build number live in `project.yml`; `CHANGELOG.md` is updated as part of the release PR. Do not infer the current Mac binary version from the historical download URL in the README. diff --git a/OSGKeyboard/OSGKeyboardApp.swift b/OSGKeyboard/OSGKeyboardApp.swift index 9b31f43..3021bfb 100644 --- a/OSGKeyboard/OSGKeyboardApp.swift +++ b/OSGKeyboard/OSGKeyboardApp.swift @@ -29,8 +29,15 @@ struct OSGKeyboardApp: App { var body: some Scene { WindowGroup { #if DEBUG - if ProcessInfo.processInfo.arguments.contains("--edit-demo") { + if ProcessInfo.processInfo.arguments.contains("--whats-new-host") { + // Approach A: Notes-like host only; real keyboard extension overlays it. + Self.makeWhatsNewHostView() + } else if ProcessInfo.processInfo.arguments.contains("--edit-demo") { EditDemoView() + } else if ProcessInfo.processInfo.arguments.contains("--ai-demo") { + AIKeyboardDemoView() + } else if ProcessInfo.processInfo.arguments.contains("--clipboard-demo") { + ClipboardHistoryDemoView() } else if ProcessInfo.processInfo.arguments.contains("--edit-pager-ui-test") { ThemedRoot { EditPagerUITestHarness() @@ -62,4 +69,86 @@ struct OSGKeyboardApp: App { #endif } } + + #if DEBUG + @MainActor + private static func makeWhatsNewHostView() -> some View { + let args = ProcessInfo.processInfo.arguments + let scenario = whatsNewScenario(from: args) ?? .edit + let language = whatsNewLanguage(from: args) + let seed = whatsNewSeedText(for: scenario, language: language) + WhatsNewDemoScenario.clear() + WhatsNewDemoScenario.arm(scenario, seedText: seed, language: language) + if let defaults = AppGroup.defaultsIfAvailable { + defaults.set(true, forKey: AppGroupConfiguration.Keys.hasCompletedOnboarding) + // Force extension ExtL10n / SharedL10n into the demo language. + defaults.set( + (language == .en ? AppUILanguage.english : AppUILanguage.chinese).rawValue, + forKey: AppGroupConfiguration.Keys.uiLanguage + ) + // Clipboard demo needs history + suggestion strip flags on. + if scenario == .clipboard { + defaults.set(true, forKey: AppGroupConfiguration.Keys.clipboardHistoryEnabled) + defaults.set( + true, + forKey: AppGroupConfiguration.Keys.clipboardCandidateBarEnabled + ) + } + defaults.synchronize() + } + return NotesHostDemoView( + scenario: scenario, + seedText: seed, + language: language + ) + } + + private static func whatsNewScenario(from args: [String]) -> WhatsNewDemoScenario? { + if let paired = args.first(where: { $0.hasPrefix("--whats-new-scenario=") }) { + let raw = String(paired.dropFirst("--whats-new-scenario=".count)) + return WhatsNewDemoScenario(rawValue: raw) + } + if let idx = args.firstIndex(of: "--whats-new-scenario"), + args.index(after: idx) < args.endIndex + { + return WhatsNewDemoScenario(rawValue: args[args.index(after: idx)]) + } + return nil + } + + private static func whatsNewLanguage(from args: [String]) -> WhatsNewDemoScenario.Language { + if let paired = args.first(where: { $0.hasPrefix("--whats-new-lang=") }) { + let raw = String(paired.dropFirst("--whats-new-lang=".count)) + return WhatsNewDemoScenario.Language(rawValue: raw) ?? .zh + } + if let idx = args.firstIndex(of: "--whats-new-lang"), + args.index(after: idx) < args.endIndex + { + return WhatsNewDemoScenario.Language( + rawValue: args[args.index(after: idx)] + ) ?? .zh + } + return .zh + } + + private static func whatsNewSeedText( + for scenario: WhatsNewDemoScenario, + language: WhatsNewDemoScenario.Language + ) -> String { + switch (scenario, language) { + case (.edit, .zh): + return "明天下午三点开会讨论方案" + case (.edit, .en): + return "Meeting at 3pm tomorrow to discuss the plan" + case (.ai, .zh): + return "周末想找个地方放松一下" + case (.ai, .en): + return "Looking for a place to relax this weekend" + case (.clipboard, .zh): + return "待办:" + case (.clipboard, .en): + return "Todo: " + } + } + #endif } diff --git a/OSGKeyboard/Resources/PrivacyPolicy.html b/OSGKeyboard/Resources/PrivacyPolicy.html index 5575d2d..fc939cf 100644 --- a/OSGKeyboard/Resources/PrivacyPolicy.html +++ b/OSGKeyboard/Resources/PrivacyPolicy.html @@ -22,18 +22,22 @@

中文 · English

OSGKeyboard Privacy Policy

-

Last updated: August 10, 2026

+

Last updated: August 12, 2026

OSGKeyboard is a custom iOS keyboard that turns your voice into text. This policy explains what data the app processes and how it is used.

What we collect

  • Voice audio — captured only while you actively record. The default on-device mode transcribes locally with Apple’s speech APIs and does not upload raw audio. If you explicitly enable cloud recognition, recordings are sent to the speech provider you configure for transcription; that provider’s privacy policy applies. OSGKeyboard does not store or proxy the audio on its own servers.
  • Transcribed text — when AI polish is enabled, the final text (not audio) is sent to the LLM provider whose API key you configured (e.g. OpenAI, DeepSeek) for punctuation and formatting. Without an API key, raw ASR text is inserted and no polish request is sent.
  • -
  • AI mode questions — in AI keyboard mode, your spoken question text is sent to the same configured LLM provider to generate an answer. When that provider supports server-side web search, the provider may retrieve public web results to answer time-sensitive questions. Search queries and retrieved snippets are processed by that provider under its own privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.
  • +
  • AI mode questions — in AI keyboard mode, your spoken question text—or a question you tap from an idle suggestion—is sent to the same configured LLM provider to generate an answer. When that provider supports server-side web search, the provider may retrieve public web results to answer time-sensitive questions. Search queries and retrieved snippets are processed by that provider under its own privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.
  • +
  • AI idle suggestions — the main app may periodically download public hint titles (e.g. hot topics) from OSGKeyboard’s hint feed and, using your configured polish LLM, compress them into short on-device suggestion labels. Suggestion packs are cached in the App Group for the keyboard; the keyboard extension does not fetch the feed itself.
  • +
  • Clipboard history (opt-in) — when you enable Clipboard History, the keyboard may read plain-text pasteboard content while it is visible and keep recent copies on device for the history panel and optional suggestion strip. Within about 30 seconds after a copy, AI mode may also offer clipboard-related idle suggestions; tapping one sends the clipboard text with that prompt to your configured LLM. Saying “clipboard” in an AI question does the same, because naming it is how you choose that text; every other AI question is sent without it. In both cases the clipboard body travels as separate quoted data, never as instructions. Clipboard history is local-only and not synced via iCloud.
  • API credentials — stored in the iOS Keychain and shared between the main app and keyboard extension. When iCloud settings sync is enabled, keys replicate through iCloud Keychain (not iCloud KVS JSON).
  • -
  • App preferences — engine mode, language, and keyboard settings stored in App Group UserDefaults. Optional iCloud sync mirrors preferences, usage statistics, and voice history through your private iCloud account.
  • +
  • App preferences — engine mode, language, and keyboard settings stored in App Group UserDefaults. Optional iCloud sync mirrors eligible preferences, usage statistics, and voice history through your private iCloud account. Clipboard-history consent and its suggestion-strip switch stay device-local and are not activated by iCloud settings sync.
  • +
  • Optional clipboard history — off by default. When you turn it on, the keyboard may read text from the clipboard on this device or from Universal Clipboard; iOS does not provide a reliable way to distinguish those sources. Up to 15 accepted text items are stored only in the local App Group shared by this device’s host app and keyboard extension. Turning history off stops capture, turns off the suggestion strip, and keeps existing items. Reset Settings also keeps them; only the separate confirmed “Clear clipboard history” action removes them. There is no fixed expiry. Secure fields immediately hide clipboard UI and are not captured. Conservative filters reject common OTP shapes, private-key headers, JWTs, Bearer tokens, recognizable provider-key prefixes, and common Luhn-valid 16-digit card numbers, but cannot identify every password or secret. A rejected item can still be pasted through iOS; it is simply not added to history. Clipboard history is not automatically sent to AI. If you insert an item and then actively use polish, the inserted text may be included as context sent to the provider you configured.
  • On-device typing learning — the Chinese keyboard stores selected words and candidate frequencies in the App Group on your device. OSGKeyboard does not upload this user dictionary.
+

Clipboard sensitive-content filtering is applied to newly captured items. Existing history is retained until you use the confirmed clear action.

What we do not collect