c2f07bd8d2
Introduce a standalone macOS menu-bar app (OSGKeyboardMac) that reuses the platform-agnostic OSGKeyboardShared core: record -> cloud/local ASR -> polish -> insert. Local mode uses Qwen3-ASR via mlx-swift-asr (macOS 15+, Apple Silicon); iOS targets stay zero-SPM. Harden iCloud sync for multi-device correctness: - Per-field settings merge (appSettings.v2) so concurrent edits no longer clobber each other's unrelated fields. - Per-device usage statistics (G-Counter) that sum instead of max(). - Tombstoned dictionary/history merge so deletes propagate and entries can't resurrect. - API keys replicate via iCloud Keychain, never iCloud KVS JSON; pulling a legacy blob without key fields no longer wipes local Keychain entries. - Add a low-risk "Sync Now" action in Settings. Fix Flow keyboard mic state: stay orange until the host publishes a real ready contract, share a single MicVoiceAvailability gate, and self-heal stale cross-process heartbeat jitter instead of getting stuck. Extract shared storage (SpeechHistoryStore/UsageStatisticsStore, ConfigurationStore) into OSGKeyboardShared and add tests for the new sync/merge logic.
87 lines
3.1 KiB
Swift
87 lines
3.1 KiB
Swift
// MacAppContextService.swift
|
|
// OSGKeyboard · Mac
|
|
//
|
|
// Unlike the iOS keyboard extension, macOS can read the frontmost app's
|
|
// bundle ID via NSWorkspace and map it to `AppContext` for polish prompts.
|
|
|
|
import AppKit
|
|
import Foundation
|
|
|
|
enum MacAppContextService {
|
|
/// Bundle IDs → coarse polish context (macOS + cross-platform).
|
|
private static let contextByBundleId: [String: AppContext] = [
|
|
// Code / dev
|
|
"com.apple.dt.Xcode": .code,
|
|
"com.microsoft.VSCode": .code,
|
|
"com.google.android.studio": .code,
|
|
"com.jetbrains.intellij": .code,
|
|
"com.jetbrains.AppCode": .code,
|
|
"com.sublimetext.4": .code,
|
|
"com.github.GitHubClient": .code,
|
|
"com.apple.Terminal": .code,
|
|
"com.googlecode.iterm2": .code,
|
|
"dev.warp.Warp-Stable": .code,
|
|
// Email
|
|
"com.apple.mail": .email,
|
|
"com.microsoft.Outlook": .email,
|
|
"com.google.Gmail": .email,
|
|
"com.readdle.smartemail": .email,
|
|
// Chat / IM
|
|
"com.tencent.xinWeChat": .chat,
|
|
"com.tencent.qq": .chat,
|
|
"com.tencent.wework": .chat,
|
|
"com.tinyspeck.slackmacgap": .chat,
|
|
"com.hnc.Discord": .chat,
|
|
"com.microsoft.teams": .chat,
|
|
"com.microsoft.teams2": .chat,
|
|
"ru.keepcoder.Telegram": .chat,
|
|
"net.whatsapp.WhatsApp": .chat,
|
|
"com.apple.MobileSMS": .chat,
|
|
"com.facebook.archon": .chat,
|
|
"com.laiwang.DingTalk": .chat,
|
|
"com.bytedance.feishu": .chat,
|
|
// Documents / notes
|
|
"com.apple.Notes": .document,
|
|
"com.apple.iWork.Pages": .document,
|
|
"notion.id": .document,
|
|
"md.obsidian": .document,
|
|
"net.shinyfrog.bear": .document,
|
|
"com.agiletortoise.Drafts-OSX": .document,
|
|
"com.microsoft.Word": .document,
|
|
"com.google.GoogleDocs": .document,
|
|
"com.evernote.Evernote": .document,
|
|
"com.microsoft.onenote.mac": .document,
|
|
]
|
|
|
|
/// Chat-style apps from the shared host registry (iOS bundle IDs often
|
|
/// match Mac counterparts for cross-platform IM).
|
|
private static let chatBundleIdsFromRegistry: Set<String> = {
|
|
Set(HostAppURLRegistry.entries.map(\.bundleId))
|
|
}()
|
|
|
|
static func frontmostApplicationName() -> String? {
|
|
NSWorkspace.shared.frontmostApplication?.localizedName
|
|
}
|
|
|
|
static func frontmostBundleIdentifier() -> String? {
|
|
NSWorkspace.shared.frontmostApplication?.bundleIdentifier
|
|
}
|
|
|
|
static func detectContext() -> AppContext {
|
|
guard let bundleId = frontmostBundleIdentifier() else { return .unknown }
|
|
if let mapped = contextByBundleId[bundleId] { return mapped }
|
|
if chatBundleIdsFromRegistry.contains(bundleId) { return .chat }
|
|
if bundleId.hasPrefix("com.apple.Safari") || bundleId.contains("chrome") {
|
|
return .document
|
|
}
|
|
return .unknown
|
|
}
|
|
|
|
/// Persist detected context into the shared configuration store so
|
|
/// `PolishingService` reads the same signal as on iOS.
|
|
static func captureAndPersist(to store: AppGroupStore) {
|
|
let context = detectContext()
|
|
store.setDetectedAppContext(context)
|
|
}
|
|
}
|