Merge pull request #7 from hkgood/feature/in-keyboard-onboarding-and-context-chip

feat(keyboard): in-keyboard onboarding overlay + AppContext chip
This commit is contained in:
Rocky
2026-07-03 15:57:00 +08:00
committed by GitHub
9 changed files with 760 additions and 16 deletions
@@ -276,6 +276,23 @@ public struct AppGroupStore: @unchecked Sendable {
defaults.set(intensity.rawValue, forKey: Key.polishIntensity)
}
// MARK: - Onboarding (v0.3.0+)
//
// Mirrored from `ProviderConfig` so the keyboard extension's
// overlay can read / write the same source of truth without
// instantiating the main-app config (which would drag in
// SwiftUI / Combine and fight the keyboard's main-thread budget).
public var hasCompletedOnboarding: Bool {
get { defaults.bool(forKey: "config.hasCompletedOnboarding") }
set { defaults.set(newValue, forKey: "config.hasCompletedOnboarding") }
}
public var onboardingPage: Int {
get { defaults.integer(forKey: "config.onboardingPage") }
set { defaults.set(newValue, forKey: "config.onboardingPage") }
}
// MARK: - Detected app context (v0.3.0+)
/// Last app context the keyboard extension detected for this
@@ -128,6 +128,36 @@ public final class KeyboardState: ObservableObject {
/// Convenience shorthand used by the pipeline and views.
public var isLocalEngine: Bool { engineMode == "local" }
// MARK: - First-launch onboarding (mirrored from ProviderConfig)
/// Drives the in-keyboard onboarding overlay. When `false`, the
/// keyboard shows a step-by-step overlay instead of the normal UI;
/// when `true`, normal UI renders. Mirrored from `ProviderConfig`
/// so the keyboard never has to instantiate the main-app config.
@Published public var hasCompletedOnboarding: Bool = false
/// Step the user is currently on (0-based). The overlay reads this
/// to render the right page; main-app `ProviderConfig` is the
/// source of truth and the keyboard mirrors it.
@Published public var onboardingPage: Int = 0
/// Per-call context the polish prompt should adapt to. Mirrored
/// from `AppGroupStore.detectedAppContext` on every `viewWillAppear`
/// so the chip stays consistent across the openjump cycle.
@Published public var appContext: AppContext = .unknown
/// `true` when the user tapped something (mic, settings) right
/// before a forced jump to the host app. The keyboard reads this
/// on return and auto-resumes the action so the user does not have
/// to tap the same button twice.
@Published public var pendingResumeAction: ResumeAction = .none
/// Action the keyboard should auto-trigger after a host-app jump
/// completes. Set just before `openHostApp`, consumed (set back to
/// `.none`) after the action fires once.
public enum ResumeAction: Equatable {
case none
case startRecording
case openSettings
}
// Action hooks injected by the view controller at install time.
public var beginRecording: () -> Void = {}
public var endRecording: () -> Void = {}
@@ -143,6 +173,15 @@ public final class KeyboardState: ObservableObject {
/// persist. Wired in `KeyboardViewController.installStateActions`.
public var setTranslationTargetLocaleId: (String) -> Void = { _ in }
public var setPolishScenarioId: (String) -> Void = { _ in }
/// v0.3.0: manually override the auto-detected app context (e.g.
/// when the heuristic guessed wrong). Writes to the App Group so
/// `PolishingService` picks it up on the next take.
public var setAppContext: (AppContext) -> Void = { _ in }
public var advanceOnboarding: () -> Void = {}
public var completeOnboarding: () -> Void = {}
public var requestMicPermission: () -> Void = {}
public var requestSpeechPermission: () -> Void = {}
public var openSystemSettings: () -> Void = {}
public var insertNewline: () -> Void = {}
public var insertSpace: () -> Void = {}
public var deleteBackward: () -> Void = {}