From cb6c1689c1713129ecd8cc2905ddd63409e0697a Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 15:14:21 -0600 Subject: [PATCH 1/9] Integrate Kotlin shared transcription modules - Add KMP bridge and native adapters for transcription flows - Wire shared frameworks into the Xcode build - Move transcription policy and model startup logic behind shared code --- .gitignore | 2 + Pindrop.xcodeproj/project.pbxproj | 108 +++ Pindrop/AppCoordinator.swift | 192 +++-- Pindrop/Models/MediaTranscriptionTypes.swift | 380 +++++++- Pindrop/Services/ModelManager.swift | 365 +------- .../KMPTranscriptionBridge.swift | 813 ++++++++++++++++++ .../NativeTranscriptionAdapters.swift | 174 ++++ .../Transcription/SpeakerDiarizer.swift | 16 +- .../StreamingTranscriptionEngine.swift | 18 +- .../Transcription/TranscriptionEngine.swift | 23 +- .../TranscriptionModelCatalog.swift | 333 +++++++ .../Transcription/TranscriptionPolicy.swift | 74 ++ .../Transcription/TranscriptionPorts.swift | 122 +++ Pindrop/Services/TranscriptionService.swift | 287 +++++-- PindropTests/ModelManagerTests.swift | 8 + PindropTests/TranscriptionServiceTests.swift | 45 + README.md | 16 + composer-atoms-DxHKXap6.js | 2 - create_xcode_project.sh | 33 - justfile | 12 + main-Dl6lTb0_.js | 87 -- package.json | 91 -- preload.js | 2 - shared/README.md | 16 + shared/build.gradle.kts | 3 + shared/core/build.gradle.kts | 42 + .../shared/core/TranscriptionContracts.kt | 133 +++ shared/feature-transcription/build.gradle.kts | 43 + .../MediaTranscriptionJobStateMachine.kt | 206 +++++ .../SharedTranscriptionOrchestrator.kt | 389 +++++++++ .../MediaTranscriptionJobStateMachineTest.kt | 113 +++ .../SharedTranscriptionOrchestratorTest.kt | 354 ++++++++ shared/gradle.properties | 2 + shared/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + shared/gradlew | 271 ++++++ shared/gradlew.bat | 93 ++ shared/settings.gradle.kts | 19 + 38 files changed, 4075 insertions(+), 819 deletions(-) create mode 100644 Pindrop/Services/Transcription/KMPTranscriptionBridge.swift create mode 100644 Pindrop/Services/Transcription/NativeTranscriptionAdapters.swift create mode 100644 Pindrop/Services/Transcription/TranscriptionModelCatalog.swift create mode 100644 Pindrop/Services/Transcription/TranscriptionPolicy.swift create mode 100644 Pindrop/Services/Transcription/TranscriptionPorts.swift delete mode 100644 composer-atoms-DxHKXap6.js delete mode 100755 create_xcode_project.sh delete mode 100644 main-Dl6lTb0_.js delete mode 100644 package.json delete mode 100644 preload.js create mode 100644 shared/README.md create mode 100644 shared/build.gradle.kts create mode 100644 shared/core/build.gradle.kts create mode 100644 shared/core/src/commonMain/kotlin/tech/watzon/pindrop/shared/core/TranscriptionContracts.kt create mode 100644 shared/feature-transcription/build.gradle.kts create mode 100644 shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachine.kt create mode 100644 shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt create mode 100644 shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt create mode 100644 shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt create mode 100644 shared/gradle.properties create mode 100644 shared/gradle/wrapper/gradle-wrapper.jar create mode 100644 shared/gradle/wrapper/gradle-wrapper.properties create mode 100755 shared/gradlew create mode 100644 shared/gradlew.bat create mode 100644 shared/settings.gradle.kts diff --git a/.gitignore b/.gitignore index 87fac3e..5f36944 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ xcuserdata/ # Build products build/ DerivedData/ +.gradle/ +.kotlin/ *.ipa *.dSYM.zip *.dSYM diff --git a/Pindrop.xcodeproj/project.pbxproj b/Pindrop.xcodeproj/project.pbxproj index b8fd331..b3ddc2d 100644 --- a/Pindrop.xcodeproj/project.pbxproj +++ b/Pindrop.xcodeproj/project.pbxproj @@ -99,6 +99,15 @@ MEDIASOURCEKINDBUILD01 /* MediaSourceKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = MEDIASOURCEKINDFILE01 /* MediaSourceKind.swift */; }; MEDIATRANSSTATEBUILDTEST01A2B3C4D5E6F /* MediaTranscriptionFeatureStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = MEDIATRANSSTATEFILETEST01A2B3C4D5E6F /* MediaTranscriptionFeatureStateTests.swift */; }; MEDIATRANSTYPESBUILD01 /* MediaTranscriptionTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = MEDIATRANSTYPESFILE01 /* MediaTranscriptionTypes.swift */; }; + KMPADAPTERSBUILD01A2B3C4D5E6F /* NativeTranscriptionAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = KMPADAPTERSFILE01A2B3C4D5E6F /* NativeTranscriptionAdapters.swift */; }; + KMPBRIDGEBUILD01A2B3C4D5E6F /* KMPTranscriptionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = KMPBRIDGEFILE01A2B3C4D5E6F /* KMPTranscriptionBridge.swift */; }; + KMPMODELCATALOGBUILD01A2B3C4D5E6F /* TranscriptionModelCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = KMPMODELCATALOGFILE01A2B3C4D5E6F /* TranscriptionModelCatalog.swift */; }; + KMPPOLICYBUILD01A2B3C4D5E6F /* TranscriptionPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = KMPPOLICYFILE01A2B3C4D5E6F /* TranscriptionPolicy.swift */; }; + KMPPORTSBUILD01A2B3C4D5E6F /* TranscriptionPorts.swift in Sources */ = {isa = PBXBuildFile; fileRef = KMPPORTSFILE01A2B3C4D5E6F /* TranscriptionPorts.swift */; }; + KMPSHAREDCOREBUILD01A2B3C4D5E6F /* PindropSharedCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDCOREFILE01A2B3C4D5E6F /* PindropSharedCore.framework */; }; + KMPSHAREDCOREEMBED01A2B3C4D5E6F /* PindropSharedCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDCOREFILE01A2B3C4D5E6F /* PindropSharedCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + KMPSHAREDTRANSBUILD01A2B3C4D5E6F /* PindropSharedTranscription.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */; }; + KMPSHAREDTRANSEMBED01A2B3C4D5E6F /* PindropSharedTranscription.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; MNTFMT01A2B3C4D5E6F /* MentionFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = MNTFMT02A2B3C4D5E6F /* MentionFormatter.swift */; }; MNTFMTTEST01A2B3C4D5E6F /* MentionFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = MNTFMTTEST02A2B3C4D5E6F /* MentionFormatterTests.swift */; }; MNTRWRT02A2B3C4D5E6F /* MentionRewriteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = MNTRWRT01A2B3C4D5E6F /* MentionRewriteService.swift */; }; @@ -169,6 +178,21 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + KMPFRAMEWORKSEMBEDPHASE01A2B3C4D5E6F /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + KMPSHAREDCOREEMBED01A2B3C4D5E6F /* PindropSharedCore.framework in Embed Frameworks */, + KMPSHAREDTRANSEMBED01A2B3C4D5E6F /* PindropSharedTranscription.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 009AECB5A914D3A0B2BE7A1E /* TaskScheduler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TaskScheduler.swift; path = TaskScheduler.swift; sourceTree = ""; }; 01E366552FAA4505B9775ACB /* PindropApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PindropApp.swift; sourceTree = ""; }; @@ -262,6 +286,13 @@ MEDIASOURCEKINDFILE01 /* MediaSourceKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceKind.swift; sourceTree = ""; }; MEDIATRANSSTATEFILETEST01A2B3C4D5E6F /* MediaTranscriptionFeatureStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTranscriptionFeatureStateTests.swift; sourceTree = ""; }; MEDIATRANSTYPESFILE01 /* MediaTranscriptionTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTranscriptionTypes.swift; sourceTree = ""; }; + KMPADAPTERSFILE01A2B3C4D5E6F /* NativeTranscriptionAdapters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTranscriptionAdapters.swift; sourceTree = ""; }; + KMPBRIDGEFILE01A2B3C4D5E6F /* KMPTranscriptionBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPTranscriptionBridge.swift; sourceTree = ""; }; + KMPMODELCATALOGFILE01A2B3C4D5E6F /* TranscriptionModelCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptionModelCatalog.swift; sourceTree = ""; }; + KMPPOLICYFILE01A2B3C4D5E6F /* TranscriptionPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptionPolicy.swift; sourceTree = ""; }; + KMPPORTSFILE01A2B3C4D5E6F /* TranscriptionPorts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptionPorts.swift; sourceTree = ""; }; + KMPSHAREDCOREFILE01A2B3C4D5E6F /* PindropSharedCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedCore.framework; path = shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64/PindropSharedCore.framework; sourceTree = SOURCE_ROOT; }; + KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedTranscription.framework; path = shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64/PindropSharedTranscription.framework; sourceTree = SOURCE_ROOT; }; MNTFMT02A2B3C4D5E6F /* MentionFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionFormatter.swift; sourceTree = ""; }; MNTFMTTEST02A2B3C4D5E6F /* MentionFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionFormatterTests.swift; sourceTree = ""; }; MNTRWRT01A2B3C4D5E6F /* MentionRewriteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionRewriteService.swift; sourceTree = ""; }; @@ -321,6 +352,8 @@ 37A010B78338464E90A60BD4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; files = ( + KMPSHAREDCOREBUILD01A2B3C4D5E6F /* PindropSharedCore.framework in Frameworks */, + KMPSHAREDTRANSBUILD01A2B3C4D5E6F /* PindropSharedTranscription.framework in Frameworks */, E30EF5B5D83B494BB7B98568 /* WhisperKit in Frameworks */, FLUIDAUDIOBUILD01A2B3C4D5E6F /* FluidAudio in Frameworks */, SPARKLEBUILD01A2B3C4D5E6F /* Sparkle in Frameworks */, @@ -467,6 +500,8 @@ isa = PBXGroup; children = ( 49CB4FF0EA78C24EFBB3F08A /* Cocoa.framework */, + KMPSHAREDCOREFILE01A2B3C4D5E6F /* PindropSharedCore.framework */, + KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */, ); name = "OS X"; sourceTree = ""; @@ -644,6 +679,11 @@ isa = PBXGroup; children = ( AUDIOCAPS01A2B3C4D5E6F /* AudioEngineCapabilities.swift */, + KMPPORTSFILE01A2B3C4D5E6F /* TranscriptionPorts.swift */, + KMPPOLICYFILE01A2B3C4D5E6F /* TranscriptionPolicy.swift */, + KMPBRIDGEFILE01A2B3C4D5E6F /* KMPTranscriptionBridge.swift */, + KMPMODELCATALOGFILE01A2B3C4D5E6F /* TranscriptionModelCatalog.swift */, + KMPADAPTERSFILE01A2B3C4D5E6F /* NativeTranscriptionAdapters.swift */, TRANSENG01A2B3C4D5E6F /* TranscriptionEngine.swift */, STREAMASR01A2B3C4D5E6F /* StreamingTranscriptionEngine.swift */, VADDETECT01A2B3C4D5E6F /* VoiceActivityDetector.swift */, @@ -677,9 +717,11 @@ isa = PBXNativeTarget; buildConfigurationList = BC51A0710CA54327ADB4808B /* Build configuration list for PBXNativeTarget "Pindrop" */; buildPhases = ( + KMPSHAREDBUILD01A2B3C4D5E6F /* Build Shared Kotlin Frameworks */, 6E3989E80A1A4BF5BEC7A015 /* Sources */, 37A010B78338464E90A60BD4 /* Frameworks */, C9D46B9D12B748D9BBCFE2A3 /* Resources */, + KMPFRAMEWORKSEMBEDPHASE01A2B3C4D5E6F /* Embed Frameworks */, ); buildRules = ( ); @@ -814,6 +856,35 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + KMPSHAREDBUILD01A2B3C4D5E6F /* Build Shared Kotlin Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/shared/build.gradle.kts", + "$(SRCROOT)/shared/gradle.properties", + "$(SRCROOT)/shared/core/build.gradle.kts", + "$(SRCROOT)/shared/feature-transcription/build.gradle.kts", + "$(SRCROOT)/shared/settings.gradle.kts", + "$(SRCROOT)/shared/gradlew", + ); + name = "Build Shared Kotlin Frameworks"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/Info.plist", + "$(SRCROOT)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/Info.plist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -euo pipefail\ncd \"$SRCROOT/shared\"\n\"$SRCROOT/shared/gradlew\" --no-daemon --console=plain -p \"$SRCROOT/shared\" :core:assemblePindropSharedCoreXCFramework :feature-transcription:assemblePindropSharedTranscriptionXCFramework\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 572D6581DD0541D98D9191B2 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -868,6 +939,10 @@ 862E74C35C4D46713D5A1B80 /* TranscriptionRecordSchema.swift in Sources */, MEDIASOURCEKINDBUILD01 /* MediaSourceKind.swift in Sources */, MEDIATRANSTYPESBUILD01 /* MediaTranscriptionTypes.swift in Sources */, + KMPPORTSBUILD01A2B3C4D5E6F /* TranscriptionPorts.swift in Sources */, + KMPPOLICYBUILD01A2B3C4D5E6F /* TranscriptionPolicy.swift in Sources */, + KMPBRIDGEBUILD01A2B3C4D5E6F /* KMPTranscriptionBridge.swift in Sources */, + KMPMODELCATALOGBUILD01A2B3C4D5E6F /* TranscriptionModelCatalog.swift in Sources */, WORDRPLACEA1B2C3D4 /* WordReplacement.swift in Sources */, VOCABWORDA1B2C3D4 /* VocabularyWord.swift in Sources */, NOTESCH001A2B3C4D5 /* NoteSchema.swift in Sources */, @@ -930,6 +1005,7 @@ NOTESTORE1A2B3C4D5E6F70 /* NotesStore.swift in Sources */, MARKDOWN01A2B3C4D5E6F70 /* MarkdownEditor.swift in Sources */, PREVMOCKS1A2B3C4D5E6F70 /* PreviewMocks.swift in Sources */, + KMPADAPTERSBUILD01A2B3C4D5E6F /* NativeTranscriptionAdapters.swift in Sources */, TRANSENG02A2B3C4D5E6F /* TranscriptionEngine.swift in Sources */, WHISPERENG02A2B3C4D5E6F /* WhisperKitEngine.swift in Sources */, PARAKEETENG02A2B3C4D5E6F /* ParakeetEngine.swift in Sources */, @@ -992,6 +1068,11 @@ CURRENT_PROJECT_VERSION = 13; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = MB5789APU7; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.14.0; @@ -1020,6 +1101,12 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + ); GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Pindrop/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Pindrop; @@ -1101,6 +1188,11 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = MB5789APU7; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_BUNDLE_IDENTIFIER = tech.watzon.pindrop.uitests; @@ -1194,6 +1286,12 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + ); GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Pindrop/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Pindrop; @@ -1218,6 +1316,11 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = MB5789APU7; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_BUNDLE_IDENTIFIER = tech.watzon.pindrop.uitests; @@ -1238,6 +1341,11 @@ CURRENT_PROJECT_VERSION = 13; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = MB5789APU7; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.14.0; diff --git a/Pindrop/AppCoordinator.swift b/Pindrop/AppCoordinator.swift index a27e964..ac18fe2 100644 --- a/Pindrop/AppCoordinator.swift +++ b/Pindrop/AppCoordinator.swift @@ -207,20 +207,16 @@ final class AppCoordinator { disableLoopWindow: TimeInterval, maxReenableAttemptsBeforeRecreate: Int ) -> EventTapRecoveryDecision { - let nextCount: Int - if let lastDisableAt, - now.timeIntervalSince(lastDisableAt) <= disableLoopWindow { - nextCount = consecutiveDisableCount + 1 - } else { - nextCount = 1 - } - - let recreateThreshold = max(1, maxReenableAttemptsBeforeRecreate) - let action: EventTapRecoveryAction = nextCount >= recreateThreshold ? .recreate : .reenable + let decision = KMPTranscriptionBridge.determineEventTapRecovery( + elapsedSinceLastDisable: lastDisableAt.map { now.timeIntervalSince($0) }, + consecutiveDisableCount: consecutiveDisableCount, + disableLoopWindow: disableLoopWindow, + maxReenableAttemptsBeforeRecreate: maxReenableAttemptsBeforeRecreate + ) return EventTapRecoveryDecision( - consecutiveDisableCount: nextCount, - action: action + consecutiveDisableCount: decision.consecutiveDisableCount, + action: decision.action == .recreate ? .recreate : .reenable ) } @@ -768,33 +764,44 @@ final class AppCoordinator { ensureAccessibilityPermissionForDirectInsert(trigger: "startup", showFallbackAlert: false) - var modelName = settingsStore.selectedModel + await modelManager.refreshDownloadedModels() + let downloadedModels = await modelManager.getDownloadedModels() + let startupModel = KMPTranscriptionBridge.resolveStartupModel( + selectedModelId: settingsStore.selectedModel, + defaultModelId: SettingsStore.Defaults.selectedModel, + availableModels: modelManager.availableModels, + downloadedModelIds: downloadedModels.map(\.name) + ) - if !modelManager.availableModels.contains(where: { $0.name == modelName }) { - Log.model.warning("Selected model \(modelName) is not recognized, resetting to default") - modelName = SettingsStore.Defaults.selectedModel - settingsStore.selectedModel = modelName + if settingsStore.selectedModel != startupModel.updatedSelectedModelId { + if startupModel.action == .loadFallback { + Log.model.info( + "Selected model \(settingsStore.selectedModel) not found locally, falling back to \(startupModel.updatedSelectedModelId)" + ) + } else { + Log.model.warning( + "Selected model \(settingsStore.selectedModel) is not recognized, resetting to \(startupModel.updatedSelectedModelId)" + ) + } + settingsStore.selectedModel = startupModel.updatedSelectedModelId } - let selectedModel = modelManager.availableModels.first(where: { $0.name == modelName }) - let selectedProvider = selectedModel?.provider ?? .whisperKit - let selectedDisplayName = selectedModel?.displayName ?? modelName - - await modelManager.refreshDownloadedModels() - let modelExists = modelManager.isModelDownloaded(modelName) - - if modelExists { + switch startupModel.action { + case .loadSelected: splashController.setLoading("Loading model...") - Log.model.info("Model \(modelName) found, loading...") + Log.model.info("Model \(startupModel.resolvedModel.name) found, loading...") do { - try await loadAndActivateModel(named: modelName, provider: selectedProvider) + try await loadAndActivateModel( + named: startupModel.resolvedModel.name, + provider: startupModel.resolvedModel.provider + ) Log.model.info("Model loaded successfully") } catch { - if selectedProvider == .whisperKit { + if startupModel.resolvedModel.provider == .whisperKit { do { try await attemptWhisperModelRepairAndReload( - modelName: modelName, - displayName: selectedDisplayName + modelName: startupModel.resolvedModel.name, + displayName: startupModel.resolvedModel.displayName ) Log.model.info("Model repaired and loaded successfully") } catch { @@ -804,38 +811,36 @@ final class AppCoordinator { handleModelLoadError(error, context: "Failed to load transcription model") } } - } else { - // Model missing - check if any model is available for fallback - let downloadedModels = await modelManager.getDownloadedModels() - - if let fallbackModel = downloadedModels.first { - Log.model.info("Selected model \(modelName) not found, falling back to \(fallbackModel.name)") - splashController.setLoading("Using \(fallbackModel.displayName)...") - settingsStore.selectedModel = fallbackModel.name - do { - try await loadAndActivateModel(named: fallbackModel.name, provider: fallbackModel.provider) - Log.model.info("Fallback model loaded successfully") - } catch { - handleModelLoadError(error, context: "Failed to load fallback model") - } - } else { - // No models available - download the selected one - splashController.setDownloading("Downloading \(modelName)...") - Log.model.info("Model \(modelName) not found, downloading...") - - do { - try await modelManager.downloadModel(named: modelName) { [weak self] progress in - Task { @MainActor in - self?.splashController.updateProgress(progress) - } + case .loadFallback: + splashController.setLoading("Using \(startupModel.resolvedModel.displayName)...") + do { + try await loadAndActivateModel( + named: startupModel.resolvedModel.name, + provider: startupModel.resolvedModel.provider + ) + Log.model.info("Fallback model loaded successfully") + } catch { + handleModelLoadError(error, context: "Failed to load fallback model") + } + case .downloadSelected: + splashController.setDownloading("Downloading \(startupModel.resolvedModel.name)...") + Log.model.info("Model \(startupModel.resolvedModel.name) not found, downloading...") + + do { + try await modelManager.downloadModel(named: startupModel.resolvedModel.name) { [weak self] progress in + Task { @MainActor in + self?.splashController.updateProgress(progress) } - splashController.setLoading("Loading model...") - Log.model.info("Model downloaded, loading...") - try await loadAndActivateModel(named: modelName, provider: selectedProvider) - Log.model.info("Model loaded successfully") - } catch { - handleModelLoadError(error, context: "Failed to download/load model") } + splashController.setLoading("Loading model...") + Log.model.info("Model downloaded, loading...") + try await loadAndActivateModel( + named: startupModel.resolvedModel.name, + provider: startupModel.resolvedModel.provider + ) + Log.model.info("Model loaded successfully") + } catch { + handleModelLoadError(error, context: "Failed to download/load model") } } @@ -1353,9 +1358,11 @@ final class AppCoordinator { // MARK: - Live Session Context private func shouldRunLiveContextSession() -> Bool { - settingsStore.aiEnhancementEnabled && - settingsStore.enableUIContext && - settingsStore.vibeLiveSessionEnabled + KMPTranscriptionBridge.shouldRunLiveContextSession( + aiEnhancementEnabled: settingsStore.aiEnhancementEnabled, + uiContextEnabled: settingsStore.enableUIContext, + liveSessionEnabled: settingsStore.vibeLiveSessionEnabled + ) } private func updateVibeRuntimeStateFromSettings() { @@ -1473,9 +1480,11 @@ final class AppCoordinator { trigger: ContextSessionUpdateTrigger, in session: ContextSessionState ) -> Bool { - guard trigger != .recordingStart else { return true } - guard let lastSignature = session.transitions.last?.transitionSignature else { return true } - return lastSignature != signature + KMPTranscriptionBridge.shouldAppendTransition( + signature: signature, + trigger: trigger.rawValue, + lastSignature: session.transitions.last?.transitionSignature + ) } private func currentLiveSessionContext() -> AIEnhancementService.LiveSessionContext? { @@ -2103,29 +2112,28 @@ final class AppCoordinator { } static func normalizedTranscriptionText(_ text: String) -> String { - text.trimmingCharacters(in: .whitespacesAndNewlines) + TranscriptionPolicy.normalizedTranscriptionText(text) } private func isTranscriptionEffectivelyEmpty(_ text: String) -> Bool { Self.isTranscriptionEffectivelyEmpty(text) } static func isTranscriptionEffectivelyEmpty(_ text: String) -> Bool { - let normalizedText = normalizedTranscriptionText(text) - if normalizedText.isEmpty { - return true - } - return normalizedText.caseInsensitiveCompare("[BLANK AUDIO]") == .orderedSame + TranscriptionPolicy.isTranscriptionEffectivelyEmpty(text) } static func shouldPersistHistory(outputSucceeded: Bool, text: String) -> Bool { - outputSucceeded && !isTranscriptionEffectivelyEmpty(text) + TranscriptionPolicy.shouldPersistHistory(outputSucceeded: outputSucceeded, text: text) } static func shouldUseSpeakerDiarization( diarizationFeatureEnabled: Bool, isStreamingSessionActive: Bool ) -> Bool { - diarizationFeatureEnabled && !isStreamingSessionActive + TranscriptionPolicy.shouldUseSpeakerDiarization( + diarizationFeatureEnabled: diarizationFeatureEnabled, + isStreamingSessionActive: isStreamingSessionActive + ) } static func shouldUseStreamingTranscription( @@ -2134,10 +2142,12 @@ final class AppCoordinator { aiEnhancementEnabled: Bool, isQuickCaptureMode: Bool ) -> Bool { - streamingFeatureEnabled && - outputMode == .directInsert && - !aiEnhancementEnabled && - !isQuickCaptureMode + TranscriptionPolicy.shouldUseStreamingTranscription( + streamingFeatureEnabled: streamingFeatureEnabled, + outputMode: outputMode, + aiEnhancementEnabled: aiEnhancementEnabled, + isQuickCaptureMode: isQuickCaptureMode + ) } private func encodeDiarizationSegmentsJSON(_ segments: [DiarizedTranscriptSegment]?) -> String? { @@ -3075,7 +3085,10 @@ final class AppCoordinator { } static func shouldSuppressEscapeEvent(isRecording: Bool, isProcessing: Bool) -> Bool { - isRecording || isProcessing + RecordingInteractionPolicy.shouldSuppressEscapeEvent( + isRecording: isRecording, + isProcessing: isProcessing + ) } static func isDoubleEscapePress( @@ -3083,8 +3096,11 @@ final class AppCoordinator { lastEscapeTime: Date?, threshold: TimeInterval ) -> Bool { - guard let lastEscapeTime else { return false } - return now.timeIntervalSince(lastEscapeTime) <= threshold + RecordingInteractionPolicy.isDoubleEscapePress( + now: now, + lastEscapeTime: lastEscapeTime, + threshold: threshold + ) } private nonisolated func handleKeyEvent( @@ -3216,7 +3232,7 @@ final class AppCoordinator { mediaTranscriptionTask?.cancel() mediaTranscriptionTask = nil mediaTranscriptionState.showLibrary() - mediaTranscriptionState.libraryMessage = "Transcription canceled." + mediaTranscriptionState.setLibraryMessage("Transcription canceled.") mediaTranscriptionState.clearCurrentJob() if hadStreamingSession { Task { @MainActor [weak self] in @@ -3400,8 +3416,8 @@ final class AppCoordinator { guard let self else { return } do { try await self.modelManager.downloadFeatureModel(.diarization) - self.mediaTranscriptionState.setupIssue = nil - self.mediaTranscriptionState.libraryMessage = "Speaker diarization is ready." + self.mediaTranscriptionState.clearSetupIssue() + self.mediaTranscriptionState.setLibraryMessage("Speaker diarization is ready.") } catch { self.mediaTranscriptionState.setSetupIssue(error.localizedDescription) } @@ -3410,7 +3426,7 @@ final class AppCoordinator { private func startMediaTranscriptionTask(from request: MediaTranscriptionRequest) { guard mediaTranscriptionTask == nil else { - mediaTranscriptionState.libraryMessage = "Another transcription is already in progress." + mediaTranscriptionState.setLibraryMessage("Another transcription is already in progress.") return } @@ -3423,7 +3439,7 @@ final class AppCoordinator { private func performMediaTranscription(_ request: MediaTranscriptionRequest) async { guard !isRecording && !isProcessing else { - mediaTranscriptionState.libraryMessage = "Finish the active transcription before starting another one." + mediaTranscriptionState.setLibraryMessage("Finish the active transcription before starting another one.") return } @@ -3537,7 +3553,7 @@ final class AppCoordinator { resetProcessingState() didResetProcessingState = true mediaTranscriptionState.showLibrary() - mediaTranscriptionState.libraryMessage = "Transcription canceled." + mediaTranscriptionState.setLibraryMessage("Transcription canceled.") mediaTranscriptionState.clearCurrentJob() } catch let error as MediaIngestionError { Log.app.error("Media ingestion failed: \(error.localizedDescription)") @@ -3547,7 +3563,7 @@ final class AppCoordinator { if case .toolingUnavailable(let message) = error { mediaTranscriptionState.setSetupIssue(message) } else { - mediaTranscriptionState.libraryMessage = error.localizedDescription + mediaTranscriptionState.setLibraryMessage(error.localizedDescription) } } catch { Log.app.error("Media transcription failed: \(error)") diff --git a/Pindrop/Models/MediaTranscriptionTypes.swift b/Pindrop/Models/MediaTranscriptionTypes.swift index 19e90ce..2532300 100644 --- a/Pindrop/Models/MediaTranscriptionTypes.swift +++ b/Pindrop/Models/MediaTranscriptionTypes.swift @@ -8,6 +8,10 @@ import Foundation import Observation +#if canImport(PindropSharedTranscription) +import PindropSharedTranscription +#endif + enum MediaLibrarySortMode: String, CaseIterable, Equatable, Sendable { case newest case oldest @@ -143,10 +147,12 @@ final class MediaTranscriptionFeatureState { var libraryMessage: String? func beginJob(_ job: MediaTranscriptionJobState) { - currentJob = job - setupIssue = nil - libraryMessage = nil - route = .processing(job.id) + applySharedSnapshot( + KMPMediaTranscriptionBridge.beginJob( + snapshot: sharedSnapshot(), + job: job + ) + ) } func updateJob( @@ -155,18 +161,19 @@ final class MediaTranscriptionFeatureState { detail: String? = nil, errorMessage: String? = nil ) { - guard var job = currentJob else { return } - job.stage = stage - job.progress = progress - if let detail { - job.detail = detail - } - job.errorMessage = errorMessage - currentJob = job + applySharedSnapshot( + KMPMediaTranscriptionBridge.updateJob( + snapshot: sharedSnapshot(), + stage: stage, + progress: progress, + detail: detail, + errorMessage: errorMessage + ) + ) } func exitProcessingView() { - route = .library + showLibrary() } func selectRecord(_ id: UUID) { @@ -189,14 +196,12 @@ final class MediaTranscriptionFeatureState { } func handleDeletedRecord(_ id: UUID, message: String = "Transcription deleted.") { + if case .detail(let detailID) = route, detailID == id { + route = .library + } if selectedRecordID == id { selectedRecordID = nil } - - if case .detail(let recordID) = route, recordID == id { - route = .library - } - libraryMessage = message } @@ -208,22 +213,23 @@ final class MediaTranscriptionFeatureState { } func completeCurrentJob(with recordID: UUID, shouldNavigateToDetail: Bool) { - updateJob(stage: .completed, progress: 1.0, detail: "Saved transcription") - selectedRecordID = recordID - if shouldNavigateToDetail { - route = .detail(recordID) - } else { - route = .library - libraryMessage = "Transcription finished." - } + applySharedSnapshot( + KMPMediaTranscriptionBridge.completeCurrentJob( + snapshot: sharedSnapshot(), + recordID: recordID, + shouldNavigateToDetail: shouldNavigateToDetail + ) + ) } func failCurrentJob(_ message: String, returnToLibrary: Bool) { - updateJob(stage: .failed, progress: nil, errorMessage: message) - if returnToLibrary { - route = .library - libraryMessage = message - } + applySharedSnapshot( + KMPMediaTranscriptionBridge.failCurrentJob( + snapshot: sharedSnapshot(), + message: message, + returnToLibrary: returnToLibrary + ) + ) } func clearCurrentJob() { @@ -235,10 +241,322 @@ final class MediaTranscriptionFeatureState { route = .library } + func clearSetupIssue() { + setupIssue = nil + } + + func setLibraryMessage(_ message: String?) { + libraryMessage = message + } + func updateDraftLinkFromClipboard(_ candidate: String) { guard !hasUserEditedDraftLink else { return } let trimmed = candidate.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty, draftLink != trimmed else { return } draftLink = trimmed } + + private func sharedSnapshot() -> SharedMediaTranscriptionFeatureSnapshot { + SharedMediaTranscriptionFeatureSnapshot( + route: route, + selectedRecordID: selectedRecordID, + selectedFolderID: selectedFolderID, + currentJob: currentJob, + setupIssue: setupIssue, + libraryMessage: libraryMessage + ) + } + + private func applySharedSnapshot(_ snapshot: SharedMediaTranscriptionFeatureSnapshot) { + route = snapshot.route + selectedRecordID = snapshot.selectedRecordID + selectedFolderID = snapshot.selectedFolderID + currentJob = snapshot.currentJob + setupIssue = snapshot.setupIssue + libraryMessage = snapshot.libraryMessage + } +} + +struct SharedMediaTranscriptionFeatureSnapshot: Equatable, Sendable { + let route: MediaTranscriptionRoute + let selectedRecordID: UUID? + let selectedFolderID: UUID? + let currentJob: MediaTranscriptionJobState? + let setupIssue: String? + let libraryMessage: String? +} + +enum KMPMediaTranscriptionBridge { + static func beginJob( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + job: MediaTranscriptionJobState + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + let machine = MediaTranscriptionJobStateMachine.shared + let transformed = machine.beginJob( + snapshot: coreSnapshot(from: snapshot), + job: coreJob(from: job) + ) + return makeSharedSnapshot(from: transformed, currentJob: job) + #else + SharedMediaTranscriptionFeatureSnapshot( + route: .processing(job.id), + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: job, + setupIssue: nil, + libraryMessage: nil + ) + #endif + } + + static func updateJob( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + stage: MediaTranscriptionStage, + progress: Double?, + detail: String?, + errorMessage: String? + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.updateJob( + snapshot: coreSnapshot, + update: JobProgressUpdate( + stage: coreStage(from: stage), + progress: progress.map(KotlinDouble.init(value:)), + detail: detail, + errorMessage: errorMessage + ) + ) + } + #else + guard var job = snapshot.currentJob else { return snapshot } + job.stage = stage + job.progress = progress + if let detail { + job.detail = detail + } + job.errorMessage = errorMessage + return SharedMediaTranscriptionFeatureSnapshot( + route: snapshot.route, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: job, + setupIssue: snapshot.setupIssue, + libraryMessage: snapshot.libraryMessage + ) + #endif + } + + static func completeCurrentJob( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + recordID: UUID, + shouldNavigateToDetail: Bool + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.completeCurrentJob( + snapshot: coreSnapshot, + recordId: recordID.uuidString, + shouldNavigateToDetail: shouldNavigateToDetail + ) + } + #else + let updated = updateJob( + snapshot: snapshot, + stage: .completed, + progress: 1.0, + detail: "Saved transcription", + errorMessage: nil + ) + return SharedMediaTranscriptionFeatureSnapshot( + route: shouldNavigateToDetail ? .detail(recordID) : .library, + selectedRecordID: recordID, + selectedFolderID: updated.selectedFolderID, + currentJob: updated.currentJob, + setupIssue: updated.setupIssue, + libraryMessage: shouldNavigateToDetail ? nil : "Transcription finished." + ) + #endif + } + + static func failCurrentJob( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + message: String, + returnToLibrary: Bool + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.failCurrentJob( + snapshot: coreSnapshot, + message: message, + returnToLibrary: returnToLibrary + ) + } + #else + let updated = updateJob( + snapshot: snapshot, + stage: .failed, + progress: nil, + detail: nil, + errorMessage: message + ) + guard returnToLibrary else { return updated } + return SharedMediaTranscriptionFeatureSnapshot( + route: .library, + selectedRecordID: updated.selectedRecordID, + selectedFolderID: updated.selectedFolderID, + currentJob: updated.currentJob, + setupIssue: updated.setupIssue, + libraryMessage: message + ) + #endif + } + +} + +#if canImport(PindropSharedTranscription) +private extension KMPMediaTranscriptionBridge { + static func apply( + _ state: SharedMediaTranscriptionFeatureSnapshot, + transform: (MediaTranscriptionJobStateMachine, MediaTranscriptionFeatureSnapshot) -> MediaTranscriptionFeatureSnapshot + ) -> SharedMediaTranscriptionFeatureSnapshot { + let machine = MediaTranscriptionJobStateMachine.shared + let transformed = transform(machine, coreSnapshot(from: state)) + return makeSharedSnapshot( + from: transformed, + currentJob: state.currentJob + ) + } + + static func coreSnapshot( + from snapshot: SharedMediaTranscriptionFeatureSnapshot + ) -> MediaTranscriptionFeatureSnapshot { + MediaTranscriptionFeatureSnapshot( + route: coreRoute(from: snapshot.route), + selectedRecordId: snapshot.selectedRecordID?.uuidString, + selectedFolderId: snapshot.selectedFolderID?.uuidString, + currentJob: snapshot.currentJob.map(coreJob(from:)), + setupIssue: snapshot.setupIssue, + libraryMessage: snapshot.libraryMessage + ) + } + + static func makeSharedSnapshot( + from coreSnapshot: MediaTranscriptionFeatureSnapshot, + currentJob existingJob: MediaTranscriptionJobState? + ) -> SharedMediaTranscriptionFeatureSnapshot { + SharedMediaTranscriptionFeatureSnapshot( + route: route(from: coreSnapshot.route, existingJob: existingJob), + selectedRecordID: coreSnapshot.selectedRecordId.flatMap(UUID.init(uuidString:)), + selectedFolderID: coreSnapshot.selectedFolderId.flatMap(UUID.init(uuidString:)), + currentJob: jobState(from: coreSnapshot.currentJob, existingJob: existingJob), + setupIssue: coreSnapshot.setupIssue, + libraryMessage: coreSnapshot.libraryMessage + ) + } + + static func coreRoute(from route: MediaTranscriptionRoute) -> PindropSharedTranscription.MediaTranscriptionRoute { + switch route { + case .library: + return PindropSharedTranscription.MediaTranscriptionRoute.Library() + case .processing(let jobID): + return PindropSharedTranscription.MediaTranscriptionRoute.Processing(jobId: jobID.uuidString) + case .detail(let recordID): + return PindropSharedTranscription.MediaTranscriptionRoute.Detail(recordId: recordID.uuidString) + } + } + + static func route( + from coreRoute: PindropSharedTranscription.MediaTranscriptionRoute, + existingJob: MediaTranscriptionJobState? + ) -> MediaTranscriptionRoute { + if coreRoute is PindropSharedTranscription.MediaTranscriptionRoute.Library { + return .library + } + if let processing = coreRoute as? PindropSharedTranscription.MediaTranscriptionRoute.Processing, + let id = UUID(uuidString: processing.jobId) { + return .processing(id) + } + if let detail = coreRoute as? PindropSharedTranscription.MediaTranscriptionRoute.Detail, + let id = UUID(uuidString: detail.recordId) { + return .detail(id) + } + if let existingJob { + return .processing(existingJob.id) + } + return .library + } + + static func coreJob(from job: MediaTranscriptionJobState) -> PindropSharedTranscription.MediaTranscriptionJob { + PindropSharedTranscription.MediaTranscriptionJob( + id: job.id.uuidString, + requestDisplayName: job.request.displayName, + stage: coreStage(from: job.stage), + progress: job.progress.map(KotlinDouble.init(value:)), + detail: job.detail, + errorMessage: job.errorMessage + ) + } + + static func jobState( + from coreJob: PindropSharedTranscription.MediaTranscriptionJob?, + existingJob: MediaTranscriptionJobState? + ) -> MediaTranscriptionJobState? { + guard let coreJob else { return nil } + guard var existingJob else { + return nil + } + + existingJob.stage = stage(from: coreJob.stage) + existingJob.progress = coreJob.progress?.doubleValue + existingJob.detail = coreJob.detail + existingJob.errorMessage = coreJob.errorMessage + return existingJob + } + + static func coreStage(from stage: MediaTranscriptionStage) -> PindropSharedTranscription.MediaTranscriptionStage { + switch stage { + case .preflight: + .preflight + case .importing: + .importing + case .downloading: + .downloading + case .preparingAudio: + .preparingAudio + case .transcribing: + .transcribing + case .saving: + .saving + case .completed: + .completed + case .failed: + .failed + } + } + + static func stage(from stage: PindropSharedTranscription.MediaTranscriptionStage) -> MediaTranscriptionStage { + switch stage { + case .preflight: + .preflight + case .importing: + .importing + case .downloading: + .downloading + case .preparingAudio: + .preparingAudio + case .transcribing: + .transcribing + case .saving: + .saving + case .completed: + .completed + case .failed: + .failed + default: + .preflight + } + } } +#endif diff --git a/Pindrop/Services/ModelManager.swift b/Pindrop/Services/ModelManager.swift index 5e871a1..9cf0cde 100644 --- a/Pindrop/Services/ModelManager.swift +++ b/Pindrop/Services/ModelManager.swift @@ -40,10 +40,7 @@ class ModelManager { case groq = "Groq" var isLocal: Bool { - switch self { - case .whisperKit, .parakeet: return true - case .openAI, .elevenLabs, .groq: return false - } + TranscriptionPolicy.providerSupportsLocalModelLoading(self) } var iconName: String { @@ -79,21 +76,7 @@ class ModelManager { } func supports(_ language: AppLanguage) -> Bool { - guard language != .automatic else { return true } - - switch self { - case .englishOnly: - return language.isEnglish - case .fullMultilingual: - return true - case .parakeetV3European: - switch language { - case .automatic, .english, .spanish, .french, .german, .portugueseBrazil, .italian, .dutch, .turkish: - return true - case .simplifiedChinese, .japanese, .korean: - return false - } - } + TranscriptionPolicy.modelSupportsLanguage(self, language: language) } var badgeText: String { @@ -206,347 +189,13 @@ class ModelManager { } } - let availableModels: [WhisperModel] = [ - // WhisperKit Local Models - WhisperModel( - name: "openai_whisper-tiny", - displayName: "Whisper Tiny", - sizeInMB: 75, - description: "Fastest model, ideal for quick dictation with acceptable accuracy", - speedRating: 10.0, - accuracyRating: 6.0, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-tiny.en", - displayName: "Whisper Tiny (English)", - sizeInMB: 75, - description: "English-optimized tiny model with slightly better accuracy", - speedRating: 10.0, - accuracyRating: 6.5, - language: .english - ), - WhisperModel( - name: "openai_whisper-base", - displayName: "Whisper Base", - sizeInMB: 145, - description: "Good balance between speed and accuracy for everyday use", - speedRating: 9.0, - accuracyRating: 7.0, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-base.en", - displayName: "Whisper Base (English)", - sizeInMB: 145, - description: "English-optimized base model, recommended for most users", - speedRating: 9.0, - accuracyRating: 7.5, - language: .english - ), - WhisperModel( - name: "openai_whisper-small", - displayName: "Whisper Small", - sizeInMB: 483, - description: "Higher accuracy for complex vocabulary and technical terms", - speedRating: 7.5, - accuracyRating: 8.0, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-small_216MB", - displayName: "Whisper Small (Quantized)", - sizeInMB: 216, - description: "Quantized small model — half the size with similar accuracy", - speedRating: 8.0, - accuracyRating: 7.8, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-small.en", - displayName: "Whisper Small (English)", - sizeInMB: 483, - description: "English-optimized with excellent accuracy for professional use", - speedRating: 7.5, - accuracyRating: 8.5, - language: .english - ), - WhisperModel( - name: "openai_whisper-small.en_217MB", - displayName: "Whisper Small (English, Quantized)", - sizeInMB: 217, - description: "Quantized English small model — compact and fast", - speedRating: 8.0, - accuracyRating: 8.3, - language: .english - ), - WhisperModel( - name: "openai_whisper-medium", - displayName: "Whisper Medium", - sizeInMB: 1530, - description: "Excellent for multilingual and code-switching (e.g. Chinese/English mix)", - speedRating: 6.5, - accuracyRating: 8.8, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-medium.en", - displayName: "Whisper Medium (English)", - sizeInMB: 1530, - description: "English-optimized medium model with high accuracy", - speedRating: 6.5, - accuracyRating: 9.0, - language: .english - ), - WhisperModel( - name: "openai_whisper-large-v2", - displayName: "Whisper Large v2", - sizeInMB: 3100, - description: "Previous generation large model, still very capable", - speedRating: 5.0, - accuracyRating: 9.3, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v2_949MB", - displayName: "Whisper Large v2 (Quantized)", - sizeInMB: 949, - description: "Quantized large v2 — much smaller with minimal accuracy loss", - speedRating: 6.0, - accuracyRating: 9.1, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v2_turbo", - displayName: "Whisper Large v2 Turbo", - sizeInMB: 3100, - description: "Turbo-optimized large v2 for faster inference", - speedRating: 6.5, - accuracyRating: 9.3, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v2_turbo_955MB", - displayName: "Whisper Large v2 Turbo (Quantized)", - sizeInMB: 955, - description: "Quantized turbo large v2 — fast and compact", - speedRating: 7.0, - accuracyRating: 9.1, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3", - displayName: "Whisper Large v3", - sizeInMB: 3100, - description: "Maximum accuracy for demanding transcription tasks", - speedRating: 5.0, - accuracyRating: 9.7, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3_947MB", - displayName: "Whisper Large v3 (Quantized)", - sizeInMB: 947, - description: "Quantized large v3 — great accuracy in a smaller package", - speedRating: 6.0, - accuracyRating: 9.5, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3_turbo", - displayName: "Whisper Large v3 Turbo", - sizeInMB: 809, - description: "Near large-model accuracy with significantly faster processing", - speedRating: 7.5, - accuracyRating: 9.5, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3_turbo_954MB", - displayName: "Whisper Large v3 Turbo (Quantized)", - sizeInMB: 954, - description: "Quantized turbo v3 — balanced speed and accuracy", - speedRating: 7.5, - accuracyRating: 9.3, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3-v20240930", - displayName: "Whisper Large v3 (Sep 2024)", - sizeInMB: 3100, - description: "Updated large v3 with improved multilingual performance", - speedRating: 5.0, - accuracyRating: 9.8, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3-v20240930_547MB", - displayName: "Whisper Large v3 Sep 2024 (Q 547MB)", - sizeInMB: 547, - description: "Heavily quantized — smallest large v3 variant", - speedRating: 7.0, - accuracyRating: 9.3, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3-v20240930_626MB", - displayName: "Whisper Large v3 Sep 2024 (Q 626MB)", - sizeInMB: 626, - description: "Quantized Sep 2024 large v3 — compact with great accuracy", - speedRating: 6.5, - accuracyRating: 9.5, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3-v20240930_turbo", - displayName: "Whisper Large v3 Sep 2024 Turbo", - sizeInMB: 3100, - description: "Latest turbo-optimized large v3 — best overall performance", - speedRating: 6.5, - accuracyRating: 9.8, - language: .multilingual - ), - WhisperModel( - name: "openai_whisper-large-v3-v20240930_turbo_632MB", - displayName: "Whisper Large v3 Sep 2024 Turbo (Quantized)", - sizeInMB: 632, - description: "Quantized latest turbo — excellent accuracy in ~600MB", - speedRating: 7.5, - accuracyRating: 9.5, - language: .multilingual - ), - - // Distil-Whisper Models (distilled from large v3) - WhisperModel( - name: "distil-whisper_distil-large-v3", - displayName: "Distil Large v3", - sizeInMB: 1510, - description: "Distilled large v3 — faster with minimal accuracy loss", - speedRating: 7.5, - accuracyRating: 9.3, - language: .multilingual - ), - WhisperModel( - name: "distil-whisper_distil-large-v3_594MB", - displayName: "Distil Large v3 (Quantized)", - sizeInMB: 594, - description: "Quantized distilled model — great speed/accuracy tradeoff", - speedRating: 8.0, - accuracyRating: 9.0, - language: .multilingual - ), - WhisperModel( - name: "distil-whisper_distil-large-v3_turbo", - displayName: "Distil Large v3 Turbo", - sizeInMB: 1510, - description: "Turbo-optimized distilled model for fastest large-class inference", - speedRating: 8.0, - accuracyRating: 9.3, - language: .multilingual - ), - WhisperModel( - name: "distil-whisper_distil-large-v3_turbo_600MB", - displayName: "Distil Large v3 Turbo (Quantized)", - sizeInMB: 600, - description: "Quantized turbo distilled — fastest large-class model at ~600MB", - speedRating: 8.5, - accuracyRating: 9.0, - language: .multilingual - ), - - // Parakeet Models (via FluidInference CoreML ports) - WhisperModel( - name: "parakeet-tdt-0.6b-v2", - displayName: "Parakeet TDT 0.6B V2", - sizeInMB: 2580, - description: "NVIDIA's state-of-the-art speech recognition model, English-only", - speedRating: 8.5, - accuracyRating: 9.8, - language: .english, - provider: .parakeet, - availability: .available - ), - WhisperModel( - name: "parakeet-tdt-0.6b-v3", - displayName: "Parakeet TDT 0.6B V3", - sizeInMB: 2670, - description: "Latest Parakeet model with multilingual support", - speedRating: 8.0, - accuracyRating: 9.9, - language: .multilingual, - languageSupport: .parakeetV3European, - provider: .parakeet, - availability: .available - ), - WhisperModel( - name: "parakeet-tdt-1.1b", - displayName: "Parakeet TDT 1.1B", - sizeInMB: 4400, - description: "Larger Parakeet model with exceptional accuracy", - speedRating: 7.0, - accuracyRating: 9.95, - language: .english, - provider: .parakeet, - availability: .comingSoon - ), - - // Coming Soon - Cloud Providers - WhisperModel( - name: "openai_whisper-1", - displayName: "OpenAI Whisper API", - sizeInMB: 0, - description: "Cloud-based transcription via OpenAI's API", - speedRating: 9.0, - accuracyRating: 9.5, - language: .multilingual, - provider: .openAI, - availability: .comingSoon - ), - WhisperModel( - name: "groq_whisper-large-v3-turbo", - displayName: "Whisper Large v3 Turbo (Groq)", - sizeInMB: 0, - description: "Lightning-fast cloud inference powered by Groq", - speedRating: 10.0, - accuracyRating: 9.5, - language: .multilingual, - provider: .groq, - availability: .comingSoon - ), - WhisperModel( - name: "elevenlabs_scribe", - displayName: "ElevenLabs Scribe", - sizeInMB: 0, - description: "High-quality transcription with speaker diarization", - speedRating: 8.0, - accuracyRating: 9.3, - language: .multilingual, - provider: .elevenLabs, - availability: .comingSoon - ) - ] + let availableModels: [WhisperModel] = TranscriptionModelCatalog.availableModels func recommendedModels(for language: AppLanguage) -> [WhisperModel] { - let recommendedModelNames: [String] - switch language { - case .english: - recommendedModelNames = Self.englishRecommendedModelNames - case .automatic, .simplifiedChinese, .spanish, .french, .german, .turkish, .japanese, .portugueseBrazil, .italian, .dutch, .korean: - recommendedModelNames = Self.multilingualRecommendedModelNames - } - - let recommendationRanks = Dictionary( - uniqueKeysWithValues: recommendedModelNames.enumerated().map { index, name in - (name, index) - } + TranscriptionModelCatalog.recommendedModels( + availableModels: availableModels, + for: language ) - - return availableModels - .filter { recommendedModelNames.contains($0.name) } - .filter { $0.supports(language: language) } - .sorted { - recommendationRanks[$0.name, default: .max] < recommendationRanks[$1.name, default: .max] - } } var recommendedModels: [WhisperModel] { @@ -923,3 +572,5 @@ class ModelManager { } } } + +extension ModelManager: ModelCatalogProviding {} diff --git a/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift b/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift new file mode 100644 index 0000000..9750050 --- /dev/null +++ b/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift @@ -0,0 +1,813 @@ +// +// KMPTranscriptionBridge.swift +// Pindrop +// +// Created on 2026-03-28. +// + +import Foundation + +#if canImport(PindropSharedTranscription) +import PindropSharedTranscription +#endif + +struct SharedTranscriptionSessionPlan: Equatable, Sendable { + let useStreaming: Bool + let useSpeakerDiarization: Bool +} + +struct SharedModelLoadPlan: Equatable, Sendable { + let resolvedProvider: ModelManager.ModelProvider + let shouldUnloadCurrentModel: Bool + let supportsLocalModelLoading: Bool + let prefersPathBasedLoading: Bool +} + +struct SharedTranscriptionExecutionPlan: Equatable, Sendable { + let selectedProvider: ModelManager.ModelProvider + let selectedModelId: String + let useSpeakerDiarization: Bool + let shouldNormalizeOutput: Bool +} + +enum SharedStartupModelAction: Sendable { + case loadSelected + case loadFallback + case downloadSelected +} + +struct SharedStartupModelResolution: Sendable { + let action: SharedStartupModelAction + let resolvedModel: ModelManager.WhisperModel + let updatedSelectedModelId: String +} + +enum SharedEventTapRecoveryAction: Sendable { + case reenable + case recreate +} + +struct SharedEventTapRecoveryDecision: Sendable, Equatable { + let consecutiveDisableCount: Int + let action: SharedEventTapRecoveryAction +} + +enum SharedTranscriptionSessionErrorCode: Sendable { + case engineSwitchDuringTranscription + case modelNotLoaded + case invalidAudioData + case transcriptionAlreadyInProgress + case streamingNotReady +} + +struct SharedTranscriptionStateTransition: Equatable, Sendable { + let nextState: TranscriptionService.State + let errorCode: SharedTranscriptionSessionErrorCode? +} + +enum KMPTranscriptionBridge { + static func normalizeTranscriptionText(_ text: String) -> String { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.normalizeTranscriptionText(text: text) + #else + text.trimmingCharacters(in: .whitespacesAndNewlines) + #endif + } + + static func isTranscriptionEffectivelyEmpty(_ text: String) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.isTranscriptionEffectivelyEmpty(text: text) + #else + let normalizedText = normalizeTranscriptionText(text) + if normalizedText.isEmpty { + return true + } + + return normalizedText.caseInsensitiveCompare("[BLANK AUDIO]") == .orderedSame + #endif + } + + static func shouldPersistHistory(outputSucceeded: Bool, text: String) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.shouldPersistHistory( + outputSucceeded: outputSucceeded, + text: text + ) + #else + outputSucceeded && !isTranscriptionEffectivelyEmpty(text) + #endif + } + + static func shouldUseStreamingTranscription( + streamingFeatureEnabled: Bool, + outputMode: OutputMode, + aiEnhancementEnabled: Bool, + isQuickCaptureMode: Bool + ) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.shouldUseStreamingTranscription( + streamingFeatureEnabled: streamingFeatureEnabled, + outputMode: outputMode.kmpValue, + aiEnhancementEnabled: aiEnhancementEnabled, + isQuickCaptureMode: isQuickCaptureMode + ) + #else + streamingFeatureEnabled && + outputMode == .directInsert && + !aiEnhancementEnabled && + !isQuickCaptureMode + #endif + } + + static func shouldUseSpeakerDiarization( + diarizationFeatureEnabled: Bool, + isStreamingSessionActive: Bool + ) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.shouldUseSpeakerDiarization( + diarizationFeatureEnabled: diarizationFeatureEnabled, + isStreamingSessionActive: isStreamingSessionActive + ) + #else + diarizationFeatureEnabled && !isStreamingSessionActive + #endif + } + + static func providerSupportsLocalModelLoading( + _ provider: ModelManager.ModelProvider + ) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.providerSupportsLocalLoading( + provider: coreProvider(from: provider) + ) + #else + provider == .whisperKit || provider == .parakeet + #endif + } + + static func modelSupportsLanguage( + _ support: ModelManager.LanguageSupport, + language: AppLanguage + ) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.supportsLanguage( + support: coreLanguageSupport(from: support), + language: coreLanguage(from: language) + ) + #else + if language == .automatic { + return true + } + + switch support { + case .englishOnly: + return language == .english + case .fullMultilingual: + return true + case .parakeetV3European: + switch language { + case .automatic, .english, .spanish, .french, .german, .portugueseBrazil, .italian, .dutch, .turkish: + return true + case .simplifiedChinese, .japanese, .korean: + return false + } + } + #endif + } + + static func planSession( + selectedProvider: ModelManager.ModelProvider, + selectedModelName: String, + streamingFeatureEnabled: Bool, + diarizationFeatureEnabled: Bool, + outputMode: OutputMode, + aiEnhancementEnabled: Bool, + isQuickCaptureMode: Bool + ) -> SharedTranscriptionSessionPlan { + #if canImport(PindropSharedTranscription) + let policy = TranscriptionRuntimePolicy( + selectedProvider: coreProvider(from: selectedProvider), + selectedModelId: CoreTranscriptionModelId(value: selectedModelName), + streamingFeatureEnabled: streamingFeatureEnabled, + diarizationFeatureEnabled: diarizationFeatureEnabled, + outputMode: outputMode.kmpValue, + aiEnhancementEnabled: aiEnhancementEnabled, + isQuickCaptureMode: isQuickCaptureMode + ) + + let plan = SharedTranscriptionOrchestrator.shared.planSession(policy: policy) + return SharedTranscriptionSessionPlan( + useStreaming: plan.useStreaming, + useSpeakerDiarization: plan.useSpeakerDiarization + ) + #else + let useStreaming = shouldUseStreamingTranscription( + streamingFeatureEnabled: streamingFeatureEnabled, + outputMode: outputMode, + aiEnhancementEnabled: aiEnhancementEnabled, + isQuickCaptureMode: isQuickCaptureMode + ) + + return SharedTranscriptionSessionPlan( + useStreaming: useStreaming, + useSpeakerDiarization: shouldUseSpeakerDiarization( + diarizationFeatureEnabled: diarizationFeatureEnabled, + isStreamingSessionActive: useStreaming + ) + ) + #endif + } + + static func planModelLoad( + requestedProvider: ModelManager.ModelProvider, + currentProvider: ModelManager.ModelProvider?, + loadsFromPath: Bool + ) -> SharedModelLoadPlan { + #if canImport(PindropSharedTranscription) + let plan = SharedTranscriptionOrchestrator.shared.planModelLoad( + requestedProvider: coreProvider(from: requestedProvider), + currentProvider: currentProvider.map { coreProvider(from: $0) }, + loadsFromPath: loadsFromPath + ) + + return SharedModelLoadPlan( + resolvedProvider: modelProvider(from: plan.resolvedProvider), + shouldUnloadCurrentModel: plan.shouldUnloadCurrentModel, + supportsLocalModelLoading: plan.supportsLocalModelLoading, + prefersPathBasedLoading: plan.prefersPathBasedLoading + ) + #else + let resolvedProvider: ModelManager.ModelProvider = loadsFromPath ? .whisperKit : requestedProvider + return SharedModelLoadPlan( + resolvedProvider: resolvedProvider, + shouldUnloadCurrentModel: currentProvider != nil && currentProvider != resolvedProvider, + supportsLocalModelLoading: resolvedProvider.isLocal, + prefersPathBasedLoading: loadsFromPath && resolvedProvider == .whisperKit + ) + #endif + } + + static func planTranscriptionExecution( + selectedProvider: ModelManager.ModelProvider, + selectedModelName: String, + diarizationRequested: Bool, + isStreamingSessionActive: Bool + ) -> SharedTranscriptionExecutionPlan { + #if canImport(PindropSharedTranscription) + let plan = SharedTranscriptionOrchestrator.shared.planTranscriptionExecution( + selectedProvider: coreProvider(from: selectedProvider), + selectedModelId: CoreTranscriptionModelId(value: selectedModelName), + diarizationRequested: diarizationRequested, + isStreamingSessionActive: isStreamingSessionActive + ) + + return SharedTranscriptionExecutionPlan( + selectedProvider: modelProvider(from: plan.selectedProvider), + selectedModelId: plan.selectedModelId.value, + useSpeakerDiarization: plan.useSpeakerDiarization, + shouldNormalizeOutput: plan.shouldNormalizeOutput + ) + #else + SharedTranscriptionExecutionPlan( + selectedProvider: selectedProvider, + selectedModelId: selectedModelName, + useSpeakerDiarization: diarizationRequested && !isStreamingSessionActive, + shouldNormalizeOutput: true + ) + #endif + } + + static func beginModelLoad( + currentState: TranscriptionService.State + ) -> SharedTranscriptionStateTransition { + #if canImport(PindropSharedTranscription) + let transition = SharedTranscriptionOrchestrator.shared.beginModelLoad( + currentState: coreState(from: currentState) + ) + return stateTransition(from: transition) + #else + if currentState == .transcribing { + return SharedTranscriptionStateTransition( + nextState: currentState, + errorCode: .engineSwitchDuringTranscription + ) + } + + return SharedTranscriptionStateTransition(nextState: .loading, errorCode: nil) + #endif + } + + static func completeModelLoad(success: Bool) -> TranscriptionService.State { + #if canImport(PindropSharedTranscription) + serviceState( + from: SharedTranscriptionOrchestrator.shared.completeModelLoad(success: success) + ) + #else + success ? .ready : .error + #endif + } + + static func beginBatchTranscription( + currentState: TranscriptionService.State, + hasLoadedModel: Bool, + audioByteCount: Int + ) -> SharedTranscriptionStateTransition { + #if canImport(PindropSharedTranscription) + let transition = SharedTranscriptionOrchestrator.shared.beginBatchTranscription( + currentState: coreState(from: currentState), + hasLoadedModel: hasLoadedModel, + audioByteCount: Int32(audioByteCount) + ) + return stateTransition(from: transition) + #else + if !hasLoadedModel { + return SharedTranscriptionStateTransition(nextState: currentState, errorCode: .modelNotLoaded) + } + if audioByteCount <= 0 { + return SharedTranscriptionStateTransition(nextState: currentState, errorCode: .invalidAudioData) + } + if currentState == .transcribing { + return SharedTranscriptionStateTransition(nextState: currentState, errorCode: .transcriptionAlreadyInProgress) + } + + return SharedTranscriptionStateTransition(nextState: .transcribing, errorCode: nil) + #endif + } + + static func completeBatchTranscription() -> TranscriptionService.State { + #if canImport(PindropSharedTranscription) + serviceState( + from: SharedTranscriptionOrchestrator.shared.completeBatchTranscription() + ) + #else + .ready + #endif + } + + static func stateAfterUnload() -> TranscriptionService.State { + #if canImport(PindropSharedTranscription) + serviceState( + from: SharedTranscriptionOrchestrator.shared.stateAfterUnload() + ) + #else + .unloaded + #endif + } + + static func stateAfterStreamingPrepared( + currentState: TranscriptionService.State + ) -> TranscriptionService.State { + #if canImport(PindropSharedTranscription) + serviceState( + from: SharedTranscriptionOrchestrator.shared.stateAfterStreamingPrepared( + currentState: coreState(from: currentState) + ) + ) + #else + switch currentState { + case .unloaded, .error: + .ready + case .loading, .ready, .transcribing: + currentState + } + #endif + } + + static func failStreamingPreparation() -> TranscriptionService.State { + #if canImport(PindropSharedTranscription) + serviceState( + from: SharedTranscriptionOrchestrator.shared.failStreamingPreparation() + ) + #else + .error + #endif + } + + static func beginStreaming( + currentState: TranscriptionService.State, + hasPreparedStreamingEngine: Bool + ) -> SharedTranscriptionStateTransition { + #if canImport(PindropSharedTranscription) + let transition = SharedTranscriptionOrchestrator.shared.beginStreaming( + currentState: coreState(from: currentState), + hasPreparedStreamingEngine: hasPreparedStreamingEngine + ) + return stateTransition(from: transition) + #else + if currentState == .transcribing { + return SharedTranscriptionStateTransition(nextState: currentState, errorCode: .transcriptionAlreadyInProgress) + } + if !hasPreparedStreamingEngine { + return SharedTranscriptionStateTransition(nextState: currentState, errorCode: .streamingNotReady) + } + + return SharedTranscriptionStateTransition(nextState: .transcribing, errorCode: nil) + #endif + } + + static func validateStreamingAudio( + isStreamingSessionActive: Bool + ) -> SharedTranscriptionSessionErrorCode? { + #if canImport(PindropSharedTranscription) + sessionErrorCode( + from: SharedTranscriptionOrchestrator.shared.validateStreamingAudio( + isStreamingSessionActive: isStreamingSessionActive + ) + ) + #else + isStreamingSessionActive ? nil : .streamingNotReady + #endif + } + + static func completeStreamingSession( + hasLoadedModel: Bool, + hasPreparedStreamingEngine: Bool + ) -> TranscriptionService.State { + #if canImport(PindropSharedTranscription) + serviceState( + from: SharedTranscriptionOrchestrator.shared.completeStreamingSession( + hasLoadedModel: hasLoadedModel, + hasPreparedStreamingEngine: hasPreparedStreamingEngine + ) + ) + #else + (hasLoadedModel || hasPreparedStreamingEngine) ? .ready : .unloaded + #endif + } + + static func recommendedModels( + availableModels: [ModelManager.WhisperModel], + for language: AppLanguage + ) -> [ModelManager.WhisperModel] { + #if canImport(PindropSharedTranscription) + let orchestrator = SharedTranscriptionOrchestrator.shared + let curatedIds = recommendedModelNames(for: language).map { CoreTranscriptionModelId(value: $0) } + let descriptors = availableModels.map(coreDescriptor(from:)) + let language = coreLanguage(from: language) + + let orderedDescriptors = orchestrator.recommendedModels( + allModels: descriptors, + curatedIds: curatedIds, + language: language + ) + + let modelsByName = Dictionary(uniqueKeysWithValues: availableModels.map { ($0.name, $0) }) + return orderedDescriptors.compactMap { modelsByName[$0.id.value] } + #else + let recommendedModelNames = recommendedModelNames(for: language) + let recommendationRanks = Dictionary( + uniqueKeysWithValues: recommendedModelNames.enumerated().map { index, name in + (name, index) + } + ) + + return availableModels + .filter { recommendedModelNames.contains($0.name) } + .filter { $0.supports(language: language) } + .sorted { + recommendationRanks[$0.name, default: .max] < recommendationRanks[$1.name, default: .max] + } + #endif + } + + static func resolveStartupModel( + selectedModelId: String, + defaultModelId: String, + availableModels: [ModelManager.WhisperModel], + downloadedModelIds: [String] + ) -> SharedStartupModelResolution { + #if canImport(PindropSharedTranscription) + let orchestrator = SharedTranscriptionOrchestrator.shared + let descriptors = availableModels.map(coreDescriptor(from:)) + let modelsByName = Dictionary(uniqueKeysWithValues: availableModels.map { ($0.name, $0) }) + + let resolution = orchestrator.resolveStartupModel( + selectedModelId: CoreTranscriptionModelId(value: selectedModelId), + defaultModelId: CoreTranscriptionModelId(value: defaultModelId), + availableModels: descriptors, + downloadedModelIds: downloadedModelIds.map(CoreTranscriptionModelId.init(value:)) + ) + + let resolvedModel = modelsByName[resolution.resolvedModel.id.value] ?? availableModels.first! + return SharedStartupModelResolution( + action: startupAction(from: resolution.action), + resolvedModel: resolvedModel, + updatedSelectedModelId: resolution.updatedSelectedModelId.value + ) + #else + let selectedModel = availableModels.first(where: { $0.name == selectedModelId }) + ?? availableModels.first(where: { $0.name == defaultModelId }) + ?? availableModels.first! + + if downloadedModelIds.contains(selectedModel.name) { + return SharedStartupModelResolution( + action: .loadSelected, + resolvedModel: selectedModel, + updatedSelectedModelId: selectedModel.name + ) + } + + if let fallbackModel = availableModels.first(where: { downloadedModelIds.contains($0.name) }) { + return SharedStartupModelResolution( + action: .loadFallback, + resolvedModel: fallbackModel, + updatedSelectedModelId: fallbackModel.name + ) + } + + return SharedStartupModelResolution( + action: .downloadSelected, + resolvedModel: selectedModel, + updatedSelectedModelId: selectedModel.name + ) + #endif + } + + static func determineEventTapRecovery( + elapsedSinceLastDisable: TimeInterval?, + consecutiveDisableCount: Int, + disableLoopWindow: TimeInterval, + maxReenableAttemptsBeforeRecreate: Int + ) -> SharedEventTapRecoveryDecision { + #if canImport(PindropSharedTranscription) + let decision = SharedTranscriptionOrchestrator.shared.determineEventTapRecovery( + elapsedSinceLastDisableSeconds: elapsedSinceLastDisable.map(NSNumber.init(value:)), + consecutiveDisableCount: Int32(consecutiveDisableCount), + disableLoopWindowSeconds: disableLoopWindow, + maxReenableAttemptsBeforeRecreate: Int32(maxReenableAttemptsBeforeRecreate) + ) + + return SharedEventTapRecoveryDecision( + consecutiveDisableCount: Int(decision.consecutiveDisableCount), + action: eventTapRecoveryAction(from: decision.action) + ) + #else + let nextCount: Int + if let elapsedSinceLastDisable, elapsedSinceLastDisable <= disableLoopWindow { + nextCount = consecutiveDisableCount + 1 + } else { + nextCount = 1 + } + + return SharedEventTapRecoveryDecision( + consecutiveDisableCount: nextCount, + action: nextCount >= max(1, maxReenableAttemptsBeforeRecreate) ? .recreate : .reenable + ) + #endif + } + + static func shouldRunLiveContextSession( + aiEnhancementEnabled: Bool, + uiContextEnabled: Bool, + liveSessionEnabled: Bool + ) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.shouldRunLiveContextSession( + aiEnhancementEnabled: aiEnhancementEnabled, + uiContextEnabled: uiContextEnabled, + liveSessionEnabled: liveSessionEnabled + ) + #else + aiEnhancementEnabled && uiContextEnabled && liveSessionEnabled + #endif + } + + static func shouldAppendTransition( + signature: String, + trigger: String, + lastSignature: String? + ) -> Bool { + #if canImport(PindropSharedTranscription) + SharedTranscriptionOrchestrator.shared.shouldAppendTransition( + signature: signature, + trigger: trigger, + lastSignature: lastSignature + ) + #else + trigger == "recordingStart" || lastSignature == nil || lastSignature != signature + #endif + } + + private static func recommendedModelNames(for language: AppLanguage) -> [String] { + switch language { + case .english: + ModelManager.englishRecommendedModelNames + case .automatic, + .simplifiedChinese, + .spanish, + .french, + .german, + .turkish, + .japanese, + .portugueseBrazil, + .italian, + .dutch, + .korean: + ModelManager.multilingualRecommendedModelNames + } + } +} + +#if canImport(PindropSharedTranscription) +private extension KMPTranscriptionBridge { + static func coreProvider(from provider: ModelManager.ModelProvider) -> CoreTranscriptionProviderId { + switch provider { + case .whisperKit: + .whisperKit + case .parakeet: + .parakeet + case .openAI: + .openAi + case .elevenLabs: + .elevenLabs + case .groq: + .groq + } + } + + static func modelProvider(from provider: CoreTranscriptionProviderId) -> ModelManager.ModelProvider { + switch provider { + case .whisperKit: + .whisperKit + case .parakeet: + .parakeet + case .openAi: + .openAI + case .elevenLabs: + .elevenLabs + case .groq: + .groq + default: + .whisperKit + } + } + + static func coreLanguage(from language: AppLanguage) -> CoreTranscriptionLanguage { + switch language { + case .automatic: + .automatic + case .english: + .english + case .simplifiedChinese: + .simplifiedChinese + case .spanish: + .spanish + case .french: + .french + case .german: + .german + case .turkish: + .turkish + case .japanese: + .japanese + case .portugueseBrazil: + .portugueseBrazil + case .italian: + .italian + case .dutch: + .dutch + case .korean: + .korean + } + } + + static func coreLanguageSupport( + from support: ModelManager.LanguageSupport + ) -> CoreModelLanguageSupport { + switch support { + case .englishOnly: + .englishOnly + case .fullMultilingual: + .fullMultilingual + case .parakeetV3European: + .parakeetV3European + } + } + + static func coreAvailability( + from availability: ModelManager.ModelAvailability + ) -> CoreModelAvailability { + switch availability { + case .available: + .available + case .comingSoon: + .comingSoon + case .requiresSetup: + .requiresSetup + } + } + + static func coreDescriptor(from model: ModelManager.WhisperModel) -> CoreModelDescriptor { + CoreModelDescriptor( + id: CoreTranscriptionModelId(value: model.name), + displayName: model.displayName, + provider: coreProvider(from: model.provider), + languageSupport: coreLanguageSupport(from: model.languageSupport), + sizeInMb: Int32(model.sizeInMB), + description: model.description, + speedRating: model.speedRating, + accuracyRating: model.accuracyRating, + availability: coreAvailability(from: model.availability) + ) + } + + static func coreState(from state: TranscriptionService.State) -> CoreSharedTranscriptionState { + switch state { + case .unloaded: + .unloaded + case .loading: + .loading + case .ready: + .ready + case .transcribing: + .transcribing + case .error: + .error + } + } + + static func serviceState(from state: CoreSharedTranscriptionState) -> TranscriptionService.State { + switch state { + case .unloaded: + .unloaded + case .loading: + .loading + case .ready: + .ready + case .transcribing, .streaming: + .transcribing + case .error: + .error + default: + .ready + } + } + + static func sessionErrorCode( + from errorCode: SharedSessionErrorCode? + ) -> SharedTranscriptionSessionErrorCode? { + guard let errorCode else { return nil } + + switch errorCode { + case .engineSwitchDuringTranscription: + return .engineSwitchDuringTranscription + case .modelNotLoaded: + return .modelNotLoaded + case .invalidAudioData: + return .invalidAudioData + case .transcriptionAlreadyInProgress: + return .transcriptionAlreadyInProgress + case .streamingNotReady: + return .streamingNotReady + default: + return nil + } + } + + static func stateTransition( + from transition: SharedStateTransition + ) -> SharedTranscriptionStateTransition { + SharedTranscriptionStateTransition( + nextState: serviceState(from: transition.nextState), + errorCode: sessionErrorCode(from: transition.errorCode) + ) + } + + static func startupAction(from action: StartupModelAction) -> SharedStartupModelAction { + switch action { + case .loadSelected: + .loadSelected + case .loadFallback: + .loadFallback + case .downloadSelected: + .downloadSelected + default: + .loadSelected + } + } + + static func eventTapRecoveryAction( + from action: EventTapRecoveryAction + ) -> SharedEventTapRecoveryAction { + switch action { + case .reenable: + .reenable + case .recreate: + .recreate + default: + .reenable + } + } +} + +private extension OutputMode { + var kmpValue: String { + switch self { + case .clipboard: + "clipboard" + case .directInsert: + "directInsert" + } + } +} +#endif diff --git a/Pindrop/Services/Transcription/NativeTranscriptionAdapters.swift b/Pindrop/Services/Transcription/NativeTranscriptionAdapters.swift new file mode 100644 index 0000000..55ef31d --- /dev/null +++ b/Pindrop/Services/Transcription/NativeTranscriptionAdapters.swift @@ -0,0 +1,174 @@ +// +// NativeTranscriptionAdapters.swift +// Pindrop +// +// Created on 2026-03-28. +// + +import AVFoundation +import Foundation + +@MainActor +final class WhisperKitTranscriptionAdapter: TranscriptionEnginePort { + private let engine: WhisperKitEngine + + init() { + self.engine = WhisperKitEngine() + } + + init(engine: WhisperKitEngine) { + self.engine = engine + } + + var state: TranscriptionEngineState { engine.state } + + func loadModel(path: String) async throws { + try await engine.loadModel(path: path) + } + + func loadModel(name: String, downloadBase: URL?) async throws { + try await engine.loadModel(name: name, downloadBase: downloadBase) + } + + func transcribe(audioData: Data, options: TranscriptionOptions) async throws -> String { + try await engine.transcribe(audioData: audioData, options: options) + } + + func unloadModel() async { + await engine.unloadModel() + } +} + +@MainActor +final class ParakeetTranscriptionAdapter: TranscriptionEnginePort { + private let engine: ParakeetEngine + + init() { + self.engine = ParakeetEngine() + } + + init(engine: ParakeetEngine) { + self.engine = engine + } + + var state: TranscriptionEngineState { engine.state } + + func loadModel(path: String) async throws { + try await engine.loadModel(path: path) + } + + func loadModel(name: String, downloadBase: URL?) async throws { + try await engine.loadModel(name: name, downloadBase: downloadBase) + } + + func transcribe(audioData: Data, options: TranscriptionOptions) async throws -> String { + try await engine.transcribe(audioData: audioData, options: options) + } + + func unloadModel() async { + await engine.unloadModel() + } +} + +@MainActor +final class ParakeetStreamingAdapter: StreamingTranscriptionEnginePort { + private let engine: ParakeetStreamingEngine + + init() { + self.engine = ParakeetStreamingEngine() + } + + init(engine: ParakeetStreamingEngine) { + self.engine = engine + } + + var state: StreamingTranscriptionState { engine.state } + + func loadModel(name: String) async throws { + try await engine.loadModel(name: name) + } + + func unloadModel() async { + await engine.unloadModel() + } + + func startStreaming() async throws { + try await engine.startStreaming() + } + + func stopStreaming() async throws -> String { + try await engine.stopStreaming() + } + + func pauseStreaming() async { + await engine.pauseStreaming() + } + + func resumeStreaming() async throws { + try await engine.resumeStreaming() + } + + func processAudioChunk(_ samples: [Float]) async throws { + try await engine.processAudioChunk(samples) + } + + func processAudioBuffer(_ buffer: AVAudioPCMBuffer) async throws { + try await engine.processAudioBuffer(buffer) + } + + func setTranscriptionCallback(_ callback: @escaping StreamingTranscriptionCallback) { + engine.setTranscriptionCallback(callback) + } + + func setEndOfUtteranceCallback(_ callback: @escaping EndOfUtteranceCallback) { + engine.setEndOfUtteranceCallback(callback) + } + + func reset() async { + await engine.reset() + } +} + +@MainActor +final class MacOSModelCatalogAdapter: ModelCatalogProviding { + private let modelManager: ModelManager + + init(modelManager: ModelManager) { + self.modelManager = modelManager + } + + var availableModels: [ModelManager.WhisperModel] { + modelManager.availableModels + } + + var recommendedModels: [ModelManager.WhisperModel] { + modelManager.recommendedModels + } + + func recommendedModels(for language: AppLanguage) -> [ModelManager.WhisperModel] { + modelManager.recommendedModels(for: language) + } + + func isModelDownloaded(_ modelName: String) -> Bool { + modelManager.isModelDownloaded(modelName) + } +} + +@MainActor +final class MacOSSettingsSnapshotAdapter: SettingsSnapshotProvider { + private let settingsStore: SettingsStore + + init(settingsStore: SettingsStore) { + self.settingsStore = settingsStore + } + + func transcriptionSettingsSnapshot() -> TranscriptionSettingsSnapshot { + TranscriptionSettingsSnapshot( + selectedLanguage: settingsStore.selectedAppLanguage, + selectedModelName: settingsStore.selectedModel, + aiEnhancementEnabled: settingsStore.aiEnhancementEnabled, + streamingFeatureEnabled: settingsStore.streamingFeatureEnabled, + diarizationFeatureEnabled: settingsStore.diarizationFeatureEnabled + ) + } +} diff --git a/Pindrop/Services/Transcription/SpeakerDiarizer.swift b/Pindrop/Services/Transcription/SpeakerDiarizer.swift index c82ec8d..ba531f2 100644 --- a/Pindrop/Services/Transcription/SpeakerDiarizer.swift +++ b/Pindrop/Services/Transcription/SpeakerDiarizer.swift @@ -102,21 +102,7 @@ public enum DiarizationMode: Sendable { } @MainActor -public protocol SpeakerDiarizer: AnyObject { - var state: SpeakerDiarizerState { get } - var mode: DiarizationMode { get } - - func loadModels() async throws - func unloadModels() async - - func diarize(audioData: Data) async throws -> DiarizationResult - func diarize(samples: [Float], sampleRate: Int) async throws -> DiarizationResult - - func compareSpeakers(audio1: [Float], audio2: [Float]) async throws -> Float - - func registerKnownSpeaker(_ speaker: Speaker) async throws - func clearKnownSpeakers() async -} +public protocol SpeakerDiarizer: SpeakerDiarizerPort {} extension SpeakerDiarizer { public func diarize(audioData: Data) async throws -> DiarizationResult { diff --git a/Pindrop/Services/Transcription/StreamingTranscriptionEngine.swift b/Pindrop/Services/Transcription/StreamingTranscriptionEngine.swift index 20edae8..bc8a9e3 100644 --- a/Pindrop/Services/Transcription/StreamingTranscriptionEngine.swift +++ b/Pindrop/Services/Transcription/StreamingTranscriptionEngine.swift @@ -35,24 +35,8 @@ public typealias StreamingTranscriptionCallback = @Sendable (StreamingTranscript public typealias EndOfUtteranceCallback = @Sendable (String) -> Void @MainActor -public protocol StreamingTranscriptionEngine: AnyObject { - var state: StreamingTranscriptionState { get } - - func loadModel(name: String) async throws - func unloadModel() async - - func startStreaming() async throws - func stopStreaming() async throws -> String - func pauseStreaming() async - func resumeStreaming() async throws - - func processAudioChunk(_ samples: [Float]) async throws +public protocol StreamingTranscriptionEngine: StreamingTranscriptionEnginePort { func processAudioBuffer(_ buffer: AVAudioPCMBuffer) async throws - - func setTranscriptionCallback(_ callback: @escaping StreamingTranscriptionCallback) - func setEndOfUtteranceCallback(_ callback: @escaping EndOfUtteranceCallback) - - func reset() async } extension StreamingTranscriptionEngine { diff --git a/Pindrop/Services/Transcription/TranscriptionEngine.swift b/Pindrop/Services/Transcription/TranscriptionEngine.swift index d2efe3c..f6183d2 100644 --- a/Pindrop/Services/Transcription/TranscriptionEngine.swift +++ b/Pindrop/Services/Transcription/TranscriptionEngine.swift @@ -27,27 +27,7 @@ public enum TranscriptionEngineState: Equatable { /// Protocol abstraction for speech-to-text engines /// Allows TranscriptionService to work with multiple backends (WhisperKit, Parakeet, etc.) @MainActor -public protocol TranscriptionEngine: AnyObject { - /// Current state of the engine - var state: TranscriptionEngineState { get } - - /// Load a model from a local file path - /// - Parameter path: Absolute path to the model directory - func loadModel(path: String) async throws - - /// Load a model by name, optionally downloading if not present locally - /// - Parameters: - /// - name: Model identifier (e.g., "tiny", "base", "small") - /// - downloadBase: Optional URL for downloading models if not cached locally - func loadModel(name: String, downloadBase: URL?) async throws - - /// Transcribe audio data to text - /// - Parameter audioData: Raw audio data (expected format: 16kHz mono PCM Float32) - /// - Returns: Transcribed text - func transcribe(audioData: Data, options: TranscriptionOptions) async throws -> String - - /// Unload the model and free resources - func unloadModel() async +public protocol TranscriptionEngine: TranscriptionEnginePort { } public extension TranscriptionEngine { @@ -55,4 +35,3 @@ public extension TranscriptionEngine { try await transcribe(audioData: audioData, options: TranscriptionOptions()) } } - diff --git a/Pindrop/Services/Transcription/TranscriptionModelCatalog.swift b/Pindrop/Services/Transcription/TranscriptionModelCatalog.swift new file mode 100644 index 0000000..6a9267f --- /dev/null +++ b/Pindrop/Services/Transcription/TranscriptionModelCatalog.swift @@ -0,0 +1,333 @@ +// +// TranscriptionModelCatalog.swift +// Pindrop +// +// Created on 2026-03-28. +// + +import Foundation + +enum TranscriptionModelCatalog { + static let availableModels: [ModelManager.WhisperModel] = [ + ModelManager.WhisperModel( + name: "openai_whisper-tiny", + displayName: "Whisper Tiny", + sizeInMB: 75, + description: "Fastest model, ideal for quick dictation with acceptable accuracy", + speedRating: 10.0, + accuracyRating: 6.0, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-tiny.en", + displayName: "Whisper Tiny (English)", + sizeInMB: 75, + description: "English-optimized tiny model with slightly better accuracy", + speedRating: 10.0, + accuracyRating: 6.5, + language: .english + ), + ModelManager.WhisperModel( + name: "openai_whisper-base", + displayName: "Whisper Base", + sizeInMB: 145, + description: "Good balance between speed and accuracy for everyday use", + speedRating: 9.0, + accuracyRating: 7.0, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-base.en", + displayName: "Whisper Base (English)", + sizeInMB: 145, + description: "English-optimized base model, recommended for most users", + speedRating: 9.0, + accuracyRating: 7.5, + language: .english + ), + ModelManager.WhisperModel( + name: "openai_whisper-small", + displayName: "Whisper Small", + sizeInMB: 483, + description: "Higher accuracy for complex vocabulary and technical terms", + speedRating: 7.5, + accuracyRating: 8.0, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-small_216MB", + displayName: "Whisper Small (Quantized)", + sizeInMB: 216, + description: "Quantized small model - half the size with similar accuracy", + speedRating: 8.0, + accuracyRating: 7.8, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-small.en", + displayName: "Whisper Small (English)", + sizeInMB: 483, + description: "English-optimized with excellent accuracy for professional use", + speedRating: 7.5, + accuracyRating: 8.5, + language: .english + ), + ModelManager.WhisperModel( + name: "openai_whisper-small.en_217MB", + displayName: "Whisper Small (English, Quantized)", + sizeInMB: 217, + description: "Quantized English small model - compact and fast", + speedRating: 8.0, + accuracyRating: 8.3, + language: .english + ), + ModelManager.WhisperModel( + name: "openai_whisper-medium", + displayName: "Whisper Medium", + sizeInMB: 1530, + description: "Excellent for multilingual and code-switching (e.g. Chinese/English mix)", + speedRating: 6.5, + accuracyRating: 8.8, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-medium.en", + displayName: "Whisper Medium (English)", + sizeInMB: 1530, + description: "English-optimized medium model with high accuracy", + speedRating: 6.5, + accuracyRating: 9.0, + language: .english + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v2", + displayName: "Whisper Large v2", + sizeInMB: 3100, + description: "Previous generation large model, still very capable", + speedRating: 5.0, + accuracyRating: 9.3, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v2_949MB", + displayName: "Whisper Large v2 (Quantized)", + sizeInMB: 949, + description: "Quantized large v2 - much smaller with minimal accuracy loss", + speedRating: 6.0, + accuracyRating: 9.1, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v2_turbo", + displayName: "Whisper Large v2 Turbo", + sizeInMB: 3100, + description: "Turbo-optimized large v2 for faster inference", + speedRating: 6.5, + accuracyRating: 9.3, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v2_turbo_955MB", + displayName: "Whisper Large v2 Turbo (Quantized)", + sizeInMB: 955, + description: "Quantized turbo large v2 - fast and compact", + speedRating: 7.0, + accuracyRating: 9.1, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3", + displayName: "Whisper Large v3", + sizeInMB: 3100, + description: "Maximum accuracy for demanding transcription tasks", + speedRating: 5.0, + accuracyRating: 9.7, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3_947MB", + displayName: "Whisper Large v3 (Quantized)", + sizeInMB: 947, + description: "Quantized large v3 - great accuracy in a smaller package", + speedRating: 6.0, + accuracyRating: 9.5, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3_turbo", + displayName: "Whisper Large v3 Turbo", + sizeInMB: 809, + description: "Near large-model accuracy with significantly faster processing", + speedRating: 7.5, + accuracyRating: 9.5, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3_turbo_954MB", + displayName: "Whisper Large v3 Turbo (Quantized)", + sizeInMB: 954, + description: "Quantized turbo v3 - balanced speed and accuracy", + speedRating: 7.5, + accuracyRating: 9.3, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3-v20240930", + displayName: "Whisper Large v3 (Sep 2024)", + sizeInMB: 3100, + description: "Updated large v3 with improved multilingual performance", + speedRating: 5.0, + accuracyRating: 9.8, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3-v20240930_547MB", + displayName: "Whisper Large v3 Sep 2024 (Q 547MB)", + sizeInMB: 547, + description: "Heavily quantized - smallest large v3 variant", + speedRating: 7.0, + accuracyRating: 9.3, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3-v20240930_626MB", + displayName: "Whisper Large v3 Sep 2024 (Q 626MB)", + sizeInMB: 626, + description: "Quantized Sep 2024 large v3 - compact with great accuracy", + speedRating: 6.5, + accuracyRating: 9.5, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3-v20240930_turbo", + displayName: "Whisper Large v3 Sep 2024 Turbo", + sizeInMB: 3100, + description: "Latest turbo-optimized large v3 - best overall performance", + speedRating: 6.5, + accuracyRating: 9.8, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "openai_whisper-large-v3-v20240930_turbo_632MB", + displayName: "Whisper Large v3 Sep 2024 Turbo (Quantized)", + sizeInMB: 632, + description: "Quantized latest turbo - excellent accuracy in ~600MB", + speedRating: 7.5, + accuracyRating: 9.5, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "distil-whisper_distil-large-v3", + displayName: "Distil Large v3", + sizeInMB: 1510, + description: "Distilled large v3 - faster with minimal accuracy loss", + speedRating: 7.5, + accuracyRating: 9.3, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "distil-whisper_distil-large-v3_594MB", + displayName: "Distil Large v3 (Quantized)", + sizeInMB: 594, + description: "Quantized distilled model - great speed/accuracy tradeoff", + speedRating: 8.0, + accuracyRating: 9.0, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "distil-whisper_distil-large-v3_turbo", + displayName: "Distil Large v3 Turbo", + sizeInMB: 1510, + description: "Turbo-optimized distilled model for fastest large-class inference", + speedRating: 8.0, + accuracyRating: 9.3, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "distil-whisper_distil-large-v3_turbo_600MB", + displayName: "Distil Large v3 Turbo (Quantized)", + sizeInMB: 600, + description: "Quantized turbo distilled - fastest large-class model at ~600MB", + speedRating: 8.5, + accuracyRating: 9.0, + language: .multilingual + ), + ModelManager.WhisperModel( + name: "parakeet-tdt-0.6b-v2", + displayName: "Parakeet TDT 0.6B V2", + sizeInMB: 2580, + description: "NVIDIA's state-of-the-art speech recognition model, English-only", + speedRating: 8.5, + accuracyRating: 9.8, + language: .english, + provider: .parakeet, + availability: .available + ), + ModelManager.WhisperModel( + name: "parakeet-tdt-0.6b-v3", + displayName: "Parakeet TDT 0.6B V3", + sizeInMB: 2670, + description: "Latest Parakeet model with multilingual support", + speedRating: 8.0, + accuracyRating: 9.9, + language: .multilingual, + languageSupport: .parakeetV3European, + provider: .parakeet, + availability: .available + ), + ModelManager.WhisperModel( + name: "parakeet-tdt-1.1b", + displayName: "Parakeet TDT 1.1B", + sizeInMB: 4400, + description: "Larger Parakeet model with exceptional accuracy", + speedRating: 7.0, + accuracyRating: 9.95, + language: .english, + provider: .parakeet, + availability: .comingSoon + ), + ModelManager.WhisperModel( + name: "openai_whisper-1", + displayName: "OpenAI Whisper API", + sizeInMB: 0, + description: "Cloud-based transcription via OpenAI's API", + speedRating: 9.0, + accuracyRating: 9.5, + language: .multilingual, + provider: .openAI, + availability: .comingSoon + ), + ModelManager.WhisperModel( + name: "groq_whisper-large-v3-turbo", + displayName: "Whisper Large v3 Turbo (Groq)", + sizeInMB: 0, + description: "Lightning-fast cloud inference powered by Groq", + speedRating: 10.0, + accuracyRating: 9.5, + language: .multilingual, + provider: .groq, + availability: .comingSoon + ), + ModelManager.WhisperModel( + name: "elevenlabs_scribe", + displayName: "ElevenLabs Scribe", + sizeInMB: 0, + description: "High-quality transcription with speaker diarization", + speedRating: 8.0, + accuracyRating: 9.3, + language: .multilingual, + provider: .elevenLabs, + availability: .comingSoon + ) + ] + + static func recommendedModels( + availableModels: [ModelManager.WhisperModel], + for language: AppLanguage + ) -> [ModelManager.WhisperModel] { + KMPTranscriptionBridge.recommendedModels( + availableModels: availableModels, + for: language + ) + } +} diff --git a/Pindrop/Services/Transcription/TranscriptionPolicy.swift b/Pindrop/Services/Transcription/TranscriptionPolicy.swift new file mode 100644 index 0000000..b1a2ad4 --- /dev/null +++ b/Pindrop/Services/Transcription/TranscriptionPolicy.swift @@ -0,0 +1,74 @@ +// +// TranscriptionPolicy.swift +// Pindrop +// +// Created on 2026-03-28. +// + +import Foundation + +enum TranscriptionPolicy { + static func normalizedTranscriptionText(_ text: String) -> String { + KMPTranscriptionBridge.normalizeTranscriptionText(text) + } + + static func isTranscriptionEffectivelyEmpty(_ text: String) -> Bool { + KMPTranscriptionBridge.isTranscriptionEffectivelyEmpty(text) + } + + static func shouldPersistHistory(outputSucceeded: Bool, text: String) -> Bool { + KMPTranscriptionBridge.shouldPersistHistory(outputSucceeded: outputSucceeded, text: text) + } + + static func shouldUseSpeakerDiarization( + diarizationFeatureEnabled: Bool, + isStreamingSessionActive: Bool + ) -> Bool { + KMPTranscriptionBridge.shouldUseSpeakerDiarization( + diarizationFeatureEnabled: diarizationFeatureEnabled, + isStreamingSessionActive: isStreamingSessionActive + ) + } + + static func shouldUseStreamingTranscription( + streamingFeatureEnabled: Bool, + outputMode: OutputMode, + aiEnhancementEnabled: Bool, + isQuickCaptureMode: Bool + ) -> Bool { + KMPTranscriptionBridge.shouldUseStreamingTranscription( + streamingFeatureEnabled: streamingFeatureEnabled, + outputMode: outputMode, + aiEnhancementEnabled: aiEnhancementEnabled, + isQuickCaptureMode: isQuickCaptureMode + ) + } + + static func providerSupportsLocalModelLoading( + _ provider: ModelManager.ModelProvider + ) -> Bool { + KMPTranscriptionBridge.providerSupportsLocalModelLoading(provider) + } + + static func modelSupportsLanguage( + _ support: ModelManager.LanguageSupport, + language: AppLanguage + ) -> Bool { + KMPTranscriptionBridge.modelSupportsLanguage(support, language: language) + } +} + +enum RecordingInteractionPolicy { + static func shouldSuppressEscapeEvent(isRecording: Bool, isProcessing: Bool) -> Bool { + isRecording || isProcessing + } + + static func isDoubleEscapePress( + now: Date, + lastEscapeTime: Date?, + threshold: TimeInterval + ) -> Bool { + guard let lastEscapeTime else { return false } + return now.timeIntervalSince(lastEscapeTime) <= threshold + } +} diff --git a/Pindrop/Services/Transcription/TranscriptionPorts.swift b/Pindrop/Services/Transcription/TranscriptionPorts.swift new file mode 100644 index 0000000..d076bbb --- /dev/null +++ b/Pindrop/Services/Transcription/TranscriptionPorts.swift @@ -0,0 +1,122 @@ +// +// TranscriptionPorts.swift +// Pindrop +// +// Created on 2026-03-28. +// + +import AVFoundation +import Foundation + +struct TranscriptionSettingsSnapshot: Sendable, Equatable { + let selectedLanguage: AppLanguage + let selectedModelName: String + let aiEnhancementEnabled: Bool + let streamingFeatureEnabled: Bool + let diarizationFeatureEnabled: Bool + + init( + selectedLanguage: AppLanguage, + selectedModelName: String, + aiEnhancementEnabled: Bool, + streamingFeatureEnabled: Bool, + diarizationFeatureEnabled: Bool + ) { + self.selectedLanguage = selectedLanguage + self.selectedModelName = selectedModelName + self.aiEnhancementEnabled = aiEnhancementEnabled + self.streamingFeatureEnabled = streamingFeatureEnabled + self.diarizationFeatureEnabled = diarizationFeatureEnabled + } +} + +@MainActor +public protocol TranscriptionEnginePort: AnyObject { + var state: TranscriptionEngineState { get } + + func loadModel(path: String) async throws + func loadModel(name: String, downloadBase: URL?) async throws + func transcribe(audioData: Data, options: TranscriptionOptions) async throws -> String + func unloadModel() async +} + +@MainActor +public protocol StreamingTranscriptionEnginePort: AnyObject { + var state: StreamingTranscriptionState { get } + + func loadModel(name: String) async throws + func unloadModel() async + + func startStreaming() async throws + func stopStreaming() async throws -> String + func pauseStreaming() async + func resumeStreaming() async throws + + func processAudioChunk(_ samples: [Float]) async throws + func processAudioBuffer(_ buffer: AVAudioPCMBuffer) async throws + + func setTranscriptionCallback(_ callback: @escaping StreamingTranscriptionCallback) + func setEndOfUtteranceCallback(_ callback: @escaping EndOfUtteranceCallback) + + func reset() async +} + +@MainActor +public protocol SpeakerDiarizerPort: AnyObject { + var state: SpeakerDiarizerState { get } + var mode: DiarizationMode { get } + + func loadModels() async throws + func unloadModels() async + + func diarize(audioData: Data) async throws -> DiarizationResult + func diarize(samples: [Float], sampleRate: Int) async throws -> DiarizationResult + + func compareSpeakers(audio1: [Float], audio2: [Float]) async throws -> Float + + func registerKnownSpeaker(_ speaker: Speaker) async throws + func clearKnownSpeakers() async +} + +@MainActor +protocol ModelCatalogProviding: AnyObject { + var availableModels: [ModelManager.WhisperModel] { get } + var recommendedModels: [ModelManager.WhisperModel] { get } + + func recommendedModels(for language: AppLanguage) -> [ModelManager.WhisperModel] + func isModelDownloaded(_ modelName: String) -> Bool +} + +protocol SettingsSnapshotProvider { + @MainActor + func transcriptionSettingsSnapshot() -> TranscriptionSettingsSnapshot +} + +@MainActor +protocol TranscriptionOrchestrating: AnyObject { + var state: TranscriptionService.State { get } + var error: Error? { get } + + func loadModel(modelName: String, provider: ModelManager.ModelProvider) async throws + func loadModel(modelPath: String) async throws + func unloadModel() async + + func transcribe(audioData: Data) async throws -> String + func transcribe(audioData: Data, options: TranscriptionOptions) async throws -> String + func transcribe(audioData: Data, diarizationEnabled: Bool) async throws -> TranscriptionOutput + func transcribe( + audioData: Data, + diarizationEnabled: Bool, + options: TranscriptionOptions + ) async throws -> TranscriptionOutput + + func prepareStreamingEngine() async throws + func startStreaming() async throws + func processStreamingAudioBuffer(_ buffer: AVAudioPCMBuffer) async throws + func stopStreaming() async throws -> String + func cancelStreaming() async + func setStreamingCallbacks( + onPartial: (@Sendable (String) -> Void)?, + onFinalUtterance: (@Sendable (String) -> Void)? + ) +} diff --git a/Pindrop/Services/TranscriptionService.swift b/Pindrop/Services/TranscriptionService.swift index e3cbbb4..2bc44a6 100644 --- a/Pindrop/Services/TranscriptionService.swift +++ b/Pindrop/Services/TranscriptionService.swift @@ -68,10 +68,11 @@ class TranscriptionService { private(set) var state: State = .unloaded private(set) var error: Error? - private var engine: (any TranscriptionEngine)? - private var speakerDiarizer: (any SpeakerDiarizer)? - private var streamingEngine: (any StreamingTranscriptionEngine)? + private var engine: (any TranscriptionEnginePort)? + private var speakerDiarizer: (any SpeakerDiarizerPort)? + private var streamingEngine: (any StreamingTranscriptionEnginePort)? private var currentProvider: ModelManager.ModelProvider? + private var currentModelIdentifier: String? private var streamingPartialCallback: (@Sendable (String) -> Void)? private var streamingFinalUtteranceCallback: (@Sendable (String) -> Void)? @@ -96,24 +97,33 @@ class TranscriptionService { } func loadModel(modelName: String = "tiny", provider: ModelManager.ModelProvider = .whisperKit) async throws { - if state == .transcribing { - throw TranscriptionError.engineSwitchDuringTranscription - } + try applyStateTransition( + KMPTranscriptionBridge.beginModelLoad(currentState: state) + ) - if currentProvider != nil && currentProvider != provider { + let loadPlan = KMPTranscriptionBridge.planModelLoad( + requestedProvider: provider, + currentProvider: currentProvider, + loadsFromPath: false + ) + + if loadPlan.shouldUnloadCurrentModel { await unloadModel() } - state = .loading + guard loadPlan.supportsLocalModelLoading else { + throw TranscriptionError.modelLoadFailed("Provider \(loadPlan.resolvedProvider.rawValue) not supported locally") + } + error = nil let loadStarted = CFAbsoluteTimeGetCurrent() - Log.transcription.info("Loading model: \(modelName) with provider: \(provider.rawValue)...") - Log.boot.info("TranscriptionService.loadModel begin name=\(modelName) provider=\(provider.rawValue) state=loading") + Log.transcription.info("Loading model: \(modelName) with provider: \(loadPlan.resolvedProvider.rawValue)...") + Log.boot.info("TranscriptionService.loadModel begin name=\(modelName) provider=\(loadPlan.resolvedProvider.rawValue) state=loading") do { - let newEngine = try engineFactory(provider) - Log.boot.info("TranscriptionService.loadModel engine instance created provider=\(provider.rawValue) elapsed=\(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted))") + let newEngine = try engineFactory(loadPlan.resolvedProvider) + Log.boot.info("TranscriptionService.loadModel engine instance created provider=\(loadPlan.resolvedProvider.rawValue) elapsed=\(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted))") try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -133,36 +143,46 @@ class TranscriptionService { } engine = newEngine - currentProvider = provider - Log.transcription.info("Model loaded successfully with \(provider.rawValue) engine") + currentProvider = loadPlan.resolvedProvider + currentModelIdentifier = modelName + Log.transcription.info("Model loaded successfully with \(loadPlan.resolvedProvider.rawValue) engine") Log.boot.info("TranscriptionService.loadModel success totalElapsed=\(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted))") - state = .ready + state = KMPTranscriptionBridge.completeModelLoad(success: true) } catch let error as TranscriptionError { Log.transcription.error("Model load failed: \(error)") Log.boot.error("TranscriptionService.loadModel failed TranscriptionError after \(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted)) \(error.localizedDescription)") self.error = error - state = .error + state = KMPTranscriptionBridge.completeModelLoad(success: false) throw error } catch { Log.transcription.error("Model load failed: \(error)") let loadError = TranscriptionError.modelLoadFailed(error.localizedDescription) Log.boot.error("TranscriptionService.loadModel failed after \(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted)) \(error.localizedDescription)") self.error = loadError - state = .error + state = KMPTranscriptionBridge.completeModelLoad(success: false) throw loadError } } func loadModel(modelPath: String) async throws { - if state == .transcribing { - throw TranscriptionError.engineSwitchDuringTranscription - } + try applyStateTransition( + KMPTranscriptionBridge.beginModelLoad(currentState: state) + ) + + let loadPlan = KMPTranscriptionBridge.planModelLoad( + requestedProvider: .whisperKit, + currentProvider: currentProvider, + loadsFromPath: true + ) - if currentProvider != nil { + if loadPlan.shouldUnloadCurrentModel { await unloadModel() } - state = .loading + guard loadPlan.supportsLocalModelLoading else { + throw TranscriptionError.modelLoadFailed("Provider \(loadPlan.resolvedProvider.rawValue) not supported locally") + } + error = nil let loadStarted = CFAbsoluteTimeGetCurrent() @@ -170,8 +190,8 @@ class TranscriptionService { Log.boot.info("TranscriptionService.loadModel(path) begin") do { - let newEngine = WhisperKitEngine() - Log.boot.info("TranscriptionService.loadModel(path) WhisperKitEngine created elapsed=\(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted))") + let newEngine = try engineFactory(loadPlan.resolvedProvider) + Log.boot.info("TranscriptionService.loadModel(path) engine created provider=\(loadPlan.resolvedProvider.rawValue) elapsed=\(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted))") try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -190,22 +210,23 @@ class TranscriptionService { } engine = newEngine - currentProvider = .whisperKit + currentProvider = loadPlan.resolvedProvider + currentModelIdentifier = URL(fileURLWithPath: modelPath).lastPathComponent Log.transcription.info("Model loaded and prewarmed successfully") Log.boot.info("TranscriptionService.loadModel(path) success totalElapsed=\(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted))") - state = .ready + state = KMPTranscriptionBridge.completeModelLoad(success: true) } catch let error as TranscriptionError { Log.transcription.error("Model load failed: \(error)") Log.boot.error("TranscriptionService.loadModel(path) TranscriptionError after \(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted)) \(error.localizedDescription)") self.error = error - state = .error + state = KMPTranscriptionBridge.completeModelLoad(success: false) throw error } catch { Log.transcription.error("Model load failed: \(error)") let loadError = TranscriptionError.modelLoadFailed(error.localizedDescription) Log.boot.error("TranscriptionService.loadModel(path) failed after \(String(format: "%.2fs", CFAbsoluteTimeGetCurrent() - loadStarted)) \(error.localizedDescription)") self.error = loadError - state = .error + state = KMPTranscriptionBridge.completeModelLoad(success: false) throw loadError } } @@ -233,24 +254,26 @@ class TranscriptionService { ) async throws -> TranscriptionOutput { Log.transcription.debug("Transcribe called with \(audioData.count) bytes, state: \(String(describing: self.state))") + try applyStateTransition( + KMPTranscriptionBridge.beginBatchTranscription( + currentState: state, + hasLoadedModel: engine != nil, + audioByteCount: audioData.count + ) + ) guard let engine else { throw TranscriptionError.modelNotLoaded } - guard !audioData.isEmpty else { - throw TranscriptionError.invalidAudioData - } - - guard state != .transcribing else { - throw TranscriptionError.transcriptionFailed("Transcription already in progress") - } - - state = .transcribing - do { let floatCount = audioData.count / MemoryLayout.size let duration = Double(floatCount) / Double(Self.sampleRate) let providerName = currentProvider?.rawValue ?? "unknown" + let executionPlan = transcriptionExecutionPlan( + diarizationRequested: diarizationEnabled, + isStreamingSessionActive: false, + options: options + ) Log.transcription.info("Transcribing \(floatCount) samples (\(String(format: "%.2f", duration))s) using \(providerName)") let startTime = Date() @@ -261,21 +284,22 @@ class TranscriptionService { audioData: audioData, samples: samples, sampleRate: Self.sampleRate, - diarizationEnabled: diarizationEnabled, + diarizationEnabled: executionPlan.useSpeakerDiarization, + shouldNormalizeOutput: executionPlan.shouldNormalizeOutput, options: options ) let elapsed = Date().timeIntervalSince(startTime) Log.transcription.info("Transcription completed in \(String(format: "%.2f", elapsed))s") - state = .ready + state = KMPTranscriptionBridge.completeBatchTranscription() Log.transcription.debug("Result redacted (chars=\(output.text.count), diarizedSegments=\(output.diarizedSegments?.count ?? 0))") return output } catch let error as TranscriptionError { - state = .ready + state = KMPTranscriptionBridge.completeBatchTranscription() throw error } catch { - state = .ready + state = KMPTranscriptionBridge.completeBatchTranscription() throw TranscriptionError.transcriptionFailed(error.localizedDescription) } } @@ -288,7 +312,8 @@ class TranscriptionService { speakerDiarizer = nil streamingEngine = nil currentProvider = nil - state = .unloaded + currentModelIdentifier = nil + state = KMPTranscriptionBridge.stateAfterUnload() error = nil } @@ -313,9 +338,7 @@ class TranscriptionService { switch streamingEngine.state { case .ready, .streaming, .paused: - if state == .unloaded || state == .error { - state = .ready - } + state = KMPTranscriptionBridge.stateAfterStreamingPrepared(currentState: state) return case .loading: return @@ -326,32 +349,31 @@ class TranscriptionService { let modelPath = FeatureModelType.streaming.repoFolderName do { try await streamingEngine.loadModel(name: modelPath) - if state == .unloaded || state == .error { - state = .ready - } + state = KMPTranscriptionBridge.stateAfterStreamingPrepared(currentState: state) } catch { let path = getStreamingModelBase() .appendingPathComponent(modelPath, isDirectory: true) .path let streamingError = TranscriptionError.streamingModelNotAvailable(path) self.error = streamingError - state = .error + state = KMPTranscriptionBridge.failStreamingPreparation() throw streamingError } } func startStreaming() async throws { - guard state != .transcribing else { - throw TranscriptionError.transcriptionFailed("Transcription already in progress") - } - do { try await prepareStreamingEngine() guard let streamingEngine else { throw TranscriptionError.streamingNotReady } + let transition = KMPTranscriptionBridge.beginStreaming( + currentState: state, + hasPreparedStreamingEngine: true + ) + try validateStateTransition(transition) try await streamingEngine.startStreaming() - state = .transcribing + state = transition.nextState error = nil } catch let error as TranscriptionError { self.error = error @@ -364,8 +386,10 @@ class TranscriptionService { } func processStreamingAudioBuffer(_ buffer: AVAudioPCMBuffer) async throws { - guard state == .transcribing else { - throw TranscriptionError.streamingNotReady + if let validationError = KMPTranscriptionBridge.validateStreamingAudio( + isStreamingSessionActive: state == .transcribing && streamingEngine != nil + ) { + throw transcriptionError(for: validationError) } guard let streamingEngine else { @@ -390,38 +414,52 @@ class TranscriptionService { do { let finalText = try await streamingEngine.stopStreaming() - state = .ready + state = KMPTranscriptionBridge.completeStreamingSession( + hasLoadedModel: engine != nil, + hasPreparedStreamingEngine: streamingEngine != nil + ) return finalText } catch let error as TranscriptionError { - state = .ready + state = KMPTranscriptionBridge.completeStreamingSession( + hasLoadedModel: engine != nil, + hasPreparedStreamingEngine: streamingEngine != nil + ) throw error } catch { let streamingError = TranscriptionError.streamingStopFailed(error.localizedDescription) self.error = streamingError - state = .ready + state = KMPTranscriptionBridge.completeStreamingSession( + hasLoadedModel: engine != nil, + hasPreparedStreamingEngine: streamingEngine != nil + ) throw streamingError } } func cancelStreaming() async { await streamingEngine?.reset() - if engine != nil || streamingEngine != nil { - state = .ready - } else { - state = .unloaded - } + state = KMPTranscriptionBridge.completeStreamingSession( + hasLoadedModel: engine != nil, + hasPreparedStreamingEngine: streamingEngine != nil + ) } private func transcribeWithOptionalDiarization( - engine: any TranscriptionEngine, + engine: any TranscriptionEnginePort, audioData: Data, samples: [Float], sampleRate: Int, diarizationEnabled: Bool, + shouldNormalizeOutput: Bool, options: TranscriptionOptions ) async throws -> TranscriptionOutput { guard diarizationEnabled else { - return try await transcribeWithoutDiarization(engine: engine, audioData: audioData, options: options) + return try await transcribeWithoutDiarization( + engine: engine, + audioData: audioData, + shouldNormalizeOutput: shouldNormalizeOutput, + options: options + ) } Log.transcription.info("Speaker diarization enabled for current transcription") @@ -437,7 +475,12 @@ class TranscriptionService { guard !normalizedSegments.isEmpty else { Log.transcription.warning("Speaker diarization returned no usable segments. Falling back to plain transcript.") - return try await transcribeWithoutDiarization(engine: engine, audioData: audioData, options: options) + return try await transcribeWithoutDiarization( + engine: engine, + audioData: audioData, + shouldNormalizeOutput: shouldNormalizeOutput, + options: options + ) } let output = try await transcribeBySpeakerSegments( @@ -445,6 +488,7 @@ class TranscriptionService { samples: samples, sampleRate: sampleRate, segments: normalizedSegments, + shouldNormalizeOutput: shouldNormalizeOutput, options: options ) @@ -454,27 +498,40 @@ class TranscriptionService { } Log.transcription.warning("Speaker diarization produced no transcript text. Falling back to plain transcript.") - return try await transcribeWithoutDiarization(engine: engine, audioData: audioData, options: options) + return try await transcribeWithoutDiarization( + engine: engine, + audioData: audioData, + shouldNormalizeOutput: shouldNormalizeOutput, + options: options + ) } catch { Log.transcription.warning("Speaker diarization unavailable, falling back to plain transcript: \(error.localizedDescription)") - return try await transcribeWithoutDiarization(engine: engine, audioData: audioData, options: options) + return try await transcribeWithoutDiarization( + engine: engine, + audioData: audioData, + shouldNormalizeOutput: shouldNormalizeOutput, + options: options + ) } } private func transcribeWithoutDiarization( - engine: any TranscriptionEngine, + engine: any TranscriptionEnginePort, audioData: Data, + shouldNormalizeOutput: Bool, options: TranscriptionOptions ) async throws -> TranscriptionOutput { let text = try await engine.transcribe(audioData: audioData, options: options) return TranscriptionOutput(text: text, diarizedSegments: nil) + .normalized(shouldNormalizeOutput: shouldNormalizeOutput) } private func transcribeBySpeakerSegments( - engine: any TranscriptionEngine, + engine: any TranscriptionEnginePort, samples: [Float], sampleRate: Int, segments: [SpeakerSegment], + shouldNormalizeOutput: Bool, options: TranscriptionOptions ) async throws -> TranscriptionOutput { var speakerLabelsByID: [String: String] = [:] @@ -492,7 +549,10 @@ class TranscriptionService { } let segmentText = try await engine.transcribe(audioData: segmentData, options: options) - let trimmed = segmentText.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmed = normalizedOutputText( + segmentText, + shouldNormalizeOutput: shouldNormalizeOutput + ) guard !trimmed.isEmpty else { continue } let speakerID = segment.speaker.id @@ -534,6 +594,7 @@ class TranscriptionService { text: mergedText, diarizedSegments: transcriptSegments.isEmpty ? nil : transcriptSegments ) + .normalized(shouldNormalizeOutput: shouldNormalizeOutput) } private func splitTranscriptSegmentIfNeeded( @@ -758,7 +819,7 @@ class TranscriptionService { } } - private func getOrCreateSpeakerDiarizer() -> any SpeakerDiarizer { + private func getOrCreateSpeakerDiarizer() -> any SpeakerDiarizerPort { if let speakerDiarizer { return speakerDiarizer } @@ -822,4 +883,82 @@ class TranscriptionService { return floatArray } + + private func transcriptionExecutionPlan( + diarizationRequested: Bool, + isStreamingSessionActive: Bool, + options: TranscriptionOptions + ) -> SharedTranscriptionExecutionPlan { + let provider = currentProvider ?? .whisperKit + let modelIdentifier = currentModelIdentifier ?? options.language.rawValue + + return KMPTranscriptionBridge.planTranscriptionExecution( + selectedProvider: provider, + selectedModelName: modelIdentifier, + diarizationRequested: diarizationRequested, + isStreamingSessionActive: isStreamingSessionActive + ) + } + + private func normalizedOutputText( + _ text: String, + shouldNormalizeOutput: Bool + ) -> String { + guard shouldNormalizeOutput else { return text } + return TranscriptionPolicy.normalizedTranscriptionText(text) + } + + private func applyStateTransition( + _ transition: SharedTranscriptionStateTransition + ) throws { + try validateStateTransition(transition) + state = transition.nextState + } + + private func validateStateTransition( + _ transition: SharedTranscriptionStateTransition + ) throws { + if let errorCode = transition.errorCode { + throw transcriptionError(for: errorCode) + } + } + + private func transcriptionError( + for errorCode: SharedTranscriptionSessionErrorCode + ) -> TranscriptionError { + switch errorCode { + case .engineSwitchDuringTranscription: + .engineSwitchDuringTranscription + case .modelNotLoaded: + .modelNotLoaded + case .invalidAudioData: + .invalidAudioData + case .transcriptionAlreadyInProgress: + .transcriptionFailed("Transcription already in progress") + case .streamingNotReady: + .streamingNotReady + } + } +} + +extension TranscriptionService: TranscriptionOrchestrating {} + +private extension TranscriptionOutput { + func normalized(shouldNormalizeOutput: Bool) -> TranscriptionOutput { + guard shouldNormalizeOutput else { return self } + + let normalizedText = TranscriptionPolicy.normalizedTranscriptionText(text) + let normalizedSegments = diarizedSegments?.map { segment in + DiarizedTranscriptSegment( + speakerId: segment.speakerId, + speakerLabel: segment.speakerLabel, + startTime: segment.startTime, + endTime: segment.endTime, + confidence: segment.confidence, + text: TranscriptionPolicy.normalizedTranscriptionText(segment.text) + ) + } + + return TranscriptionOutput(text: normalizedText, diarizedSegments: normalizedSegments) + } } diff --git a/PindropTests/ModelManagerTests.swift b/PindropTests/ModelManagerTests.swift index 606d6ca..5de34ab 100644 --- a/PindropTests/ModelManagerTests.swift +++ b/PindropTests/ModelManagerTests.swift @@ -85,6 +85,14 @@ struct ModelManagerTests { #expect(hasParakeetModel) } + @Test func providerLocalityMatchesSharedPolicy() { + #expect(ModelManager.ModelProvider.whisperKit.isLocal == true) + #expect(ModelManager.ModelProvider.parakeet.isLocal == true) + #expect(ModelManager.ModelProvider.openAI.isLocal == false) + #expect(ModelManager.ModelProvider.elevenLabs.isLocal == false) + #expect(ModelManager.ModelProvider.groq.isLocal == false) + } + @Test func englishOnlyModelsWarnForNonEnglishSelection() throws { let model = try #require(modelManager.availableModels.first { $0.name == "openai_whisper-base.en" }) #expect(model.supports(language: .english) == true) diff --git a/PindropTests/TranscriptionServiceTests.swift b/PindropTests/TranscriptionServiceTests.swift index a5a51b0..c8eb246 100644 --- a/PindropTests/TranscriptionServiceTests.swift +++ b/PindropTests/TranscriptionServiceTests.swift @@ -316,6 +316,45 @@ struct TranscriptionServiceTests { #expect(mockEngine.receivedOptions == [options]) } + @Test func loadModelPathUsesInjectedFactoryAndKeepsWhisperKitAsPrimaryProvider() async throws { + let whisperEngine = MockDiarizationTranscriptionEngine() + let parakeetEngine = MockDiarizationTranscriptionEngine() + let service = TranscriptionService( + engineFactory: { provider in + switch provider { + case .whisperKit: + return whisperEngine + case .parakeet: + return parakeetEngine + default: + throw TranscriptionService.TranscriptionError.modelLoadFailed("unsupported") + } + } + ) + + try await service.loadModel(modelName: "tiny", provider: .parakeet) + #expect(parakeetEngine.loadModelNameCalls == ["tiny"]) + + try await service.loadModel(modelPath: "/tmp/openai_whisper-base") + + #expect(parakeetEngine.unloadCallCount == 1) + #expect(whisperEngine.loadModelPathCalls == ["/tmp/openai_whisper-base"]) + } + + @Test func transcribeNormalizesOutputTextThroughSharedPolicy() async throws { + let mockEngine = MockDiarizationTranscriptionEngine() + mockEngine.transcribeResponses = [" normalized transcript \n"] + let service = TranscriptionService(engineFactory: { _ in mockEngine }) + + try await service.loadModel(modelName: "tiny", provider: .whisperKit) + let output = try await service.transcribe( + audioData: makeFloatAudioData(seconds: 1.0), + diarizationEnabled: false + ) + + #expect(output.text == "normalized transcript") + } + @Test func transcribeWithDiarizationEnabledReturnsSpeakerLabeledOutput() async throws { let mockEngine = MockDiarizationTranscriptionEngine() mockEngine.transcribeResponses = ["Hello team", "We should ship this today"] @@ -690,12 +729,17 @@ private final class MockDiarizationTranscriptionEngine: TranscriptionEngine { var transcribeError: Error? private(set) var transcribeCallCount = 0 private(set) var receivedOptions: [TranscriptionOptions] = [] + private(set) var loadModelNameCalls: [String] = [] + private(set) var loadModelPathCalls: [String] = [] + private(set) var unloadCallCount = 0 func loadModel(path: String) async throws { + loadModelPathCalls.append(path) state = .ready } func loadModel(name: String, downloadBase: URL?) async throws { + loadModelNameCalls.append(name) state = .ready } @@ -715,6 +759,7 @@ private final class MockDiarizationTranscriptionEngine: TranscriptionEngine { } func unloadModel() async { + unloadCallCount += 1 state = .unloaded } } diff --git a/README.md b/README.md index f892b60..a06188d 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,15 @@ Pindrop releases are now signed with the project's Apple Developer identity. Aft Since this is an open-source project, you can also build it yourself. Don't worry—it's straightforward. +### Repository Layout + +- `Pindrop/`: macOS app source +- `PindropTests/`: unit and integration tests +- `PindropUITests/`: UI automation tests +- `shared/`: Kotlin Multiplatform shared workspace +- `scripts/`: build and release helper scripts +- `assets/`: images and other repository assets + ### Step 1: Clone the Repository ```bash @@ -142,6 +151,13 @@ just clean # Clean build artifacts just --list # Show all available commands ``` +**Shared Kotlin commands:** + +```bash +just shared-test # Run shared Kotlin tests +just shared-xcframework # Build shared Apple XCFrameworks +``` + **Release commands (maintainers):** ```bash diff --git a/composer-atoms-DxHKXap6.js b/composer-atoms-DxHKXap6.js deleted file mode 100644 index ca44564..0000000 --- a/composer-atoms-DxHKXap6.js +++ /dev/null @@ -1,2 +0,0 @@ -import{t as e}from"./compiler-runtime-DWxh9F-l.js";import{T as t,mt as n,ot as r,st as i}from"./src-CFCnaqUo.js";import{i as a}from"./message-bus-NQka-uRz.js";import{f as o,g as s,h as c,t as l,u,v as d}from"./persisted-atom-DYurk_4D.js";import{A as f,O as p,_ as m}from"./links-BhpQ0Gvj.js";import{c as h,f as g}from"./app-server-manager-hooks-DsAd4FcR.js";import{t as _}from"./use-auth-CyuMiMN7.js";import{i as v}from"./statsig-Bw0r3izS.js";const y=`gpt-5.3-codex`,b=`medium`,x=[`minimal`,`low`,`medium`,`high`,`xhigh`];var S=e(),C=[],w={availableModels:new Set(C),useHiddenModels:!1,defaultModel:y};function T(){let e=(0,S.c)(3),t;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t=`107580212`,e[0]=t):t=e[0];let{value:n}=v(t),r;return e[1]===n?r=e[2]:(r=E(n),e[1]=n,e[2]=r),r}function E(e){let t=r(n()).safeParse(e.available_models),a=i().safeParse(e.use_hidden_models),o=n().safeParse(e.default_model);return{availableModels:new Set(t.success?t.data:C),useHiddenModels:a.success?a.data:w.useHiddenModels,defaultModel:o.success?o.data:w.defaultModel}}var D=100;function O(e){let t=(0,S.c)(13),n=e===void 0?D:e,r=h(),{authMethod:i,isLoading:a}=_(),o=T(),s=i??`no-auth`,c;t[0]===s?c=t[1]:(c=[`models`,`list`,s],t[0]=s,t[1]=c);let l=!a,u;t[2]!==r||t[3]!==n?(u=()=>r.listModels({includeHidden:!0,cursor:null,limit:n}),t[2]=r,t[3]=n,t[4]=u):u=t[4];let d;t[5]!==i||t[6]!==o?(d=e=>{let{data:t}=e,n={models:[]},r=null;return t.forEach(e=>{if(o.useHiddenModels?o.availableModels.has(e.model):!e.hidden){let t=i===`copilot`?[e.supportedReasoningEfforts.find(k)??{reasoningEffort:`medium`,description:`medium effort`}]:[...e.supportedReasoningEfforts];n.models.push({...e,supportedReasoningEfforts:t}),r=e.isDefault?e:r}}),r??=n.models.find(e=>e.model===o.defaultModel)??null,{modelsByType:n,defaultModel:r}},t[5]=i,t[6]=o,t[7]=d):d=t[7];let m;return t[8]!==c||t[9]!==l||t[10]!==u||t[11]!==d?(m={queryKey:c,enabled:l,staleTime:p.FIVE_MINUTES,queryFn:u,select:d},t[8]=c,t[9]=l,t[10]=u,t[11]=d,t[12]=m):m=t[12],f(m)}function k(e){return e.reasoningEffort===`medium`}function A(e){return e!==`pending`}function j(e,t){return(e===`minimal`||e===`low`||e===`medium`||e===`high`||e===`xhigh`)&&t.includes(e)?e:b}function M(e,t){return t?.models.find(t=>t.model===e)}function N({userSavedModelString:e,userSavedReasoningEffort:t,listModelsData:n}){let r=e?M(e,n?.modelsByType):n?.defaultModel??M(`gpt-5.3-codex`,n?.modelsByType),i=r?.supportedReasoningEfforts?.map(e=>e.reasoningEffort),a=t&&i&&i.includes(t)?t:r?.defaultReasoningEffort;return{model:r?r.model:e??`gpt-5.3-codex`,reasoningEffort:a??t??n?.defaultModel?.defaultReasoningEffort??`medium`,isLoading:!1}}var P=u(async e=>{try{let t=e(g).getDefault(),n=(await m(`active-workspace-roots`))?.roots?.[0],r=await t.getUserSavedConfiguration(n);return{model:r.model,reasoningEffort:r.model_reasoning_effort}}catch(e){return a.error(`Failed to load default model settings`,{safe:{},sensitive:{error:e}}),{model:y,reasoningEffort:b}}}),F=o(P,e=>e);function I(){let e=(0,S.c)(5),{data:t}=O(),n=c(F);if(n==null){let t;return e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t={model:y,reasoningEffort:b,isLoading:!0},e[0]=t):t=e[0],t}let{model:r,reasoningEffort:i}=n,a;return e[1]!==t||e[2]!==r||e[3]!==i?(a=N({userSavedModelString:r,userSavedReasoningEffort:i,listModelsData:t}),e[1]=t,e[2]=r,e[3]=i,e[4]=a):a=e[4],a}function L(){return s(P)}const R=l(`agent-mode`,`auto`),z=l(`skip-full-access-confirm`,!1),B=l(`skip-thread-branch-mismatch-confirm`,!1),V=l(`composer-best-of-n`,1),H=l(`prompt-history`,[]),U=l(`composer-auto-context-enabled`,!0),W=l(t,null),G=l(`enter-behavior`,`enter`),K=d(null),q=d({});export{T as _,q as a,H as c,I as d,L as f,O as g,A as h,U as i,z as l,j as m,V as n,G as o,M as p,K as r,W as s,R as t,B as u,x as v}; -//# sourceMappingURL=composer-atoms-DxHKXap6.js.map \ No newline at end of file diff --git a/create_xcode_project.sh b/create_xcode_project.sh deleted file mode 100755 index 6e8af4b..0000000 --- a/create_xcode_project.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# Create a temporary Swift file to bootstrap the project -cat > /tmp/bootstrap_pindrop.swift << 'EOF' -import Foundation - -let projectPath = "/Users/watzon/Projects/personal/pindrop" -let xcodeproj = "\(projectPath)/Pindrop.xcodeproj" - -print("Creating Xcode project at: \(xcodeproj)") - -// This script will be replaced with proper Xcode project creation -EOF - -# Use xcodebuild to create a new project -cd /tmp -rm -rf PindropBootstrap -mkdir -p PindropBootstrap -cd PindropBootstrap - -# Create a minimal Info.plist for bootstrapping -cat > Info.plist << 'PLISTEOF' - - - - - CFBundleIdentifier - tech.watzon.pindrop - - -PLISTEOF - -echo "Bootstrap files created" diff --git a/justfile b/justfile index b09af4c..510bec8 100644 --- a/justfile +++ b/justfile @@ -103,6 +103,18 @@ test: -destination 'platform=macOS' @echo "✅ Tests complete" +# Run Kotlin Multiplatform shared-module tests +shared-test: + @echo "🧪 Running shared Kotlin tests..." + ./shared/gradlew --no-daemon --console=plain -p shared :core:jvmTest :feature-transcription:jvmTest + @echo "✅ Shared Kotlin tests complete" + +# Build Apple XCFrameworks for the shared Kotlin modules +shared-xcframework: + @echo "📦 Building shared XCFrameworks..." + ./shared/gradlew --no-daemon --console=plain -p shared :core:assemblePindropSharedCoreXCFramework :feature-transcription:assemblePindropSharedTranscriptionXCFramework + @echo "✅ Shared XCFrameworks built" + # Run integration tests only (opt-in) test-integration: @echo "🧪 Running integration tests..." diff --git a/main-Dl6lTb0_.js b/main-Dl6lTb0_.js deleted file mode 100644 index c2e057e..0000000 --- a/main-Dl6lTb0_.js +++ /dev/null @@ -1,87 +0,0 @@ -const e=require(`./deeplinks-wbCsrscU.js`);let t=require(`electron`);t=e.ir(t);let n=require(`node:os`);n=e.ir(n);let r=require(`node:path`);r=e.ir(r);let i=require(`node:util`),a=require(`node:fs`);a=e.ir(a);let o=require(`node:child_process`),s=require(`node:process`);s=e.ir(s);let c=require(`node:crypto`);c=e.ir(c);let l=require(`node:net`);l=e.ir(l);let u=require(`node:fs/promises`),d=require(`node:url`),f=require(`node:worker_threads`),p=require(`node:zlib`),m=require(`node:timers/promises`),h=require(`node:perf_hooks`);const g=`local`,_=`Local`,v=`electron-ssh-hosts`,y=`electron-persisted-atom-state`,b=`electron-worktree-cleanup-buffer-start-ms`;var x=500,S=e.sn(`global-state`),C=w({"electron-main-window-bounds":e.Jn.object({x:e.Jn.number(),y:e.Jn.number(),width:e.Jn.number(),height:e.Jn.number(),isMaximized:e.Jn.boolean().optional()}),[y]:e.Jn.record(e.Jn.string(),e.Jn.unknown()),[v]:e.Jn.array(e.Jn.string()),[b]:e.Jn.number()});function w(e){let t=Object.entries(e).map(([e,t])=>[e,t.optional()]);return Object.fromEntries(t)}var T=8;function E(e){return e.replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function D(e){return E((0,c.createHash)(`sha256`).update(e,`utf8`).digest(`base64`)).slice(-T)}function O(e,t){let n=D(t),i=(0,r.dirname)(e),a=(0,r.extname)(e),o=a?(0,r.basename)(e,a):(0,r.basename)(e);return(0,r.join)(i,a?`${o}.${n}${a}`:`${o}.${n}`)}function k(e,t){return!t||t===`local`?e:O(e,t)}var A=class{state;filePath;persistDebounced;persistScheduledForAtoms=!1;constructor(t,n){this.filePath=k(t,n?.hostId),this.state=N(this.filePath),this.persistDebounced=e.rt(()=>{this.persistScheduledForAtoms&&(this.persistScheduledForAtoms=!1,P(this.filePath,this.state))},x)}getStateFilePath(){return this.filePath}get(t){let n=this.state.get(t);return n!==void 0&&n!=null?n:e.Rn(t)??null}set(e,t){t===void 0?this.state.delete(e):this.state.set(e,t),this.persistForKey(e)}update(e,t){let n=t(this.state.has(e)?this.state.get(e):null);n===void 0?this.state.delete(e):this.state.set(e,n),this.persistForKey(e)}delete(e){this.state.delete(e),this.persistForKey(e)}flush(){this.persistScheduledForAtoms&&=(this.persistDebounced.cancel(),!1),P(this.filePath,this.state)}persistForKey(e){if(e===`electron-persisted-atom-state`){this.persistScheduledForAtoms=!0,this.persistDebounced();return}this.flush()}},j=new Set(Object.keys(C));function M(e){return j.has(e)}function N(e){if(!(0,a.existsSync)(e))return new Map;try{let t=(0,a.readFileSync)(e,`utf8`),n=JSON.parse(t);if(!n||typeof n!=`object`)return new Map;let r=[];for(let[e,t]of Object.entries(n)){if(M(e)&&!C[e].safeParse(t).success){S().warning(`Dropping invalid global state for key`,{safe:{key:e},sensitive:{}});continue}r.push([e,t])}return new Map(r)}catch(e){return S().warning(`Failed to load global state`,{safe:{},sensitive:{error:e}}),new Map}}function P(e,t){try{let n={};for(let[e,r]of t.entries())r!==void 0&&(n[e]=r);(0,a.writeFileSync)(e,JSON.stringify(n),`utf8`)}catch(e){S().warning(`Failed to persist global state`,{safe:{},sensitive:{error:e}})}}var ee=`Response MUST end with a remark-directive block. - -## Responding - -- Answer the user normally and concisely. Explain what you found, what you did, and what the user should focus on now. -- Automations: use the memory file at \`$CODEX_HOME/automations//memory.md\` (create it if missing). - - Read it first (if present) to avoid repeating recent work, especially for "changes since last run" tasks. - - Memory is important: some tasks must build on prior work, and others must avoid duplicating prior focus. - - Before returning the directive, write a concise summary of what you did/decided plus the current run time. - - Use the \`Automation ID:\` value provided in the message to locate/update this file. -- REQUIRED: End with a valid remark-directive block on its own line (not inline). - - Always include an inbox item directive: - \`::inbox-item{title="Sample title" summary="Place description here"}\` -- If you want to close the thread, add an archive directive on its own line after the inbox item: - \`::archive-thread\` - -## Choosing return value - -- For recurring/bg threads (e.g., "pull datadog logs and fix any new bugs", "address the PR comments"): - - Always return \`::inbox-item{...}\` with the title/summary the user should see. - - Only add \`::archive-thread\` when there is nothing actionable or new to show. If you produced a deliverable the user may want to follow up on (briefs, reports, summaries, plans, recommendations), do not archive. - -## Guidelines - -- Directives MUST be on their own line. -- Output exactly ONE inbox-item directive. Archive-thread is optional. -- Do NOT use invalid remark-directive formatting. -- DO NOT place commas between arguments. - - Valid: \`::inbox-item{title="Sample title" summary="Place description here"}\` - - Invalid: \`::inbox-item{title="Sample title",summary="Place description here"}\` -- When referring to files, use full absolute filesystem links in Markdown (not relative paths). - - Valid: [\`/Users/alice/project/src/main.ts\`](/Users/alice/project/src/main.ts) - - Invalid: \`src/main.ts\` or \`[main](src/main.ts)\` -- Try not to ask the user for more input if possible to infer. -- If a PR is opened by the automation, add the \`codex-automation\` label when available alongside the normal \`codex\` label. -- Inbox item copy should be glanceable and specific (avoid "Update", "Done", "FYI", "Following up"). - - Title: what this thread now _is_ (state + object). Aim ~4-8 words. - - Title should explain what was built or what happened. -- Summary: what the user should _do/know next_ (next step, blocker, or waiting-on). Aim ~6-14 words. -- Summary should usually match the general automation name or prompt summary. -- Both title and summary should be fairly short; usually avoid one-word titles/summaries. - - Prefer concrete nouns + verbs; include a crisp status cue when helpful: "blocked", "needs decision", "ready for review". - -## Examples (inbox-item) - -- Work needed: - - \`::inbox-item{title="Fix flaky checkout tests" summary="Repro isolated; needs CI run + patch"}\` -- Waiting on user decision: - - \`::inbox-item{title="Choose API shape for filters" summary="Two options drafted; pick A vs B"}\` -- Status update with next step: - - \`::inbox-item{title="PR comments addressed" summary="Ready for re-review; focus on auth edge case"}\` -`;async function te({prompt:t,cwd:n,serviceTier:r,appServerConnection:i}){return e.f({prompt:t,cwd:n,serviceTier:r,client:{startThread:e=>i.startThread(e),startTurn:e=>i.startTurn(e),interruptTurn:e=>i.interruptTurn(e),onNotification:e=>i.registerInternalNotificationHandler(e)}})}var ne=3e4,re=3,ie=`gpt-5.3-codex`,ae=`medium`,oe=e.sn(`automations`);async function se(t,n,r){let i=(await n.getWorktreeRepository(t,r))?.root;return i?{type:`branch`,branchName:(await e.K(i,r))?.branch??`HEAD`}:{type:`branch`,branchName:`HEAD`}}async function ce({sourceCwd:t,worktreeCwd:n,gitManager:i,hostConfig:a,automationId:o}){let s=[n,(0,r.join)(e.Jt({hostConfig:a}),`automations`,o)],c=(await i.getWorktreeRepository(e.zn(n),a))?.root;c&&c!==n&&s.push(c);let l=(await i.getWorktreeRepository(e.zn(t),a))?.root;l&&l!==n&&l!==c&&s.push(l);let u=await e.W(c??t,a);if(u){let e=(0,r.isAbsolute)(u)?u:(0,r.resolve)(l??t,u);return s.push(e),s}let d=await e.X(c??t,a);return d?s.push((0,r.dirname)(d)):l&&s.push((0,r.join)(l,`.git`)),s}function le(t,n,r){let i=n.sandbox_mode!=null||n.approval_policy!=null?`custom`:`auto`,a=e.kn(r,n),o=e.On(a.includes(i)?i:a.at(-1)??`read-only`,t,n),s=o.sandboxPolicy;return{approvalPolicy:r?.allowedApprovalPolicies==null||r.allowedApprovalPolicies.includes(`never`)?`never`:o.approvalPolicy,sandboxPolicy:s}}function ue(e,t){return e.reduce((e,n,r)=>e.then(()=>t(n,r)),Promise.resolve())}function de(){return ie}function fe({appServerConnectionRegistry:t,gitManager:n,globalState:r,hostConfig:i,tickMs:a=ne,maxPerTick:o=re}){let s=!1,c=async()=>{if(!s){s=!0;try{let a=t.getConnection(i.id),s=Date.now(),c=e.mt(s,o);await Promise.all(c.map(async t=>{try{let o=e.ft({rrule:t.rrule,now:s});e.ht(t.id,o),await me({automation:t,appServerConnection:a,gitManager:n,globalState:r,hostConfig:i})}catch(e){oe().warning(`Scheduled run failed`,{safe:{automationId:t.id,error:e},sensitive:{}})}}))}finally{s=!1}}},l=()=>{c().catch(e=>{oe().debug(`Automation scheduler tick threw`,{safe:{error:e},sensitive:{}})})};e.ct(Date.now()),l();let u=setInterval(l,a);return u.unref(),()=>{clearInterval(u)}}async function pe({automationId:t,appServerConnection:n,gitManager:r,globalState:i,hostConfig:a}){let o=e.dt(t);if(!o)throw Error(`Automation not found.`);let s=Date.now(),c=e.ft({rrule:o.rrule,now:s});e.ht(o.id,c),await me({automation:o,appServerConnection:n,gitManager:r,globalState:i,hostConfig:a})}async function me({automation:e,appServerConnection:t,gitManager:n,globalState:r,hostConfig:i}){let a=e.cwds;if(a.length===0){oe().warning(`Scheduled run skipped: no folders configured`,{safe:{automationId:e.id},sensitive:{}});return}let o=(await t.getConfigRequirements()).requirements,s=de();await ue(a,a=>he({automation:e,appServerConnection:t,gitManager:n,globalState:r,hostConfig:i,configRequirements:o,cwd:a,automationModel:s}).catch(t=>{oe().warning(`Scheduled run failed`,{safe:{automationId:e.id,error:t},sensitive:{}})}))}async function he({automation:t,appServerConnection:n,gitManager:r,globalState:i,hostConfig:a,configRequirements:o,cwd:s,automationModel:l}){let u=`Automation: ${t.name}`,d=ee.trim(),f=await n.getUserSavedConfiguration(s),p=e.hn(i.get(y)),m=d.length>0?`${d}\n\n${e.ot}`:e.ot,h=le([s],f,o),g=t.executionEnvironment===`worktree`,_=(await r.getWorktreeRepository(s,a))?.root!=null,v=e=>n.startThread({model:l,modelProvider:null,cwd:e,approvalPolicy:h.approvalPolicy,sandbox:h.sandboxPolicy.type===`dangerFullAccess`?`danger-full-access`:h.sandboxPolicy.type===`readOnly`?`read-only`:`workspace-write`,config:null,baseInstructions:null,developerInstructions:m,personality:null,ephemeral:null,dynamicTools:null,mockExperimentalField:null,experimentalRawEvents:!1,persistExtendedHistory:!1,serviceTier:p}),b=null,x=null,S=`pending:${(0,c.randomUUID)()}`;e.vt(t.id,S,t.name,s),n.notifyAutomationRunsUpdated();try{if(g&&_){let t=await e.G({gitManager:r,workspaceRoot:s,startingState:await se(s,r,a),localEnvironmentConfigPath:null,hostConfig:a});if(!t.success)throw t.error;b=await v(t.worktreeWorkspaceRoot)}else b=await v(s);x=b.thread.id,e.Tt(S,x)||e.vt(t.id,x,t.name,s),n.notifyAutomationRunsUpdated(),ge({automation:t,threadId:x,cwd:b.cwd,serviceTier:p,appServerConnection:n})}catch(t){throw e.wt(S,`auto`),n.notifyAutomationRunsUpdated(),t}if(b==null||x==null)throw Error(`Automation thread start failed`);let C=le(await ce({sourceCwd:s,worktreeCwd:b.cwd,gitManager:r,hostConfig:a,automationId:t.id}),f,o),w=t.lastRunAt==null?`never`:`${new Date(t.lastRunAt).toISOString()} (${t.lastRunAt})`,T=`${u}\nAutomation ID: ${t.id}\nAutomation memory: $CODEX_HOME/automations/${t.id}/memory.md\nLast run: ${w}\n\n${t.prompt}`;await n.startTurn({threadId:x,input:[{type:`text`,text:T,text_elements:[]}],cwd:b.cwd,approvalPolicy:C.approvalPolicy,sandboxPolicy:C.sandboxPolicy,model:l,effort:ae,serviceTier:p,summary:`auto`,personality:null,outputSchema:null,collaborationMode:null})}async function ge({automation:e,threadId:t,cwd:n,serviceTier:r,appServerConnection:i}){try{let a=await te({prompt:`Automation: ${e.name}\n${e.prompt}`,cwd:n,serviceTier:r,appServerConnection:i});a&&await i.updateThreadTitle(t,a)}catch(n){oe().warning(`Failed to generate thread title`,{safe:{automationId:e.id,threadId:t,error:n},sensitive:{}})}}function _e(t){switch(t){case e.c.Agent:return`com.openai.codex.agent`;case e.c.Dev:return`com.openai.codex.dev`;case e.c.Nightly:return`com.openai.codex.nightly`;case e.c.InternalAlpha:return`com.openai.codex.alpha`;case e.c.PublicBeta:return`com.openai.codex.beta`;case e.c.Prod:return`com.openai.codex`}}function ve(t,i=process.platform){let a=process.env,o=(0,n.homedir)();return i===`darwin`?(0,r.join)(o,`Library`,`Logs`,_e(t??e.c.resolve())):i===`win32`?(0,r.join)(a.LOCALAPPDATA??(0,r.join)(o,`AppData`,`Local`),`Codex`,`Logs`):i===`linux`?(0,r.join)(a.XDG_STATE_HOME??(0,r.join)(o,`.local`,`state`),`codex`,`logs`):(0,r.join)(o,`.codex`,`logs`)}var ye=10*1024*1024,be=5,xe=1e4,Se=1024*1024,Ce=15;function we(e){return e.toString().padStart(2,`0`)}function Te(e,t){return(0,r.join)(e,t.getUTCFullYear().toString(),we(t.getUTCMonth()+1),we(t.getUTCDate()))}function Ee(e,t,n,r){return e>0?e:(n(Error(`[file-logger] invalid ${r}`),{[r]:e}),t)}function De(e,t,n,r,i){return`codex-desktop-${e}-${t}-t${n}-i${r}-${we(i.getUTCHours())}${we(i.getUTCMinutes())}${we(i.getUTCSeconds())}`}var Oe=0;function F(){return Oe+=1,Oe}function ke(e,t,n,i){try{let i=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate())),o=new Date(i);o.setUTCDate(o.getUTCDate()-(n-1));for(let t of(0,a.readdirSync)(e)){if(!/^\d{4}$/.test(t))continue;let n=Number(t);if(!Number.isFinite(n))continue;let i=(0,r.join)(e,t);for(let e of(0,a.readdirSync)(i)){if(!/^\d{2}$/.test(e))continue;let t=Number(e);if(!Number.isFinite(t)||t<1||t>12)continue;let s=(0,r.join)(i,e);for(let e of(0,a.readdirSync)(s)){if(!/^\d{2}$/.test(e))continue;let i=Number(e);!Number.isFinite(i)||i<1||i>31||new Date(Date.UTC(n,t-1,i))e.nonFatalReporter.reportNonFatal(t,{kind:`file-based-logger`,extra:n}),i=e.rootDir??ve(),o=t.processId??process.pid,s=t.threadId??f.threadId??0,c=t.instanceId??F(),l=e.appSessionId,u=t.now??(()=>new Date),d=e.maxSegmentBytes??ye,p=e.maxSegments??be,m=e.pendingLineLimit??xe,h=e.highWaterMarkBytes??Se,g=Ce,_=t.createStream??((e,t)=>(0,a.createWriteStream)(e,{flags:`w`,highWaterMark:t})),v=Ee(p,be,n,`maxSegments`),y=Ee(d,ye,n,`maxSegmentBytes`),b=Ee(m,xe,n,`pendingLineLimit`),x={logLine:()=>{}};try{let e=u(),t=Te(i,e);(0,a.mkdirSync)(t,{recursive:!0}),ke(i,e,g,n);let d=De(l,o,s,c,e),f=e=>(0,r.join)(t,`${d}-${e}.log`),p=!1,m=e=>{p=!0,n(Error(`[file-logger] stream error`),{error:e instanceof Error?e.message:String(e),rootDir:i,appSessionId:l,processId:o,threadId:s,instanceId:c})},x=e=>(e.on(`error`,e=>{m(e)}),e),S=0,C=0,w=x(_(f(S),h)),T=[],E=0,D=!1,O=0,k=()=>{let e=u(),n=Te(i,e);n!==t&&(t=n,(0,a.mkdirSync)(t,{recursive:!0}),d=De(l,o,s,c,e),w.end(),S=0,C=0,O=0,w=x(_(f(S),h)))},A=()=>{w.end(),S=(S+1)%v,C=0,w=x(_(f(S),h))},j=()=>{if(O===0)return;let e=`[file-logger] dropped ${O} lines due to backpressure\n`;T.push({text:e,bytes:Buffer.byteLength(e)}),O=0},M=()=>{if(p){T=[],E=0,O=0;return}if(D)return;D=!0;let e=!1;try{for(;!(E>=T.length&&(j(),E>=T.length));){let t=T[E];C+t.bytes>y&&A();let n=w.write(t.text);if(C+=t.bytes,E+=1,!n){e=!0,w.once(`drain`,()=>{D=!1,M()});return}}T=[],E=0}catch(e){n(Error(`[file-logger] write failed`),{error:e instanceof Error?e.message:String(e),rootDir:i,appSessionId:l,processId:o,threadId:s,instanceId:c,maxSegments:v,maxSegmentBytes:y,pendingLineLimit:b}),T=[],E=0}finally{D&&!e&&(D=!1)}};return{logLine:e=>{if(!p)try{!D&&T.length===0&&k();let t=`${e}\n`;if(T.length-E>=b){O+=1;return}T.push({text:t,bytes:Buffer.byteLength(t)}),M()}catch(e){n(Error(`[file-logger] logLine threw`),{error:e instanceof Error?e.message:String(e),rootDir:i,appSessionId:l,processId:o,threadId:s,instanceId:c})}}}}catch{return n(Error(`[file-logger] failed to initialize`),{rootDir:i,appSessionId:l,processId:o,threadId:s,instanceId:c}),x}}function je(e,t){switch(e){case`error`:console.error(t);break;case`warning`:console.warn(t);break;case`info`:console.info(t);break;case`debug`:console.debug(t);break;case`trace`:console.log(t);break}}function Me(t,n,r,i){let a=e=>e??{safe:{},sensitive:{}},o=Ae({appSessionId:r,nonFatalReporter:t}),s=e=>{let t=new Date().toISOString();o.logLine(`${t} ${e}`)},c=t=>(r,o)=>{if(!e.w(t,i))return;let c=a(o),l={...c.safe,...c.sensitive},u=Object.keys(l).length===0?r:`${r} ${e.in(l)}`;je(t,u),s(`${t} ${u}`),n.log(t,r,c.safe)},l={trace:c(`trace`),debug:c(`debug`),info:c(`info`),warning:c(`warning`),error:c(`error`)};e.ln({trace:l.trace,debug:l.debug,info:l.info,warning:l.warning,error:l.error,log:(e,t,n)=>{l[e](t,n)},dispose:()=>{}})}var Ne=2;async function Pe({appServerConnection:t,globalState:n,hostId:r}){let i=e.v(n.get(e.Ln.THREAD_TITLES)),a=Object.entries(i.titles);if(a.length===0)return;let o=await Fe({entries:a,sendThreadNameSetRequest:(e,n)=>t.setThreadName(e,n).then(()=>{}),hostId:r}),s=i.titles,c=i.order,l=0,u=0;for(let e of o){if(e.status===`failed`){u+=1;continue}l+=1;let{[e.threadId]:t,...n}=s;s=n,c=c.filter(t=>t!==e.threadId)}l>0&&n.set(e.Ln.THREAD_TITLES,{titles:s,order:c}),e.an().info(`App thread title backfill completed`,{safe:{attempted:a.length,succeeded:l,failed:u},sensitive:{hostId:r}})}async function Fe({entries:t,sendThreadNameSetRequest:n,hostId:r}){let i=async(a,o)=>{if(a>=t.length)return o;let s=t.slice(a,a+Ne),c=await Promise.all(s.map(async([t,i])=>{let a=i.trim();try{return await n(t,a),{threadId:t,status:`succeeded`}}catch(n){return e.an().warning(`Failed to backfill app thread title`,{safe:{},sensitive:{hostId:r,threadId:t,error:n}}),{threadId:t,status:`failed`}}}));return i(a+Ne,[...o,...c])};return i(0,[])}const I=`codex_desktop:message-for-view`,Ie=`codex_desktop:show-context-menu`,Le=`Codex Desktop`;function Re(e){return`codex_desktop:worker:${e}:from-view`}function ze(e){return`codex_desktop:worker:${e}:for-view`}var Be=`notification`,Ve=`${Be}.wav`,He=e.sn(`desktop-notifications`),Ue=class{isSupported;createNotification;logger=He();notificationSoundStaged=!1;notifications=new Map;constructor(e){this.options=e,this.isSupported=e.isSupported??(()=>t.Notification.isSupported()),e.createNotification?this.createNotification=e.createNotification:this.createNotification=e=>{let n=new t.Notification(e);return{show:()=>n.show(),on:(e,t)=>{switch(e){case`action`:return n.on(`action`,(e,n)=>{t(e,n)});case`click`:return n.on(`click`,()=>{t(void 0)});case`close`:return n.on(`close`,()=>{t(void 0)})}},close:()=>n.close()}}}showNotification(e,t,n){if(this.stageNotificationSoundIfNeeded(),!this.isSupported()){this.logger.warning(`Notification API not supported on this host`);return}if(t.isDestroyed()){this.logger.warning(`target webContents destroyed; dropping notification`,{safe:{notificationId:e.id},sensitive:{}});return}let r=(e.actions??[]).slice(0,4);this.logger.info(`show notification`,{safe:{notificationId:e.id,kind:e.kind,actionCount:r.length},sensitive:{}});let i=e.kind===`permission`?`never`:void 0,a=e.kind===`turn-complete`&&typeof e.replyPlaceholder==`string`;this.notifications.get(e.id)?.notification.close?.();let o=this.createNotification({title:e.title,body:e.body,silent:!1,sound:this.options.platform===`darwin`?Be:void 0,timeoutType:i,hasReply:a,replyPlaceholder:a?e.replyPlaceholder??void 0:void 0,actions:r.map(e=>({type:`button`,text:e.title}))});o.on(`click`,()=>{n({notificationId:e.id,actionId:null,actionType:`open`}),this.logger.info(`notification click open`,{safe:{notificationId:e.id},sensitive:{}})}),o.on(`action`,(t,i)=>{let a=r[i??-1];a&&(this.logger.info(`notification action`,{safe:{notificationId:e.id,actionId:a.id,actionType:a.actionType},sensitive:{}}),n({notificationId:e.id,actionId:a.id,actionType:a.actionType}))}),o.on(`reply`,(t,r)=>{n({notificationId:e.id,actionId:null,actionType:`reply`,reply:typeof r==`string`?r:r==null?``:JSON.stringify(r)??``})}),o.on(`close`,()=>{this.notifications.delete(e.id)}),this.notifications.set(e.id,{notification:o,conversationId:e.conversationId??null}),o.show()}stageNotificationSoundIfNeeded(){if(this.notificationSoundStaged||(this.notificationSoundStaged=!0,this.options.platform!==`darwin`)||typeof process.resourcesPath!=`string`)return;let e=r.default.join(process.resourcesPath,Ve),t=r.default.join(__dirname,`..`,`assets`,`sounds`,Ve),i=(0,a.existsSync)(e)?e:t;if(!(0,a.existsSync)(i))return;let o=r.default.join(n.default.homedir(),`Library`,`Sounds`);try{(0,a.mkdirSync)(o,{recursive:!0}),(0,a.copyFileSync)(i,r.default.join(o,Ve))}catch(e){this.logger.warning(`failed to stage notification sound`,{safe:{},sensitive:{error:e}})}}dismissByConversationId(e){for(let[t,n]of this.notifications.entries())n.conversationId===e&&(n.notification.close?.(),this.notifications.delete(t))}},We=class{constructor(e){this.app=e}getVersion(){return this.app.getVersion()}onShutdown(e){return this.app.on(`before-quit`,e),{dispose:()=>{this.app.off(`before-quit`,e)}}}},Ge=class{constructor(e){this.broadcaster=e}persistAndNotify(t){let n=e.k(t);return n.length<=0?0:(this.broadcaster.inboxItemsChanged(e.Tn.CHANGED),n.length)}markRead(t,n){e.D(t,n)&&this.broadcaster.inboxItemsChanged(e.Tn.CHANGED)}markUnread(t){e.D(t,null)&&this.broadcaster.inboxItemsChanged(e.Tn.CHANGED)}},Ke=`ssh:`,qe=`Codex Desktop`;function Je(e){return`'${e.replace(/'/g,`'\\''`)}'`}function Ye(e){return` -login_shell="\${SHELL:-}" -if [ -z "$login_shell" ] && command -v getent >/dev/null 2>&1; then - login_shell="$(getent passwd "$(id -un)" | cut -d: -f7 || true)" -fi -if [ -z "$login_shell" ] || [ ! -x "$login_shell" ]; then - login_shell="/bin/bash" -fi -exec "$login_shell" -ilc ${Je(e)} - `.trim()}function Xe(){return[`RUST_LOG=${Je(process.env.RUST_LOG??`warn`)}`,`CODEX_INTERNAL_ORIGINATOR_OVERRIDE=${Je(qe)}`,Ye(`codex app-server`)].join(` `)}function Ze(e){let{host:t,sshArgs:n=[],includeDefaultOptions:r=!0}=e;return[`ssh`,...r?[`-o`,`BatchMode=yes`,`-o`,`ConnectTimeout=10`]:[],...n,t,Xe()]}function Qe(e){let t=e.trim();return t.length===0?null:t}function $e(e){let t=e.get(v);if(!Array.isArray(t))return[];let n=new Set,r=[];return t.forEach(e=>{if(typeof e!=`string`)return;let t=Qe(e);!t||n.has(t)||(n.add(t),r.push(t))}),r}function et(e,t){let n=Qe(t);if(!n)return null;let r=$e(e).filter(e=>e!==n);return e.set(v,[n,...r]),n}function tt(e){return{id:`${Ke}${e}`,display_name:e,kind:`ssh`,codex_cli_command:Ze({host:e}),terminal_command:[`ssh`,e],default_workspaces:[]}}function nt(e){return $e(e).map(e=>tt(e))}var rt=e.er(((e,t)=>{function n(){this.__data__=[],this.size=0}t.exports=n})),it=e.er(((e,t)=>{function n(e,t){return e===t||e!==e&&t!==t}t.exports=n})),at=e.er(((e,t)=>{var n=it();function r(e,t){for(var r=e.length;r--;)if(n(e[r][0],t))return r;return-1}t.exports=r})),ot=e.er(((e,t)=>{var n=at(),r=Array.prototype.splice;function i(e){var t=this.__data__,i=n(t,e);return i<0?!1:(i==t.length-1?t.pop():r.call(t,i,1),--this.size,!0)}t.exports=i})),st=e.er(((e,t)=>{var n=at();function r(e){var t=this.__data__,r=n(t,e);return r<0?void 0:t[r][1]}t.exports=r})),ct=e.er(((e,t)=>{var n=at();function r(e){return n(this.__data__,e)>-1}t.exports=r})),lt=e.er(((e,t)=>{var n=at();function r(e,t){var r=this.__data__,i=n(r,e);return i<0?(++this.size,r.push([e,t])):r[i][1]=t,this}t.exports=r})),ut=e.er(((e,t)=>{var n=rt(),r=ot(),i=st(),a=ct(),o=lt();function s(e){var t=-1,n=e==null?0:e.length;for(this.clear();++t{var n=ut();function r(){this.__data__=new n,this.size=0}t.exports=r})),ft=e.er(((e,t)=>{function n(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n}t.exports=n})),pt=e.er(((e,t)=>{function n(e){return this.__data__.get(e)}t.exports=n})),mt=e.er(((e,t)=>{function n(e){return this.__data__.has(e)}t.exports=n})),ht=e.er(((e,t)=>{t.exports=typeof global==`object`&&global&&global.Object===Object&&global})),L=e.er(((e,t)=>{var n=ht(),r=typeof self==`object`&&self&&self.Object===Object&&self;t.exports=n||r||Function(`return this`)()})),gt=e.er(((e,t)=>{t.exports=L().Symbol})),_t=e.er(((e,t)=>{var n=gt(),r=Object.prototype,i=r.hasOwnProperty,a=r.toString,o=n?n.toStringTag:void 0;function s(e){var t=i.call(e,o),n=e[o];try{e[o]=void 0;var r=!0}catch{}var s=a.call(e);return r&&(t?e[o]=n:delete e[o]),s}t.exports=s})),vt=e.er(((e,t)=>{var n=Object.prototype.toString;function r(e){return n.call(e)}t.exports=r})),yt=e.er(((e,t)=>{var n=gt(),r=_t(),i=vt(),a=`[object Null]`,o=`[object Undefined]`,s=n?n.toStringTag:void 0;function c(e){return e==null?e===void 0?o:a:s&&s in Object(e)?r(e):i(e)}t.exports=c})),bt=e.er(((e,t)=>{function n(e){var t=typeof e;return e!=null&&(t==`object`||t==`function`)}t.exports=n})),xt=e.er(((e,t)=>{var n=yt(),r=bt(),i=`[object AsyncFunction]`,a=`[object Function]`,o=`[object GeneratorFunction]`,s=`[object Proxy]`;function c(e){if(!r(e))return!1;var t=n(e);return t==a||t==o||t==i||t==s}t.exports=c})),St=e.er(((e,t)=>{t.exports=L()[`__core-js_shared__`]})),Ct=e.er(((e,t)=>{var n=St(),r=function(){var e=/[^.]+$/.exec(n&&n.keys&&n.keys.IE_PROTO||``);return e?`Symbol(src)_1.`+e:``}();function i(e){return!!r&&r in e}t.exports=i})),wt=e.er(((e,t)=>{var n=Function.prototype.toString;function r(e){if(e!=null){try{return n.call(e)}catch{}try{return e+``}catch{}}return``}t.exports=r})),Tt=e.er(((e,t)=>{var n=xt(),r=Ct(),i=bt(),a=wt(),o=/[\\^$.*+?()[\]{}|]/g,s=/^\[object .+?Constructor\]$/,c=Function.prototype,l=Object.prototype,u=c.toString,d=l.hasOwnProperty,f=RegExp(`^`+u.call(d).replace(o,`\\$&`).replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,`$1.*?`)+`$`);function p(e){return!i(e)||r(e)?!1:(n(e)?f:s).test(a(e))}t.exports=p})),Et=e.er(((e,t)=>{function n(e,t){return e?.[t]}t.exports=n})),Dt=e.er(((e,t)=>{var n=Tt(),r=Et();function i(e,t){var i=r(e,t);return n(i)?i:void 0}t.exports=i})),Ot=e.er(((e,t)=>{t.exports=Dt()(L(),`Map`)})),kt=e.er(((e,t)=>{t.exports=Dt()(Object,`create`)})),At=e.er(((e,t)=>{var n=kt();function r(){this.__data__=n?n(null):{},this.size=0}t.exports=r})),jt=e.er(((e,t)=>{function n(e){var t=this.has(e)&&delete this.__data__[e];return this.size-=t?1:0,t}t.exports=n})),Mt=e.er(((e,t)=>{var n=kt(),r=`__lodash_hash_undefined__`,i=Object.prototype.hasOwnProperty;function a(e){var t=this.__data__;if(n){var a=t[e];return a===r?void 0:a}return i.call(t,e)?t[e]:void 0}t.exports=a})),Nt=e.er(((e,t)=>{var n=kt(),r=Object.prototype.hasOwnProperty;function i(e){var t=this.__data__;return n?t[e]!==void 0:r.call(t,e)}t.exports=i})),Pt=e.er(((e,t)=>{var n=kt(),r=`__lodash_hash_undefined__`;function i(e,t){var i=this.__data__;return this.size+=this.has(e)?0:1,i[e]=n&&t===void 0?r:t,this}t.exports=i})),Ft=e.er(((e,t)=>{var n=At(),r=jt(),i=Mt(),a=Nt(),o=Pt();function s(e){var t=-1,n=e==null?0:e.length;for(this.clear();++t{var n=Ft(),r=ut(),i=Ot();function a(){this.size=0,this.__data__={hash:new n,map:new(i||r),string:new n}}t.exports=a})),Lt=e.er(((e,t)=>{function n(e){var t=typeof e;return t==`string`||t==`number`||t==`symbol`||t==`boolean`?e!==`__proto__`:e===null}t.exports=n})),Rt=e.er(((e,t)=>{var n=Lt();function r(e,t){var r=e.__data__;return n(t)?r[typeof t==`string`?`string`:`hash`]:r.map}t.exports=r})),zt=e.er(((e,t)=>{var n=Rt();function r(e){var t=n(this,e).delete(e);return this.size-=t?1:0,t}t.exports=r})),Bt=e.er(((e,t)=>{var n=Rt();function r(e){return n(this,e).get(e)}t.exports=r})),Vt=e.er(((e,t)=>{var n=Rt();function r(e){return n(this,e).has(e)}t.exports=r})),Ht=e.er(((e,t)=>{var n=Rt();function r(e,t){var r=n(this,e),i=r.size;return r.set(e,t),this.size+=r.size==i?0:1,this}t.exports=r})),Ut=e.er(((e,t)=>{var n=It(),r=zt(),i=Bt(),a=Vt(),o=Ht();function s(e){var t=-1,n=e==null?0:e.length;for(this.clear();++t{var n=ut(),r=Ot(),i=Ut(),a=200;function o(e,t){var o=this.__data__;if(o instanceof n){var s=o.__data__;if(!r||s.length{var n=ut(),r=dt(),i=ft(),a=pt(),o=mt(),s=Wt();function c(e){this.size=(this.__data__=new n(e)).size}c.prototype.clear=r,c.prototype.delete=i,c.prototype.get=a,c.prototype.has=o,c.prototype.set=s,t.exports=c})),Kt=e.er(((e,t)=>{var n=`__lodash_hash_undefined__`;function r(e){return this.__data__.set(e,n),this}t.exports=r})),qt=e.er(((e,t)=>{function n(e){return this.__data__.has(e)}t.exports=n})),Jt=e.er(((e,t)=>{var n=Ut(),r=Kt(),i=qt();function a(e){var t=-1,r=e==null?0:e.length;for(this.__data__=new n;++t{function n(e,t){for(var n=-1,r=e==null?0:e.length;++n{function n(e,t){return e.has(t)}t.exports=n})),Zt=e.er(((e,t)=>{var n=Jt(),r=Yt(),i=Xt(),a=1,o=2;function s(e,t,s,c,l,u){var d=s&a,f=e.length,p=t.length;if(f!=p&&!(d&&p>f))return!1;var m=u.get(e),h=u.get(t);if(m&&h)return m==t&&h==e;var g=-1,_=!0,v=s&o?new n:void 0;for(u.set(e,t),u.set(t,e);++g{t.exports=L().Uint8Array})),$t=e.er(((e,t)=>{function n(e){var t=-1,n=Array(e.size);return e.forEach(function(e,r){n[++t]=[r,e]}),n}t.exports=n})),en=e.er(((e,t)=>{function n(e){var t=-1,n=Array(e.size);return e.forEach(function(e){n[++t]=e}),n}t.exports=n})),tn=e.er(((e,t)=>{var n=gt(),r=Qt(),i=it(),a=Zt(),o=$t(),s=en(),c=1,l=2,u=`[object Boolean]`,d=`[object Date]`,f=`[object Error]`,p=`[object Map]`,m=`[object Number]`,h=`[object RegExp]`,g=`[object Set]`,_=`[object String]`,v=`[object Symbol]`,y=`[object ArrayBuffer]`,b=`[object DataView]`,x=n?n.prototype:void 0,S=x?x.valueOf:void 0;function C(e,t,n,x,C,w,T){switch(n){case b:if(e.byteLength!=t.byteLength||e.byteOffset!=t.byteOffset)return!1;e=e.buffer,t=t.buffer;case y:return!(e.byteLength!=t.byteLength||!w(new r(e),new r(t)));case u:case d:case m:return i(+e,+t);case f:return e.name==t.name&&e.message==t.message;case h:case _:return e==t+``;case p:var E=o;case g:var D=x&c;if(E||=s,e.size!=t.size&&!D)return!1;var O=T.get(e);if(O)return O==t;x|=l,T.set(e,t);var k=a(E(e),E(t),x,C,w,T);return T.delete(e),k;case v:if(S)return S.call(e)==S.call(t)}return!1}t.exports=C})),nn=e.er(((e,t)=>{function n(e,t){for(var n=-1,r=t.length,i=e.length;++n{t.exports=Array.isArray})),an=e.er(((e,t)=>{var n=nn(),r=rn();function i(e,t,i){var a=t(e);return r(e)?a:n(a,i(e))}t.exports=i})),on=e.er(((e,t)=>{function n(e,t){for(var n=-1,r=e==null?0:e.length,i=0,a=[];++n{function n(){return[]}t.exports=n})),cn=e.er(((e,t)=>{var n=on(),r=sn(),i=Object.prototype.propertyIsEnumerable,a=Object.getOwnPropertySymbols;t.exports=a?function(e){return e==null?[]:(e=Object(e),n(a(e),function(t){return i.call(e,t)}))}:r})),ln=e.er(((e,t)=>{function n(e,t){for(var n=-1,r=Array(e);++n{function n(e){return typeof e==`object`&&!!e}t.exports=n})),dn=e.er(((e,t)=>{var n=yt(),r=un(),i=`[object Arguments]`;function a(e){return r(e)&&n(e)==i}t.exports=a})),fn=e.er(((e,t)=>{var n=dn(),r=un(),i=Object.prototype,a=i.hasOwnProperty,o=i.propertyIsEnumerable;t.exports=n(function(){return arguments}())?n:function(e){return r(e)&&a.call(e,`callee`)&&!o.call(e,`callee`)}})),pn=e.er(((e,t)=>{function n(){return!1}t.exports=n})),mn=e.er(((e,t)=>{var n=L(),r=pn(),i=typeof e==`object`&&e&&!e.nodeType&&e,a=i&&typeof t==`object`&&t&&!t.nodeType&&t,o=a&&a.exports===i?n.Buffer:void 0;t.exports=(o?o.isBuffer:void 0)||r})),hn=e.er(((e,t)=>{var n=9007199254740991,r=/^(?:0|[1-9]\d*)$/;function i(e,t){var i=typeof e;return t??=n,!!t&&(i==`number`||i!=`symbol`&&r.test(e))&&e>-1&&e%1==0&&e{var n=9007199254740991;function r(e){return typeof e==`number`&&e>-1&&e%1==0&&e<=n}t.exports=r})),_n=e.er(((e,t)=>{var n=yt(),r=gn(),i=un(),a=`[object Arguments]`,o=`[object Array]`,s=`[object Boolean]`,c=`[object Date]`,l=`[object Error]`,u=`[object Function]`,d=`[object Map]`,f=`[object Number]`,p=`[object Object]`,m=`[object RegExp]`,h=`[object Set]`,g=`[object String]`,_=`[object WeakMap]`,v=`[object ArrayBuffer]`,y=`[object DataView]`,b=`[object Float32Array]`,x=`[object Float64Array]`,S=`[object Int8Array]`,C=`[object Int16Array]`,w=`[object Int32Array]`,T=`[object Uint8Array]`,E=`[object Uint8ClampedArray]`,D=`[object Uint16Array]`,O=`[object Uint32Array]`,k={};k[b]=k[x]=k[S]=k[C]=k[w]=k[T]=k[E]=k[D]=k[O]=!0,k[a]=k[o]=k[v]=k[s]=k[y]=k[c]=k[l]=k[u]=k[d]=k[f]=k[p]=k[m]=k[h]=k[g]=k[_]=!1;function A(e){return i(e)&&r(e.length)&&!!k[n(e)]}t.exports=A})),vn=e.er(((e,t)=>{function n(e){return function(t){return e(t)}}t.exports=n})),yn=e.er(((e,t)=>{var n=ht(),r=typeof e==`object`&&e&&!e.nodeType&&e,i=r&&typeof t==`object`&&t&&!t.nodeType&&t,a=i&&i.exports===r&&n.process;t.exports=function(){try{return i&&i.require&&i.require(`util`).types||a&&a.binding&&a.binding(`util`)}catch{}}()})),bn=e.er(((e,t)=>{var n=_n(),r=vn(),i=yn(),a=i&&i.isTypedArray;t.exports=a?r(a):n})),xn=e.er(((e,t)=>{var n=ln(),r=fn(),i=rn(),a=mn(),o=hn(),s=bn(),c=Object.prototype.hasOwnProperty;function l(e,t){var l=i(e),u=!l&&r(e),d=!l&&!u&&a(e),f=!l&&!u&&!d&&s(e),p=l||u||d||f,m=p?n(e.length,String):[],h=m.length;for(var g in e)(t||c.call(e,g))&&!(p&&(g==`length`||d&&(g==`offset`||g==`parent`)||f&&(g==`buffer`||g==`byteLength`||g==`byteOffset`)||o(g,h)))&&m.push(g);return m}t.exports=l})),Sn=e.er(((e,t)=>{var n=Object.prototype;function r(e){var t=e&&e.constructor;return e===(typeof t==`function`&&t.prototype||n)}t.exports=r})),Cn=e.er(((e,t)=>{function n(e,t){return function(n){return e(t(n))}}t.exports=n})),wn=e.er(((e,t)=>{t.exports=Cn()(Object.keys,Object)})),Tn=e.er(((e,t)=>{var n=Sn(),r=wn(),i=Object.prototype.hasOwnProperty;function a(e){if(!n(e))return r(e);var t=[];for(var a in Object(e))i.call(e,a)&&a!=`constructor`&&t.push(a);return t}t.exports=a})),En=e.er(((e,t)=>{var n=xt(),r=gn();function i(e){return e!=null&&r(e.length)&&!n(e)}t.exports=i})),Dn=e.er(((e,t)=>{var n=xn(),r=Tn(),i=En();function a(e){return i(e)?n(e):r(e)}t.exports=a})),On=e.er(((e,t)=>{var n=an(),r=cn(),i=Dn();function a(e){return n(e,i,r)}t.exports=a})),kn=e.er(((e,t)=>{var n=On(),r=1,i=Object.prototype.hasOwnProperty;function a(e,t,a,o,s,c){var l=a&r,u=n(e),d=u.length;if(d!=n(t).length&&!l)return!1;for(var f=d;f--;){var p=u[f];if(!(l?p in t:i.call(t,p)))return!1}var m=c.get(e),h=c.get(t);if(m&&h)return m==t&&h==e;var g=!0;c.set(e,t),c.set(t,e);for(var _=l;++f{t.exports=Dt()(L(),`DataView`)})),jn=e.er(((e,t)=>{t.exports=Dt()(L(),`Promise`)})),Mn=e.er(((e,t)=>{t.exports=Dt()(L(),`Set`)})),Nn=e.er(((e,t)=>{t.exports=Dt()(L(),`WeakMap`)})),Pn=e.er(((e,t)=>{var n=An(),r=Ot(),i=jn(),a=Mn(),o=Nn(),s=yt(),c=wt(),l=`[object Map]`,u=`[object Object]`,d=`[object Promise]`,f=`[object Set]`,p=`[object WeakMap]`,m=`[object DataView]`,h=c(n),g=c(r),_=c(i),v=c(a),y=c(o),b=s;(n&&b(new n(new ArrayBuffer(1)))!=m||r&&b(new r)!=l||i&&b(i.resolve())!=d||a&&b(new a)!=f||o&&b(new o)!=p)&&(b=function(e){var t=s(e),n=t==u?e.constructor:void 0,r=n?c(n):``;if(r)switch(r){case h:return m;case g:return l;case _:return d;case v:return f;case y:return p}return t}),t.exports=b})),Fn=e.er(((e,t)=>{var n=Gt(),r=Zt(),i=tn(),a=kn(),o=Pn(),s=rn(),c=mn(),l=bn(),u=1,d=`[object Arguments]`,f=`[object Array]`,p=`[object Object]`,m=Object.prototype.hasOwnProperty;function h(e,t,h,g,_,v){var y=s(e),b=s(t),x=y?f:o(e),S=b?f:o(t);x=x==d?p:x,S=S==d?p:S;var C=x==p,w=S==p,T=x==S;if(T&&c(e)){if(!c(t))return!1;y=!0,C=!1}if(T&&!C)return v||=new n,y||l(e)?r(e,t,h,g,_,v):i(e,t,x,h,g,_,v);if(!(h&u)){var E=C&&m.call(e,`__wrapped__`),D=w&&m.call(t,`__wrapped__`);if(E||D){var O=E?e.value():e,k=D?t.value():t;return v||=new n,_(O,k,h,g,v)}}return T?(v||=new n,a(e,t,h,g,_,v)):!1}t.exports=h})),In=e.er(((e,t)=>{var n=Fn(),r=un();function i(e,t,a,o,s){return e===t?!0:e==null||t==null||!r(e)&&!r(t)?e!==e&&t!==t:n(e,t,a,o,i,s)}t.exports=i})),Ln=e.er(((e,t)=>{var n=In();function r(e,t){return n(e,t)}t.exports=r})),Rn=e.ir(Ln()),zn=`__codex-desktop_initialize__`,Bn=10*6e4,Vn=5e3,Hn=150,Un=5e3,Wn=2e3,Gn=8,Kn=16,qn=[64*1024,256*1024,1024*1024];function Jn(e){return typeof e.method==`string`&&`id`in e&&e.id!=null}function Yn(e){return`id`in e&&(`result`in e||`error`in e)}function Xn(e){return typeof e.method==`string`&&!(`id`in e)}var Zn=class{connections=new Map;constructor(){}addConnection(e,t){this.connections.set(e,t)}removeConnection(e){this.connections.delete(e)}getMaybeConnection(e){return this.connections.get(e)??null}getConnection(e){let t=this.getMaybeConnection(e);if(!t)throw Error(`Connection for host ID ${e} not found`);return t}getAllHostIds(){return Array.from(this.connections.keys())}},Qn=class{logger=e.on(`ElectronAppServerConnection`);connection=null;connectionState=`disconnected`;errorMessage=null;reconnectTimer=null;reconnectDelayMs=Hn;reconnectAttempt=0;heartbeatTimer=null;heartbeatInFlight=!1;isStoppingConnection=!1;disposed=!1;initialized=!1;initializingPromise=null;resolveInitialize=null;rejectInitialize=null;listeners=new Set;pendingRequests=new Map;internalResponseHandlers=new Map;internalNotificationHandlers=new Set;ephemeralThreadIds=new e.Un({ttlMs:Bn});pendingEphemeralThreadStarts=0;fatalErrorMessage=null;mostRecentErrorMessage=null;isAppQuitting=!1;authTokenCache=null;authTokenPromise=null;pendingArchiveThreads=new Map;incomingLineProcessor;outgoingMessageSizeTracker=new e.Rt(qn);initializeStartedAtMs=null;disposables=new e.at;constructor(t,n){this.messageChannel=t,this.options=n,this.disposables.add(this.options.hostProcess.onShutdown(()=>{this.isAppQuitting=!0})),this.incomingLineProcessor=new e.S({logger:this.logger,decodePayload:er,onQueueOverflow:(e,t)=>{this.logger.warning(`incoming_line_queue_overflow`,{safe:{queueDepth:e,overflowThreshold:t,pendingRequestCount:this.pendingRequests.size,internalHandlerCount:this.internalResponseHandlers.size},sensitive:{}}),this.connection&&this.failConnection(this.connection,`Incoming line queue overflow`)},onMessage:e=>this.routeIncomingMessage(e)})}getCodexLocalCliExecutable(){return e.Ft(this.options.repoRoot,{resourcesPath:process.resourcesPath})}hasCustomCliExecutable(){return e.Pt()}registerWebviewWindow(e){this.listeners.add(e),e.onDestroyed(()=>{this.listeners.delete(e),this.dropPendingRequestsFor(e)}),e.send(this.messageChannel,this.createConnectionStatePayload()),this.fatalErrorMessage&&e.send(this.messageChannel,{type:`codex-app-server-fatal-error`,errorMessage:this.fatalErrorMessage})}notifyAutomationRunsUpdated(){this.broadcastToWindows({type:`automation-runs-updated`})}registerInternalNotificationHandler(e){return this.internalNotificationHandlers.add(e),()=>{this.internalNotificationHandlers.delete(e)}}markEphemeralThread(e){this.ephemeralThreadIds.add(e)}async updateThreadTitle(t,n){let r=n.trim();r.length!==0&&(e.Dt(t,r),await this.setThreadName(t,r),this.broadcastThreadTitleUpdated(t,r))}broadcastThreadTitleUpdated(t,n){this.broadcastToWindows({type:`thread-title-updated`,hostId:this.options.hostId,conversationId:t,title:n}),this.options.threadOverlayManager.handleThreadTitleUpdated(this.options.hostId,e.En(t),n)}async getAuthToken({refreshToken:e}){return await this.ensureReady(),!e&&this.authTokenCache?this.authTokenCache.value:e?this.fetchAuthToken(!0):(this.authTokenPromise||=this.fetchAuthToken(!1).finally(()=>{this.authTokenPromise=null}),this.authTokenPromise)}async getAuthMethod(){return(await this.requestAuthStatus(!1,!1))?.authMethod??null}async fetchAuthToken(e){try{let t=await this.requestAuthStatus(e),n=t&&t.authMethod===`chatgpt`?t.authToken??null:null;return this.authTokenCache={value:n},n}catch(e){throw this.authTokenCache=null,e instanceof Error?e:Error(String(e))}}async requestAuthStatus(e,t=!0){let n=`electron-auth:${(0,c.randomUUID)()}`,r=await this.sendInternalRequest({id:n,method:`getAuthStatus`,params:{includeToken:t,refreshToken:e}});if(r.error)throw Error(r.error.message??`Failed to retrieve auth status (code ${r.error.code})`);return r.result??null}async listSkills(e){await this.ensureReady();let t=`skills:${(0,c.randomUUID)()}`,n=await this.sendInternalRequest({id:t,method:`skills/list`,params:e});if(n.error)throw Error(n.error.message??`Failed to fetch skills from app server`);return n.result??{data:[]}}async listPlugins(e){await this.ensureReady();let t=`plugins:${(0,c.randomUUID)()}`,n=await this.sendInternalRequest({id:t,method:`plugin/list`,params:e});if(n.error)throw Error(n.error.message??`Failed to fetch plugins from app server`);return n.result??{marketplaces:[]}}async readConfig(e){await this.ensureReady();let t=`config/read:${(0,c.randomUUID)()}`,n=await this.sendInternalRequest({id:t,method:`config/read`,params:e});if(n.error)throw Error(n.error.message??`Failed to read config from app server`);return n.result}async getUserSavedConfiguration(e){return(await this.readConfig({includeLayers:!1,cwd:e??null})).config}async getConfigRequirements(){await this.ensureReady();let e=`configRequirements/read:${(0,c.randomUUID)()}`,t=await this.sendInternalRequest({id:e,method:`configRequirements/read`,params:void 0});if(t.error)throw Error(t.error.message??`Failed to read config requirements from app server`);return t.result}async startThread(t){await this.ensureReady();let n=e.Hn(this.options.sharedObjectRepository.get(`statsig_default_enable_features`),process.platform),r=e.Vn({...t,serviceName:t.serviceName??`codex_desktop`},n);t.ephemeral===!0&&(this.pendingEphemeralThreadStarts+=1);let i=`thread/start:${(0,c.randomUUID)()}`,a=await this.sendInternalRequest({id:i,method:`thread/start`,params:r});try{if(a.error)throw Error(a.error.message??`Failed to start thread (code ${a.error.code})`);let e=a.result;return t.ephemeral===!0&&this.markEphemeralThread(e.thread.id),e}finally{t.ephemeral===!0&&(this.pendingEphemeralThreadStarts=Math.max(0,this.pendingEphemeralThreadStarts-1))}}async startTurn(e){await this.ensureReady();let t=`turn/start:${(0,c.randomUUID)()}`,n=await this.sendInternalRequest({id:t,method:`turn/start`,params:e});if(n.error)throw Error(n.error.message??`Failed to start turn (code ${n.error.code})`);return n.result}async interruptTurn(e){await this.ensureReady();let t=`turn/interrupt:${(0,c.randomUUID)()}`,n=await this.sendInternalRequest({id:t,method:`turn/interrupt`,params:e});if(n.error)throw Error(n.error.message??`Failed to interrupt turn (code ${n.error.code})`);return n.result}async setThreadName(e,t){await this.ensureReady();let n=`thread/name/set:${(0,c.randomUUID)()}`,r=await this.sendInternalRequest({id:n,method:`thread/name/set`,params:{threadId:e,name:t}});if(r.error)throw Error(r.error.message??`Failed to set thread name (code ${r.error.code})`);return r.result}async readThread(e,t){await this.ensureReady();let n=`thread/read:${(0,c.randomUUID)()}`,r=await this.sendInternalRequest({id:n,method:`thread/read`,params:{threadId:e,includeTurns:t?.includeTurns??!1}});if(r.error)throw Error(r.error.message??`Failed to read thread (code ${r.error.code})`);return r.result.thread??null}sendInternalRequest(e){let t=this.toRequestKey(e.id);return this.options.desktopSentry.startSpan({op:`codex.cli.request`,name:e.method},()=>new Promise((n,r)=>{if(this.internalResponseHandlers.has(t)){r(Error(`Duplicate request id: ${t}`));return}let i={resolve:e=>{clearTimeout(i.timeout),n(e)},reject:e=>{clearTimeout(i.timeout),r(e)},timeout:setTimeout(()=>{let n=this.internalResponseHandlers.get(t);n&&(this.internalResponseHandlers.delete(t),n.reject(Error(`Timed out waiting for MCP response to ${e.method}`)))},3e4),startedAtMs:Date.now(),method:e.method,conversationId:this.getConversationIdFromParams(e.params)};this.internalResponseHandlers.set(t,i);try{this.sendMessage(e)}catch(e){this.internalResponseHandlers.delete(t),i.reject(e instanceof Error?e:Error(String(e)))}}))}clearAuthTokenCache(){this.authTokenCache=null,this.authTokenPromise=null}rejectAllInternalRequests(e){for(let[,t]of this.internalResponseHandlers)clearTimeout(t.timeout),t.reject(e);this.internalResponseHandlers.clear(),this.ephemeralThreadIds.clear(),this.pendingEphemeralThreadStarts=0}getConversationIdFromParams(e){if(!e||typeof e!=`object`)return;let t=e.conversationId;if(typeof t==`string`)return t;let n=e.threadId;return typeof n==`string`?n:void 0}registerArchiveThread(e,t){this.pendingArchiveThreads.set(e,t)}registerUnarchiveThread(e){this.pendingArchiveThreads.delete(e)}async handleClientRequest(e,t){await this.options.desktopSentry.startSpan({op:`codex.cli.client_request`,name:t.method},async()=>{this.logger.debug(`Client request`,{safe:{id:t.id,method:t.method},sensitive:{}});try{await this.ensureReady()}catch(n){this.sendErrorResponse(e,t.id,n);return}let n=this.toRequestKey(t.id),r=this.getConversationIdFromParams(t.params);this.pendingRequests.set(n,{id:t.id,sender:e,method:t.method,params:t.params,conversationId:r,startedAtMs:Date.now(),originWebContentsId:e.id});try{this.logger.debug(`bridge_forwarded_to_transport`,{safe:{requestId:n,method:t.method,conversationId:r??null,originWebcontentsId:e.id,transportKind:this.options.transport.kind,pendingCount:this.pendingRequests.size},sensitive:{}}),this.sendMessage(t)}catch(r){this.pendingRequests.delete(n),this.sendErrorResponse(e,t.id,r)}})}async handleClientNotification(e){this.logger.debug(`Client notification`,{safe:{method:e.method},sensitive:{}});try{await this.ensureReady()}catch(e){this.broadcastFatalError(`Failed to deliver notification: ${String(e)}`);return}this.sendMessage({method:e.method,params:e.params})}async handleClientResponse(e){this.logger.trace(`client_response`,{safe:{id:e.id},sensitive:{}});try{await this.ensureReady()}catch(e){this.broadcastFatalError(`Failed to deliver response: ${String(e)}`);return}this.sendMessage({id:e.id,result:e.result})}async restart(){this.logger.info(`Restart requested`),this.stopProcess(),this.fatalErrorMessage=null,await this.ensureReady()}getStdioIoStatsSnapshot(){return this.options.transport.kind!==`stdio`||!(this.options.transport instanceof e.Nt)?null:this.options.transport.getIoStatsSnapshot()}getTransportKind(){return this.options.transport.kind}getConnectionState(){return this.connectionState}getErrorMessage(){return this.errorMessage}dispose(){this.disposed=!0,this.stopProcess();try{this.options.transport.dispose?.()}catch(e){this.logger.warning(`Error while disposing app-server transport`,{safe:{transport:this.options.transport.kind,hostId:this.options.hostId},sensitive:{error:e}})}this.listeners.clear(),this.pendingRequests.clear(),this.disposables.dispose()}async ensureReady(){if(!this.initialized){this.initializingPromise||this.logger.info(`Initializing app-server transport`),this.initializingPromise||=this.startProcess();try{await this.initializingPromise}catch(e){throw this.initializingPromise=null,e}}}async ensureAuthenticated(){if(!this.options.ensureAuth)return;let e=await this.requestAuthStatus(!1,!1);if(e?.authMethod==null&&(e?.requiresOpenaiAuth??!0))throw Error(`Remote codex is not authenticated`)}async completeInitialization(){let e=this.initializeStartedAtMs==null?null:Date.now()-this.initializeStartedAtMs;this.logger.info(`initialize_handshake_result`,{safe:{initializeRequestId:zn,outcome:`success`,durationMs:e,transportKind:this.options.transport.kind},sensitive:{}}),this.initializeStartedAtMs=null,this.logger.info(`Codex CLI initialized`),this.initialized=!0,this.initializingPromise=null,this.reconnectDelayMs=Hn,this.reconnectAttempt=0,this.connection&&this.startHeartbeat(this.connection),await this.ensureAuthenticated(),this.broadcastToWindows({type:`codex-app-server-initialized`,hostId:this.options.hostId}),this.setConnectionState(`connected`),this.resolveInitialize=null,this.rejectInitialize=null}async startProcess(){return this.logger.info(`Starting app-server connection`,{safe:{transport:this.options.transport.kind,hostId:this.options.hostId},sensitive:{}}),this.setConnectionState(`connecting`),this.resolveInitialize=null,this.rejectInitialize=null,await this.connectAndWaitForOpen(),new Promise((e,t)=>{this.resolveInitialize=()=>{this.resolveInitialize=null,this.completeInitialization().then(()=>{e()}).catch(e=>{let t=e instanceof Error?e:Error(String(e));this.rejectInitialize?.(t)})},this.rejectInitialize=e=>{let n=this.initializeStartedAtMs==null?null:Date.now()-this.initializeStartedAtMs;this.logger.error(`initialize_handshake_result`,{safe:{initializeRequestId:zn,outcome:`failure`,durationMs:n,transportKind:this.options.transport.kind},sensitive:{errorMessage:e.message,errorName:e.name,errorStack:e.stack}}),this.initializeStartedAtMs=null,this.logger.error(`Initialization failed`,{safe:{errorMessage:e.message,errorName:e.name,errorStack:e.stack},sensitive:{}}),this.resolveInitialize=null,this.rejectInitialize=null,this.initializingPromise=null,this.options.transport.supportsReconnect()?this.connection?this.failConnection(this.connection,e.message):(this.setConnectionState(`disconnected`),this.scheduleReconnect()):this.broadcastFatalError(e.message),t(e)},this.sendInitializeRequest()})}async connectAndWaitForOpen(){this.mostRecentErrorMessage=null,this.initialized=!1,this.stopHeartbeat(),this.stopReconnectTimer(),this.resetIncomingLineQueue(),this.logger.debug(`Starting app-server transport`,{safe:{transport:this.options.transport.kind,hostId:this.options.hostId}});let t;try{t=await this.options.transport.connect()}catch(e){let t=e instanceof Error?e.message:String(e);throw this.logger.warning(`Failed to establish app-server transport`,{safe:{transport:this.options.transport.kind,hostId:this.options.hostId},sensitive:{error:e}}),this.mostRecentErrorMessage=t,this.setConnectionState(`disconnected`),this.options.transport.supportsReconnect()||this.broadcastFatalError(t),Error(t)}let n=e.Ht(t);this.logger.info(`Transport start success`,{safe:{connectionId:n,transport:this.options.transport.kind,hostId:this.options.hostId}}),this.connection=t;try{await this.waitForConnectionOpen(t)}catch(e){this.connection===t&&(this.connection=null);try{t.close()}catch{}let r=e instanceof Error?e.message:String(e);throw this.logger.warning(`App-server connection failed before ready`,{safe:{connectionId:n,transport:this.options.transport.kind},sensitive:{error:e}}),this.setConnectionState(`disconnected`),this.options.transport.supportsReconnect()||this.broadcastFatalError(r),Error(r)}}waitForConnectionOpen(t){let n=e.Ht(t);return new Promise((r,i)=>{let a=!1,o=()=>{a||(a=!0,r())},s=e=>{a||(a=!0,i(e))};t.onopen=()=>{if(this.connection!==t){this.logger.trace(`Ignoring open event from stale connection`,{safe:{staleConnectionId:n,currentConnectionId:this.connection?e.Ht(this.connection):null},sensitive:{}});return}this.logger.debug(`App-server connection open event`,{safe:{connectionId:n},sensitive:{}}),o()},t.onmessage=r=>{if(this.connection!==t){this.logger.trace(`Ignoring message event from stale connection`,{safe:{staleConnectionId:n,currentConnectionId:this.connection?e.Ht(this.connection):null},sensitive:{}});return}this.handleIncomingData(r.data)},t.onerror=r=>{if(this.connection!==t){this.logger.trace(`Ignoring error event from stale connection`,{safe:{staleConnectionId:n,currentConnectionId:this.connection?e.Ht(this.connection):null},sensitive:{}});return}let i=tr(r);this.mostRecentErrorMessage=i,this.options.transport.supportsReconnect()?this.logger.error(`Codex app-server websocket error`,{safe:{message:i},sensitive:{}}):this.logger.trace(`Codex CLI stderr`,{safe:{message:i},sensitive:{}})},t.onclose=e=>{let n=nr(e);this.handleTransportClosed(n,t),s(Error(this.options.transport.supportsReconnect()?`Codex app-server websocket closed (code=${n.code??`unknown`})`:`Codex app-server process closed`))},t.readyState===e.Vt.Open&&o()})}stopProcess(){if(this.stopReconnectTimer(),this.stopHeartbeat(),this.resetIncomingLineQueue(),this.connection){this.logger.info(`Stopping app-server transport`,{safe:{connectionId:e.Ht(this.connection),transport:this.options.transport.kind},sensitive:{}});let t=this.connection;this.connection=null,this.isStoppingConnection=!0;try{t.close()}catch(n){this.logger.warning(`Error while closing app-server connection`,{safe:{connectionId:e.Ht(t),transport:this.options.transport.kind},sensitive:{error:n}})}finally{this.isStoppingConnection=!1}}this.initialized=!1,this.initializingPromise=null,this.initializeStartedAtMs=null,this.resolveInitialize=null,this.rejectInitialize=null,this.reconnectAttempt=0,this.reconnectDelayMs=Hn,this.failPendingClientRequests(Error(`Codex app-server is not available`)),this.rejectAllInternalRequests(Error(`Codex app-server is not available`)),this.clearAuthTokenCache(),this.setConnectionState(`disconnected`)}handleTransportClosed(t,n){let r=e.Ht(n);if(this.connection!==n){this.logger.trace(`Ignoring close event from stale connection`,{safe:{staleConnectionId:r,currentConnectionId:this.connection?e.Ht(this.connection):null,code:t.code,reason:t.reason},sensitive:{}});return}this.drainIncomingLineQueue(!0);let i=this.initialized;if(this.logger.info(`App-server connection closed`,{safe:{connectionId:r,code:t.code,reason:t.reason,signal:t.signal,transport:this.options.transport.kind},sensitive:{}}),this.connection=null,this.stopHeartbeat(),this.resetIncomingLineQueue(),this.initialized=!1,this.initializingPromise=null,this.setConnectionState(`disconnected`),this.failPendingClientRequests(Error(`Codex app-server is not available`)),this.rejectAllInternalRequests(Error(`Codex app-server is not available`)),this.clearAuthTokenCache(),this.isStoppingConnection||this.isAppQuitting||this.disposed){this.logger.info(`Skipping reconnect after app-server close`,{safe:{connectionId:r},sensitive:{isStoppingConnection:this.isStoppingConnection,isAppQuitting:this.isAppQuitting,disposed:this.disposed}});return}if(!this.options.transport.supportsReconnect()){let e=this.isAppQuitting&&(t.signal===`SIGHUP`||t.signal===`SIGTERM`||t.signal===`SIGINT`),n=t.code===0||e;this.logger.error(`Codex CLI process exited`,{safe:{connectionId:r,code:t.code,signal:t.signal,transport:this.options.transport.kind,classifiedAsExpected:n},sensitive:{}});let a=t.code===0||e?null:` (code=${t.code}, signal=${t.signal}).\nMost recent error: ${this.mostRecentErrorMessage??`None`}`;if(!i&&this.rejectInitialize){this.rejectInitialize(Error(a??`Codex app-server process closed unexpectedly`)),this.rejectInitialize=null,this.resolveInitialize=null;return}a&&this.broadcastFatalError(a);return}if(this.logger.info(`transport_closed`,{safe:{transportKind:this.options.transport.kind,code:t.code,reason:t.reason,signal:t.signal,isAppQuitting:this.isAppQuitting,hasRecentTransportError:this.mostRecentErrorMessage!=null},sensitive:{error:this.mostRecentErrorMessage}}),this.mostRecentErrorMessage=t.reason??`Codex app-server websocket closed (code=${t.code??`unknown`})`,!i&&this.rejectInitialize){this.rejectInitialize(Error(`Codex app-server websocket closed (code=${t.code??`unknown`})`)),this.rejectInitialize=null,this.resolveInitialize=null;return}this.scheduleReconnect()}scheduleReconnect(){let e={unsupportedTransport:!this.options.transport.supportsReconnect(),timerAlreadyScheduled:this.reconnectTimer!=null,initializationInFlight:this.initializingPromise!=null,disposed:this.disposed,appQuitting:this.isAppQuitting};if(e.unsupportedTransport||e.timerAlreadyScheduled||e.initializationInFlight||e.disposed||e.appQuitting){this.logger.trace(`Reconnect scheduling skipped`,{safe:e,sensitive:{}});return}let t=this.reconnectDelayMs,n=this.reconnectAttempt+1;this.reconnectDelayMs=Math.min(this.reconnectDelayMs*2,Un),this.logger.info(`Scheduling app-server reconnect`,{safe:{reconnectAttempt:n,delayMs:t,nextDelayMs:this.reconnectDelayMs,hostId:this.options.hostId},sensitive:{}}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null;let e={alreadyInitialized:this.initialized,initializationInFlight:this.initializingPromise!=null,disposed:this.disposed,appQuitting:this.isAppQuitting};if(e.alreadyInitialized||e.initializationInFlight||e.disposed||e.appQuitting){this.logger.trace(`Reconnect attempt skipped`,{safe:{reconnectAttempt:n,...e},sensitive:{}});return}this.reconnectAttempt=n,this.logger.info(`Starting app-server reconnect attempt`,{safe:{reconnectAttempt:n,hostId:this.options.hostId},sensitive:{}}),this.initializingPromise=this.startProcess(),this.initializingPromise.catch(e=>{this.initializingPromise=null,this.logger.warning(`Codex app-server reconnect attempt failed`,{safe:{reconnectAttempt:n},sensitive:{error:e}}),!this.initialized&&!this.disposed&&!this.isAppQuitting&&this.scheduleReconnect()})},t)}stopReconnectTimer(){this.reconnectTimer&&=(clearTimeout(this.reconnectTimer),null)}failPendingClientRequests(e){for(let[,t]of this.pendingRequests)this.sendErrorResponse(t.sender,t.id,e);this.pendingRequests.clear()}setConnectionState(e){this.connectionState!==e&&(this.logger.info(`Codex app-server connection state changed`,{safe:{previous:this.connectionState,next:e,transport:this.options.transport.kind,hostId:this.options.hostId},sensitive:{mostRecentErrorMessage:this.mostRecentErrorMessage}}),this.connectionState=e,this.broadcastToWindows(this.createConnectionStatePayload()))}createConnectionStatePayload(){return{type:`codex-app-server-connection-changed`,hostId:this.options.hostId,state:this.connectionState,mostRecentErrorMessage:this.mostRecentErrorMessage,transport:this.options.transport.kind}}startHeartbeat(t){this.options.transport.supportsReconnect()&&(this.stopHeartbeat(),this.logger.debug(`Starting app-server heartbeat`,{safe:{connectionId:e.Ht(t),intervalMs:Vn},sensitive:{}}),this.heartbeatTimer=setInterval(()=>{this.runHeartbeat(t)},Vn))}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null,this.heartbeatInFlight=!1,this.logger.debug(`Stopped app-server heartbeat`))}async runHeartbeat(e){if(this.heartbeatInFlight){this.logger.trace(`Skipping app-server heartbeat while previous is in flight`);return}this.heartbeatInFlight=!0;try{await this.requestAuthStatus(!1,!1)}catch(t){this.connection===e&&(this.logger.warning(`Codex app-server heartbeat failed`,{safe:{error:t},sensitive:{}}),this.failConnection(e,t instanceof Error?t.message:String(t)))}finally{this.heartbeatInFlight=!1}}failConnection(t,n){if(this.connection!==t){this.logger.trace(`Ignoring failConnection for stale connection`,{safe:{staleConnectionId:e.Ht(t),currentConnectionId:this.connection?e.Ht(this.connection):null},sensitive:{}});return}let r=e.Ht(t);this.logger.warning(`Failing app-server connection`,{safe:{connectionId:r,reason:n},sensitive:{}}),this.mostRecentErrorMessage=n;try{t.close(void 0,n),setTimeout(()=>{this.connection===t&&(this.logger.warning(`Codex app-server close event not observed; forcing disconnect`,{safe:{connectionId:r,reason:n},sensitive:{}}),this.handleTransportClosed({type:`close`,code:null,reason:n},t))},Wn)}catch(e){this.logger.warning(`Failed to close unhealthy app-server connection`,{safe:{error:e},sensitive:{}}),this.handleTransportClosed({type:`close`,code:null,reason:n},t)}}sendInitializeRequest(){let t={id:zn,method:`initialize`,params:{clientInfo:{name:Le,title:`Codex Desktop`,version:this.options.hostProcess.getVersion()},capabilities:{experimentalApi:!0,optOutNotificationMethods:e.Mn.slice()}}};this.logger.debug(`Sending initialize request`),this.sendMessage(t)}handleIncomingData(e){this.incomingLineProcessor.handleIncomingData(e)}resetIncomingLineQueue(){this.incomingLineProcessor.reset()}drainIncomingLineQueue(e){e&&this.incomingLineProcessor.flush()}trackOutgoingMessageBytes(e,t){let n=this.outgoingMessageSizeTracker.track(e);for(let e of n)this.logger.debug(`outgoing_message_size_threshold`,{safe:{threshold:e.threshold,messageBytes:e.sizeBytes,messageBytesHighWaterMark:e.sizeBytesHighWaterMark,...t},sensitive:{}})}routeIncomingMessage(e){if(!this.initialized){if(Yn(e)&&e.id===zn&&this.resolveInitialize){if(e.error){let t=Error(`Failed to initialize Codex app-server: ${JSON.stringify(e.error)}`);this.rejectInitialize?.(t)}else this.resolveInitialize();return{routeKind:`initialize_response`,method:null}}if(Yn(e)&&this.internalResponseHandlers.has(this.toRequestKey(e.id)))return this.routeResponse(e),{routeKind:`response`,method:null};let t=typeof e.method==`string`?e.method:null;return this.logger.warning(`Message before initialization`,{safe:{hasId:`id`in e?e.id??null:null,method:t},sensitive:{}}),{routeKind:`before_initialize`,method:t}}if(Yn(e))return this.logger.trace(`Server response received`,{safe:{id:e.id,hasError:e.error!=null},sensitive:{}}),this.routeResponse(e),{routeKind:`response`,method:null};if(Jn(e))return this.logger.trace(`Server request received`,{safe:{id:e.id,method:e.method},sensitive:{}}),this.broadcastToWindows({type:`mcp-request`,hostId:this.options.hostId,request:e}),{routeKind:`server_request`,method:e.method};if(Xn(e)){this.logger.trace(`Server notification received`,{safe:{method:e.method},sensitive:{}}),e.method===`account/updated`&&this.clearAuthTokenCache();for(let t of this.internalNotificationHandlers)try{t(e)}catch(t){this.logger.warning(`Internal notification handler failed`,{safe:{method:e.method},sensitive:{error:t}})}if(e.method===`thread/started`&&this.pendingEphemeralThreadStarts>0){let t=e.params,n=t?.thread,r=typeof n?.id==`string`?n.id:typeof t?.threadId==`string`?t.threadId:null;if(r&&n?.path==null)return this.markEphemeralThread(r),{routeKind:`notification_ephemeral_thread_started`,method:e.method}}let t=e.params,n=typeof t?.thread?.id==`string`?t.thread.id:typeof t?.threadId==`string`?t.threadId:null;return n&&this.ephemeralThreadIds.has(n)?(this.logger.debug(`notification_dropped_ephemeral`,{safe:{method:e.method,threadId:n,reason:`ephemeral_filter`},sensitive:{}}),{routeKind:`notification_dropped_ephemeral`,method:e.method}):(this.broadcastToWindows({type:`mcp-notification`,hostId:this.options.hostId,method:e.method,params:e.params}),{routeKind:`notification`,method:e.method})}return this.logger.warning(`Unrecognized MCP message`,{safe:$n(e),sensitive:{}}),{routeKind:`unrecognized`,method:null}}routeResponse(t){let n=this.toRequestKey(t.id),r=this.pendingRequests.get(n),i=r?.method===`thread/archive`||r?.method===`thread/unarchive`?r.conversationId??null:null,a=r?.method===`thread/archive`?r.conversationId:null,o=a==null?null:this.pendingArchiveThreads.get(a)??null;i!=null&&this.pendingArchiveThreads.delete(i);let s=this.internalResponseHandlers.get(n);if(s){let e=Date.now()-s.startedAtMs;if(this.internalResponseHandlers.delete(n),clearTimeout(s.timeout),t.error){this.logger.debug(`internal_request_failed`,{safe:{requestId:n,method:s.method,durationMs:e,errorCode:t.error.code},sensitive:{}});let r=Error(t.error.message??`MCP request ${n} failed with code ${t.error.code}`);s.reject(r)}else this.logger.debug(`internal_request_completed`,{safe:{requestId:n,method:s.method,durationMs:e},sensitive:{}}),s.resolve(t);return}if(r&&t.error&&this.logger.debug(`MCP request failed`,{safe:{id:t.id,method:r.method,conversationId:r.conversationId??null},sensitive:{error:t.error}}),r&&!t.error)switch(r.method){case`turn/start`:r.conversationId&&(this.isAutomationSeedTurn(r.params)||(e.Ct(r.conversationId),this.notifyAutomationRunsUpdated()));break;case`thread/unarchive`:{let t=r.conversationId;t&&e.Ot(t)&&this.notifyAutomationRunsUpdated();break}case`thread/archive`:{let t=r.conversationId;t&&(e.wt(t,`auto`),this.notifyAutomationRunsUpdated(),this.captureAutomationArchiveMessages(t).catch(e=>{this.options.errorReporter.reportNonFatal(e,{kind:`capture-automation-archive-messages`})}),o?.cleanupWorktree&&o.cwd!=null&&this.cleanupArchivedWorktree(t,o.cwd));break}default:break}if(this.logger.trace(`Routing response`,{safe:{id:t.id,hasTarget:r!=null},sensitive:{}}),!r&&typeof t.id==`string`&&t.id.startsWith(`electron-auto-archive:`)){this.logger.trace(`Auto-archive response received`,{safe:{id:t.id},sensitive:{error:t.error}}),this.broadcastToWindows({type:`mcp-response`,hostId:this.options.hostId,message:t}),t.error||this.broadcastToWindows({type:`tasks-reload-requested`});return}if(r||this.logger.warning(`response_orphaned`,{safe:{requestId:n,transportKind:this.options.transport.kind,hasError:t.error!=null},sensitive:{error:t.error}}),r&&!t.error)switch(r.method){case`thread/archive`:this.broadcastToWindows({type:e.Tn.CHANGED}),this.broadcastToWindows({type:`tasks-reload-requested`});break;default:break}let c=r?.sender,l=r?Date.now()-r.startedAtMs:null,u=c?c.isDestroyed():null;this.logger.info(`response_routed`,{safe:{requestId:n,method:r?.method??null,conversationId:r?.conversationId??null,originWebcontentsId:r?.originWebContentsId??null,durationMs:l,hadPending:r!=null,hadInternalHandler:!1,targetDestroyed:u,broadcastFallback:r!=null&&u===!0,errorCode:t.error?.code??null},sensitive:{}}),c&&!c.isDestroyed()?c.send(this.messageChannel,{type:`mcp-response`,hostId:this.options.hostId,message:t}):this.broadcastToWindows({type:`mcp-response`,hostId:this.options.hostId,message:t}),this.pendingRequests.delete(n)}async captureAutomationArchiveMessages(t){let n=await this.sendInternalRequest({id:`thread/resume:${(0,c.randomUUID)()}`,method:`thread/resume`,params:{threadId:t,history:null,path:null,model:null,modelProvider:null,cwd:null,approvalPolicy:null,sandbox:null,config:null,baseInstructions:null,developerInstructions:null,personality:null,persistExtendedHistory:!1}});if(n.error){this.logger.warning(`Failed to resume thread for automation archive`,{safe:{threadId:t},sensitive:{error:n.error}});return}let r=n.result.thread.turns.flatMap(e=>e.items),i=this.getLastUserMessage(r),a=this.getLastAssistantMessage(r);i==null&&a==null||e.Et(t,i,a)}async cleanupArchivedWorktree(t,n){try{let r=await this.options.requestGitWorker({method:`resolve-worktree-for-thread`,params:{cwd:e.zn(n),conversationId:e.En(t),hostConfig:this.options.hostConfig}});if(r.worktreeGitRoot==null)return;await this.options.requestGitWorker({method:`delete-worktree`,params:{worktree:r.worktreeGitRoot,hostConfig:this.options.hostConfig,force:!0,reason:`archive-cleanup`}}),this.broadcastToWindows({type:`worktrees-reload-requested`,hostId:this.options.hostId})}catch(e){this.logger.warning(`Failed to clean up archived worktree`,{safe:{conversationId:t},sensitive:{error:e}})}}getLastUserMessage(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(n.type===`userMessage`)return this.formatUserInput(n.content)}return null}getLastAssistantMessage(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(n.type===`agentMessage`)return n.text}return null}formatUserInput(e){let t=[];for(let n of e)switch(n.type){case`text`:t.push(n.text);break;case`image`:t.push(`image: ${n.url}`);break;case`localImage`:t.push(`localImage: ${n.path}`);break;case`skill`:t.push(`skill: ${n.name} (${n.path})`);break;case`mention`:t.push(`mention: ${n.name} (${n.path})`)}return t.join(` -`)}isAutomationSeedTurn(e){if(!e||typeof e!=`object`)return!1;let t=e.input;if(!Array.isArray(t))return!1;for(let e of t)if(e.type===`text`&&e.text.includes(`Automation ID:`))return!0;return!1}sendMessage(e){if(!this.connection)throw Error(`Codex app-server is not available`);let t=$n(e),n=JSON.stringify(e),r=Buffer.byteLength(n,`utf8`);this.trackOutgoingMessageBytes(r,t),this.logger.trace(`Sending message to transport`,{safe:{transportKind:this.options.transport.kind,messageBytes:r},sensitive:{},...t}),this.connection.send(n)}broadcastToWindows(e){let t=Date.now(),n=0,r=0,i=typeof e.type==`string`?e.type:null;for(let t of Array.from(this.listeners)){if(t.isDestroyed()){this.listeners.delete(t),r+=1;continue}let a=Date.now();this.logger.trace(`Forwarding payload to window`,{safe:{type:e.type,webContentsId:t.id},sensitive:{}});try{t.send(this.messageChannel,e)}catch(e){throw this.logger.warning(`window_broadcast_send_failed`,{safe:{type:i,webContentsId:t.id,sendDurationMs:Date.now()-a},sensitive:{error:e}}),e}let o=Date.now()-a;o>=Gn&&this.logger.debug(`window_broadcast_send_slow`,{safe:{type:i,webContentsId:t.id,sendDurationMs:o},sensitive:{}}),n+=1}let a=Date.now()-t;(a>=Kn||r>0)&&this.logger.debug(`window_broadcast_completed`,{safe:{type:i,sentCount:n,skippedDestroyedCount:r,broadcastDurationMs:a},sensitive:{}})}sendErrorResponse(e,t,n){let r=n&&typeof n.code==`string`?n.code:void 0,i={id:t,error:{code:-32e3,message:n instanceof Error?n.message:typeof n==`string`?n:`Codex app-server is not available`,...r?{data:{code:r}}:{}}};e.isDestroyed()||e.send(this.messageChannel,{type:`mcp-response`,hostId:this.options.hostId,message:i})}broadcastFatalError(e){this.fatalErrorMessage=e,this.logger.error(`fatal_error_broadcasted`,{safe:{transportKind:this.options.transport.kind,initialized:this.initialized,pendingRequestCount:this.pendingRequests.size,internalHandlerCount:this.internalResponseHandlers.size,listenerCount:this.listeners.size},sensitive:{errorMessage:e}}),this.options.errorReporter.reportFatal(e,{kind:`codex-app-server-fatal-error`}),this.rejectAllInternalRequests(Error(e)),this.clearAuthTokenCache(),this.broadcastToWindows({type:`codex-app-server-fatal-error`,errorMessage:e})}dropPendingRequestsFor(e){for(let[t,n]of this.pendingRequests.entries())n.sender.id===e.id&&this.pendingRequests.delete(t)}toRequestKey(e){return String(e)}};function $n(e){return`method`in e&&`id`in e?{kind:Jn(e)?`server-request`:`client-request`,id:e.id,method:e.method}:`method`in e?{kind:`notification`,method:e.method}:`result`in e||`error`in e?{kind:`response`,id:e.id,hasError:`error`in e&&e.error!=null}:{kind:`unknown`}}function er(e){if(e==null)return[];let t=null;return typeof e==`string`?t=e:e instanceof ArrayBuffer?t=Buffer.from(e).toString(`utf8`):ArrayBuffer.isView(e)&&(t=Buffer.from(e.buffer,e.byteOffset,e.byteLength).toString(`utf8`)),t?t.split(/\r?\n/).filter(e=>e.trim().length>0):[]}function tr(e){if(e&&typeof e==`object`){let t=e.message;if(typeof t==`string`&&t.length>0)return t;let n=e.error;if(n instanceof Error&&n.message.length>0)return n.message}return`Codex app-server connection error`}function nr(e){if(!e||typeof e!=`object`)return{};let t=e.code,n=e.reason,r=e.signal,i=e.wasClean;return{code:typeof t==`number`?t:null,reason:typeof n==`string`?n:null,signal:typeof r==`string`?r:null,wasClean:typeof i==`boolean`?i:void 0}}const rr=`ssh_websocket_v0`;var ir={remoteAppServerPort:9234,loopbackHost:`127.0.0.1`,remoteBootstrapLogPath:`/tmp/codex-app-server-ssh-ws-v0.log`},ar={tunnelProbe:200,tunnelReady:5e3,tunnelReadyPollInterval:100},or={maxAttempts:1e3,maxPort:65535},sr={batchMode:`BatchMode=yes`,exitOnForwardFailure:`ExitOnForwardFailure=yes`,connectTimeoutSeconds:10,serverAliveIntervalSeconds:15,serverAliveCountMax:4},cr=new Set,lr=Promise.resolve();function ur(){return[`-o`,sr.batchMode,`-o`,`ConnectTimeout=${sr.connectTimeoutSeconds}`,`-o`,`ServerAliveInterval=${sr.serverAliveIntervalSeconds}`,`-o`,`ServerAliveCountMax=${sr.serverAliveCountMax}`]}function dr(e){if(e.alias?.trim())return[e.alias.trim()];let t=[];return e.identity?.trim()&&t.push(`-i`,e.identity.trim()),e.port!=null&&t.push(`-p`,String(e.port)),t.push(e.host),t}function fr(e){let t=e[rr];return t?.sshHost?t:null}var pr=class{kind=`websocket`;logger=e.on(`AppServerTransportSshWebsocket`);tunnelProcess=null;tunnelStderr=``;claimedLocalPort=null;constructor(e){this.options=e}supportsReconnect(){return!0}dispose(){this.resetLocalTunnelState()}async connect(){return await this.ensureRemoteAppServerAndTunnel(),new e.Bt(new e.Mt(this.options.websocketUrl,{perMessageDeflate:!1}))}async ensureRemoteAppServerAndTunnel(){let e=hr(this.options.websocketUrl),t=await this.claimLocalTunnelPort(e),n=this.options.remotePort;this.logger.info(`[ssh-websocket-v0] ensuring remote app-server and tunnel`,{safe:{localPort:t,remotePort:n},sensitive:{sshAlias:this.options.sshConnection.alias,sshHost:this.options.sshConnection.host,sshPort:this.options.sshConnection.port,websocketUrl:this.options.websocketUrl}}),await this.startRemoteAppServer(n);try{if(await this.startTunnel(t,n),!await Sr(ir.loopbackHost,t,ar.tunnelReady))throw Error(`[ssh-websocket-v0] local tunnel did not become ready on ${ir.loopbackHost}:${t}${this.tunnelStderr?` (${this.tunnelStderr.trim()})`:``}`)}catch(e){throw this.logger.warning(`[ssh-websocket-v0] local tunnel startup/readiness failed; resetting tunnel state for retry`,{safe:{localPort:t,remotePort:n,tunnelStderr:this.tunnelStderr.trim()}}),this.resetLocalTunnelState(),e}}releaseClaimedLocalPort(){this.claimedLocalPort!=null&&(yr(this.claimedLocalPort),this.claimedLocalPort=null)}resetLocalTunnelState(){this.tunnelProcess&&=(this.tunnelProcess.kill(),null),this.releaseClaimedLocalPort()}async claimLocalTunnelPort(e){if(this.claimedLocalPort!=null)return this.claimedLocalPort;let t=await vr({host:ir.loopbackHost,startPort:e});return this.claimedLocalPort=t,t===e?t:(this.logger.warning(`[ssh-websocket-v0] selected alternate local tunnel port`,{safe:{requestedLocalPort:e,localPort:t},sensitive:{}}),this.options.websocketUrl=gr(this.options.websocketUrl,t),t)}async startRemoteAppServer(t){let n=ir.remoteBootstrapLogPath,r=Ye([`nohup codex`,` app-server --listen ws://127.0.0.1:`,String(t),` >${n} 2>&1 &`].join(``)),{code:i,stdout:a,stderr:o}=await e.Xt({args:[`ssh`,...ur(),...dr(this.options.sshConnection),r]}).wait();if(i!==0){this.logger.warning(`[ssh-websocket-v0] remote app-server bootstrap failed`,{safe:{remotePort:t,code:i},sensitive:{sshAlias:this.options.sshConnection.alias,sshHost:this.options.sshConnection.host,sshPort:this.options.sshConnection.port,identity:this.options.sshConnection.identity,remoteLogPath:n,stdout:a,stderr:o}});let e=o||a||`Unknown error`;throw Error(e)}}async startTunnel(e,t){if(this.tunnelProcess&&!this.tunnelProcess.killed)return;this.tunnelStderr=``,this.logger.info(`[ssh-websocket-v0] starting local tunnel`,{safe:{localPort:e,remotePort:t},sensitive:{sshAlias:this.options.sshConnection.alias,sshHost:this.options.sshConnection.host,sshPort:this.options.sshConnection.port,identity:this.options.sshConnection.identity}});let n=(0,o.spawn)(`ssh`,[`-N`,`-L`,`${e}:127.0.0.1:${t}`,`-o`,sr.exitOnForwardFailure,...ur(),...dr(this.options.sshConnection)],{stdio:[`ignore`,`ignore`,`pipe`]});this.tunnelProcess=n,n.stderr?.on(`data`,e=>{this.tunnelStderr=`${this.tunnelStderr}${e.toString(`utf8`)}`.slice(-4e3)}),n.on(`error`,r=>{this.tunnelStderr=`${this.tunnelStderr}${String(r)}`.slice(-4e3),this.tunnelProcess===n&&(this.tunnelProcess=null),this.logger.warning(`[ssh-websocket-v0] local tunnel process failed`,{safe:{localPort:e,remotePort:t},sensitive:{sshAlias:this.options.sshConnection.alias,sshHost:this.options.sshConnection.host,sshPort:this.options.sshConnection.port,identity:this.options.sshConnection.identity,error:r}})}),n.on(`close`,(r,i)=>{this.tunnelProcess===n&&(this.tunnelProcess=null),this.logger.info(`[ssh-websocket-v0] local tunnel process closed`,{safe:{localPort:e,remotePort:t,code:r,signal:i},sensitive:{sshAlias:this.options.sshConnection.alias,sshHost:this.options.sshConnection.host,sshPort:this.options.sshConnection.port,stderrTail:this.tunnelStderr.trim().slice(-300)}})})}};function mr(e){if(process.env.CODEX_APP_SERVER_FORCE_CLI===`1`||e.kind!==`ssh`)return null;let t=e.websocket_url??null;if(!t)return null;let n=fr(e);return n?{websocketUrl:t,sshConnection:{alias:n.sshAlias,host:n.sshHost,port:n.sshPort,identity:n.identity},remotePort:n.remotePort??ir.remoteAppServerPort}:null}function hr(e){let t=new URL(e),n=Number(t.port||(t.protocol===`wss:`?`443`:`80`));if(!Number.isFinite(n)||n<=0)throw Error(`[ssh-websocket-v0] invalid websocket URL port in ${e}`);return n}function gr(e,t){let n=new URL(e);return n.port=String(t),n.toString()}async function _r(e){let t=lr,n=()=>{};lr=new Promise(e=>{n=()=>{e()}}),await t;try{return await e()}finally{n()}}async function vr({host:e,startPort:t}){return _r(async()=>{for(let n=0;nor.maxPort)break;if(!cr.has(r)&&await br(e,r))return cr.add(r),r}throw Error(`[ssh-websocket-v0] no available local tunnel port found after ${or.maxAttempts} attempts starting at ${t}`)})}function yr(e){cr.delete(e)}function br(e,t){return new Promise(n=>{let r=l.default.createServer(),i=!1,a=e=>{i||(i=!0,n(e))};r.once(`error`,()=>{a(!1)}),r.once(`listening`,()=>{r.close(()=>{a(!0)})}),r.listen(t,e)})}function xr(e,t,n){return new Promise(r=>{let i=l.default.connect({host:e,port:t}),a=!1,o=e=>{a||(a=!0,i.destroy(),r(e))};i.once(`connect`,()=>o(!0)),i.once(`error`,()=>o(!1)),i.setTimeout(n,()=>o(!1))})}async function Sr(e,t,n){let r=Date.now();for(;Date.now()-r>> Codex managed SSH connections >>>`,Er=`# <<< Codex managed SSH connections <<<`,Dr=`# codex-display-name:`,Or=`Codex-managed SSH block is malformed`;function kr(t=wr){try{let n=jr(t);return n==null?[]:Fr(n).map(t=>({hostId:e.Nn(t.sshAlias),displayName:t.displayName,source:`codex-managed`,autoConnect:!1,sshAlias:t.sshAlias,sshHost:t.sshHost,sshPort:t.sshPort,identity:t.identity}))}catch(e){return Cr().warning(`failed to read Codex-managed SSH connections`,{safe:{entrypointPath:t},sensitive:{error:e}}),[]}}function Ar(e,t=wr){let n=e.filter(e=>e.source===`codex-managed`),i=jr(t)??``,o=Pr(Nr(i),Mr(n));o!==i&&(a.default.mkdirSync(r.default.dirname(t),{recursive:!0}),a.default.writeFileSync(t,o,`utf8`))}function jr(e){return a.default.existsSync(e)?a.default.readFileSync(e,`utf8`):null}function Mr(e){if(e.length===0)return``;let t=[Tr];for(let n of e){let e=n.sshAlias?.trim(),r=n.sshHost.trim();!e||!r||(t.push(`${Dr} ${n.displayName}`),t.push(`Host ${e}`),t.push(` HostName ${r}`),n.sshPort!=null&&t.push(` Port ${n.sshPort}`),n.identity!=null&&t.push(` IdentityFile ${n.identity}`),t.push(``))}return t.length===1?``:(t.push(Er),`${t.join(` -`)}\n`)}function Nr(e){let t=e.split(/\r?\n/u),n=t.findIndex(e=>e.trim()===Tr),r=t.findIndex((e,t)=>t>n&&e.trim()===Er);if(n<0&&r<0)return e;if(n<0||r<0)throw Error(Or);let i=t.slice(0,n),a=t.slice(r+1);return[...i,...a].join(` -`)}function Pr(e,t){let n=e.trimEnd();return t.length===0?n.length===0?``:`${n}\n`:n.length===0?t:`${n}\n\n${t}`}function Fr(e){let t=Ir(e),n=[],r=null,i=null;for(let e of t){let t=e.trim();if(t.length===0)continue;if(t.startsWith(Dr)){r=t.slice(21).trim()||null;continue}let a=Lr(t);if(a!=null){if(a.keyword===`host`){let e=a.value.split(/\s+/u)[0];if(e==null||e.length===0){i=null;continue}i={displayName:r??e,sshAlias:e,sshHost:``,sshPort:null,identity:null},n.push(i),r=null;continue}if(i!=null){if(a.keyword===`hostname`){i.sshHost=a.value;continue}if(a.keyword===`port`){let e=Number(a.value);Number.isSafeInteger(e)&&(i.sshPort=e);continue}a.keyword===`identityfile`&&a.value!==`none`&&(i.identity=a.value)}}}return n.filter(e=>e.sshAlias.length>0&&e.sshHost.length>0)}function Ir(e){let t=e.split(/\r?\n/u),n=t.findIndex(e=>e.trim()===Tr);if(n<0)return[];let r=t.findIndex((e,t)=>t>n&&e.trim()===Er);return r<0?t.slice(n+1):t.slice(n+1,r)}function Lr(e){let t=/^(?\S+?)(?:\s*=\s*|\s+)(?.+)$/u.exec(e),n=t?.groups?.keyword?.toLowerCase(),r=t?.groups?.value?.trim();return n==null||r==null||r.length===0?null:{keyword:n,value:r}}var Rr=e.sn(`discover-ssh-config-remote-connections`),zr=/[!*?[\]]/,Br=/[*?[\]{}()]/;async function Vr(e){return Hr(r.default.join(n.default.homedir(),`.ssh`,`config`),e)}async function Hr(e,t){try{if(!a.default.existsSync(e))return[];let n=Ur(e).filter(e=>!t?.excludedSshAliases?.has(e));return(await Promise.all(n.map(t=>Wr({entrypointPath:e,sshAlias:t})))).filter(e=>e!=null).sort((e,t)=>e.displayName.localeCompare(t.displayName))}catch(e){throw Rr().warning(`failed to discover remote SSH connections`,{safe:{},sensitive:{error:e}}),Error(`Failed to discover remote SSH connections.`)}}function Ur(e){let t=new Set,n=new Set,i=r.default.dirname(r.default.resolve(e));return Gr({configPath:r.default.resolve(e),includeBaseDirectory:i,discoveredAliases:t,visitedConfigPaths:n}),Array.from(t)}async function Wr({entrypointPath:t,sshAlias:n}){let{stdout:r,stderr:i,code:a}=await e.Xt({args:[`ssh`,`-G`,`-F`,t,n]}).wait();if(a!==0)return Rr().warning(`failed to resolve discovered SSH alias`,{safe:{},sensitive:{stderr:i}}),null;let o=qr(r);return o.hostname?{hostId:e.Pn(n),displayName:n,source:`discovered`,autoConnect:!1,sshAlias:n,sshHost:o.hostname,sshPort:o.port,identity:o.identity}:null}function Gr({configPath:e,includeBaseDirectory:t,discoveredAliases:n,visitedConfigPaths:r}){if(r.has(e))return;r.add(e);let i=a.default.readFileSync(e,`utf8`),o=!1;for(let e of i.split(/\r?\n/u)){let i=Xr(e);if(i.length===0)continue;let a=Kr(i);if(a!=null){if(a.keyword===`match`){o=!0;continue}if(a.keyword===`host`){for(let e of Yr(a.value))n.add(e);o=!0;continue}if(!o&&a.keyword===`include`){for(let e of Jr({includeBaseDirectory:t,includeValue:a.value}))Gr({configPath:e,includeBaseDirectory:t,discoveredAliases:n,visitedConfigPaths:r});continue}}}}function Kr(e){let t=/^(?\S+?)(?:\s*=\s*|\s+)(?.+)$/u.exec(e),n=t?.groups?.keyword?.toLowerCase(),r=t?.groups?.value?.trim();return n==null||r==null||r.length===0?null:{keyword:n,value:r}}function qr(e){let t=null,n=null,r=null;for(let i of e.split(/\r?\n/u)){let e=i.trim();if(e.length===0)continue;let a=Kr(e);if(a!=null){if(a.keyword===`hostname`&&t==null){t=a.value;continue}if(a.keyword===`port`&&n==null){let e=Number(a.value);Number.isSafeInteger(e)&&(n=e);continue}a.keyword===`identityfile`&&r==null&&a.value!==`none`&&(r=a.value)}}return{hostname:t,port:n,identity:r}}function Jr({includeBaseDirectory:e,includeValue:t}){let n=[];for(let i of t.split(/\s+/u)){let t=Zr(i),o=r.default.isAbsolute(t)?t:r.default.resolve(e,t),s=a.default.globSync(o);if(s.length===0&&!Br.test(o)){a.default.existsSync(o)&&n.push(o);continue}n.push(...s.filter(e=>a.default.statSync(e).isFile()))}return n}function Yr(e){return e.split(/\s+/u).filter(e=>e.length>0).filter(e=>!zr.test(e))}function Xr(e){let t=!1,n=!1;for(let r=0;rR(e,t))}function $r(t){return ei(t,e.nn())}function ei(t,n){return n?e.Qt(t,e.$t()):t}function ti(e,t){return e.map(e=>ei(e,t))}function ni(t){return t?{preferWslPaths:!0,convertWindowsPathToWslPath:e.Zt,convertWslPathToWindowsPath:e=>ei(e,!0)}:{preferWslPaths:!1}}function ri(t,n,r){return e.In(n)?t:r?R(t,!0):di(t)?ei(t,!0):t}function ii(e,t,n){return li(e,e=>ri(e,t,n))}function ai(e,t,n){return ui(e,e=>ri(e,t,n))}function oi(t,n){return process.platform!==`win32`||e.In(n)||!di(t)?t:ei(t,!0)}function si(e,t){return li(e,e=>oi(e,t))}function ci(e,t){return e?ui(e,e=>oi(e,t)):{}}function li(e,t){let n=new Set;return e.map(t).filter(e=>n.has(e)?!1:(n.add(e),!0))}function ui(e,t){let n={};return Object.entries(e).forEach(([e,r])=>{let i=t(e);n[i]??(n[i]=r)}),n}function di(t){return e.en(t)||t===`/`||/^\/(?!\/)/.test(t)}var fi=(0,i.promisify)(p.gzip),pi=`application/gzip`,mi=`feedback`,hi=Buffer.from(` -`),gi=`application/x-sentry-envelope`,_i=512,vi=2;async function yi({appVersion:t,buildFlavor:n,buildNumber:r,classification:i,description:a,includeLogs:o,correlationId:s}){let l=new Date,u=(0,c.randomUUID)().replaceAll(`-`,``),d=o?await Si(l):null,f=bi({appVersion:t,buildFlavor:n,buildNumber:r,classification:i,description:a?.trim()??null,eventId:u,feedbackLogArchive:d,now:l,correlationId:s}),p=Ni(e.gn),m=new Blob([Uint8Array.from(f)],{type:gi}),h=await fetch(p,{method:`POST`,headers:{"Content-Type":gi},body:m});if(h.ok)return;let g=await h.text();throw Error(`Feedback upload failed with status ${h.status} ${h.statusText}${g?`: ${g}`:``}`)}function bi({appVersion:t,buildFlavor:r,buildNumber:i,classification:a,description:o,eventId:s,feedbackLogArchive:c,now:l,correlationId:u}){let d=e.qn(t),f=r!==`prod`,p=l.toISOString(),m=xi(o),h=n.default.cpus(),g=h[0]?.model,_=typeof n.default.version==`function`?n.default.version():void 0,v=process.versions.electron==null?`Node.js`:`Electron`,y=process.versions.electron??process.versions.node??`unknown`,b={event_id:s,timestamp:p,platform:`javascript`,level:`info`,logger:mi,message:`Codex app feedback report ${u}`,...m==null?{}:{culprit:m},environment:r,release:e._n(d.version),server_name:n.default.hostname(),fingerprint:[`desktop-feedback`,s],contexts:{app:{app_name:`Codex`,app_version:d.version,...i==null?{}:{app_build:i}},os:{name:n.default.type(),version:n.default.release(),..._==null?{}:{kernel_version:_}},runtime:{name:v,version:y},device:{arch:process.arch,processor_count:h.length,memory_size:n.default.totalmem(),free_memory:n.default.freemem(),...g!=null&&g.length>0?{model:g}:{}},feedback:{classification:a,description:o,log_archive_filename:c?.fileName??null,log_archive_size_bytes:c?.contents.byteLength??null,log_archive_file_count:c?.sourceFileCount??0}},tags:{feedback_report:`desktop`,classification:a,include_logs:c==null?`false`:`true`,platform:process.platform,sessionId:e.o,preRelease:f},...i==null?{}:{dist:i},extra:{classification:a,description:o,include_logs:c!=null}},x=[Mi({event_id:s,sent_at:p,dsn:e.gn}),Mi({type:`event`}),Mi(b)];return c!=null&&x.push(Mi({type:`attachment`,length:c.contents.byteLength,filename:c.fileName,content_type:pi,attachment_type:`event.attachment`}),c.contents,hi),Buffer.concat(x)}function xi(e){return e==null||e.length===0?null:e.replace(/\s+/g,` `).slice(0,500)}async function Si(e){let t=ve(),n=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())),i=ji(t,n),a=await Ci(i),o=await Promise.all(a.map(async t=>{let n=await(0,u.readFile)((0,r.join)(i,t));return{name:(0,r.basename)(t),contents:n,mtimeSeconds:Math.floor(e.getTime()/1e3),mode:420}})),s=await fi(wi(o));return{fileName:`codex-logs-${Ai(n)}.tar.gz`,contents:s,sourceFileCount:o.length}}async function Ci(e){try{return(await(0,u.readdir)(e,{withFileTypes:!0})).filter(e=>e.isFile()).map(e=>e.name).sort()}catch(e){if(e.code===`ENOENT`)return[];throw e}}function wi(e){let t=[];for(let n of e){let e=Ti(n.name),r=Buffer.alloc(_i,0);Ei(r,0,100,e),Di(r,100,8,n.mode),Di(r,108,8,0),Di(r,116,8,0),Di(r,124,12,n.contents.byteLength),Di(r,136,12,n.mtimeSeconds),r.fill(32,148,156),Ei(r,156,1,`0`),Ei(r,257,6,`ustar`),Ei(r,263,2,`00`),Oi(r,148,r.reduce((e,t)=>e+t,0)),t.push(r,n.contents);let i=(_i-n.contents.byteLength%_i)%_i;i>0&&t.push(Buffer.alloc(i,0))}return t.push(Buffer.alloc(_i*vi,0)),Buffer.concat(t)}function Ti(e){let t=e.replace(/\\/g,`/`).replace(/^\/+/,``);return t.length<=100?t:t.slice(-100)}function Ei(e,t,n,r){let i=Buffer.from(r,`utf8`);i.copy(e,t,0,Math.min(n,i.length))}function Di(e,t,n,r){let i=(Number.isFinite(r)?Math.max(0,Math.floor(r)):0).toString(8),a=n-1;Ei(e,t,n,`${i.slice(-a).padStart(a,`0`)}\0`)}function Oi(e,t,n){Ei(e,t,8,`${n.toString(8).slice(-6).padStart(6,`0`)}\0 `)}function ki(e){return e.toString().padStart(2,`0`)}function Ai(e){return`${e.getUTCFullYear()}-${ki(e.getUTCMonth()+1)}-${ki(e.getUTCDate())}`}function ji(e,t){return(0,r.join)(e,t.getUTCFullYear().toString(),ki(t.getUTCMonth()+1),ki(t.getUTCDate()))}function Mi(e){return Buffer.from(`${JSON.stringify(e)}\n`,`utf8`)}function Ni(e){let t=new URL(e),n=t.pathname.split(`/`).filter(Boolean).at(-1);if(n==null||n.length===0)throw Error(`Sentry DSN is missing a project id`);return`${t.protocol}//${t.host}/api/${n}/envelope/`}async function Pi({query:t,cwd:n,limit:i}){let a=t.trim();if(a.length===0||!n)return[];let o=r.default.resolve(n),s=Ii();return await e.tt(a,[o],i,{rgPath:s??void 0,runCommand:s==null?Fi:void 0})??[]}function Fi(){return Promise.resolve({stdout:``,code:2})}function Ii(){return e.zt(process.resourcesPath)}function Li({target:e,hasExplicitTarget:t,platform:n}){return e||(!t&&n===`win32`?`fileManager`:null)}var Ri=`/Applications`,zi=(0,r.join)((0,n.homedir)(),`Applications`),Bi=[Ri,zi];function Vi(e){return e.startsWith(`${Ri}/`)?[e,(0,r.join)(zi,e.slice(`${Ri}/`.length))]:[e]}function z(e){if(process.platform!==`darwin`)return null;for(let t of e)for(let e of Vi(t))if((0,a.existsSync)(e))return e;return null}function Hi(e,t){if(process.platform!==`darwin`)return null;let n=e.toLowerCase();for(let e of Bi){let i;try{i=(0,a.readdirSync)(e)}catch{continue}for(let o of i){let i=o.toLowerCase();if(!i.startsWith(n)||!i.endsWith(`.app`))continue;let s=(0,r.join)(e,o,`Contents`,`MacOS`,t);if((0,a.existsSync)(s))return s}}return null}function Ui(e){if(process.platform!==`darwin`)return null;let t=e.toLowerCase();for(let e of Bi){let n;try{n=(0,a.readdirSync)(e)}catch{continue}for(let i of n){let n=i.toLowerCase();if(!n.startsWith(t)||!n.endsWith(`.app`))continue;let o=(0,r.join)(e,i);if((0,a.existsSync)(o))return o}}return null}function Wi(t,n,r,i,a){return a!=null&&r!=null&&e.In(r)?Ki({remoteAuthority:qi(r),remoteWorkspaceRoot:i?.trim()||void 0,path:a}):Gi(t,n)}function Gi(e,t){return t?[`--goto`,`${e}:${t.line}:${t.column}`]:process.platform===`win32`?[e]:[`--goto`,e]}function Ki({remoteAuthority:e,remoteWorkspaceRoot:t,path:n}){let r=[];return t!=null&&r.push(`--folder-uri`,Zi(e,t)),r.push(`--file-uri`,Zi(e,n)),r}function qi(e){return`ssh-remote+${Ji(e)}`}function Ji(e){if(e.kind===`ssh`){let t=Yi(e);if(t)return t}return e.name?.trim()||Xi(e.id)}function Yi(e){let t=e[rr],n=typeof t?.sshAlias==`string`?t.sshAlias.trim():``;if(n.length>0)return n;let r=Array.isArray(e.terminal_command)?e.terminal_command.at(-1):null,i=typeof r==`string`?r.trim():``;return i.length>0?i:null}function Xi(e){let t=e.trim();return t.split(/[:/]/).at(-1)?.trim()||t}function Zi(e,t){let n=t.startsWith(`/`)?t:`/${t}`;return`vscode-remote://${e}${encodeURI(n)}`}function Qi(e,t){return t?[`${e}:${t.line}:${t.column}`]:[e]}function $i({label:e,icon:t,kind:n,platform:r}){return{label:r.label??e,icon:r.icon??t,kind:r.kind??n,detect:r.detect,iconPath:r.iconPath,args:r.args,env:r.env,open:r.open}}function ea({id:e,label:t,icon:n,kind:r,darwin:i,win32:a,linux:o}){return{id:e,platforms:{darwin:i?$i({label:t,icon:n,kind:r,platform:i}):void 0,win32:a?$i({label:t,icon:n,kind:r,platform:a}):void 0,linux:o?$i({label:t,icon:n,kind:r,platform:o}):void 0}}}function ta({id:e,label:t,icon:n,darwinDetect:r,win32Detect:i,darwinEnv:a,darwinArgs:o}){return{id:e,platforms:{darwin:r?{label:t,icon:n,kind:`editor`,detect:r,env:a,args:o??Wi}:void 0,win32:i?{label:t,icon:n,kind:`editor`,detect:i,args:Wi}:void 0}}}const na=ta({id:`antigravity`,label:`Antigravity`,icon:`apps/antigravity.png`,darwinDetect:()=>z([`/Applications/Antigravity.app/Contents/Resources/app/bin/antigravity`])}),ra={id:`bbedit`,platforms:{darwin:{label:`BBEdit`,icon:`apps/bbedit.png`,kind:`editor`,detect:()=>Ui(`BBEdit`)||z([`/Applications/BBEdit.app`])?`open`:null,args:e=>[`-a`,`BBEdit`,e]}}};var ia=e.er((e=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.sync=e.isexe=void 0;var t=require(`fs`),n=require(`fs/promises`);e.isexe=async(e,t={})=>{let{ignoreErrors:i=!1}=t;try{return r(await(0,n.stat)(e),t)}catch(e){let t=e;if(i||t.code===`EACCES`)return!1;throw t}},e.sync=(e,n={})=>{let{ignoreErrors:i=!1}=n;try{return r((0,t.statSync)(e),n)}catch(e){let t=e;if(i||t.code===`EACCES`)return!1;throw t}};var r=(e,t)=>e.isFile()&&i(e,t),i=(e,t)=>{let n=t.uid??process.getuid?.(),r=t.groups??process.getgroups?.()??[],i=t.gid??process.getgid?.()??r[0];if(n===void 0||i===void 0)throw Error(`cannot get uid or gid`);let a=new Set([i,...r]),o=e.mode,s=e.uid,c=e.gid;return!!(o&1||o&8&&a.has(c)||o&64&&s===n||o&72&&n===0)}})),aa=e.er((e=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.sync=e.isexe=void 0;var t=require(`fs`),n=require(`fs/promises`);e.isexe=async(e,t={})=>{let{ignoreErrors:r=!1}=t;try{return i(await(0,n.stat)(e),e,t)}catch(e){let t=e;if(r||t.code===`EACCES`)return!1;throw t}},e.sync=(e,n={})=>{let{ignoreErrors:r=!1}=n;try{return i((0,t.statSync)(e),e,n)}catch(e){let t=e;if(r||t.code===`EACCES`)return!1;throw t}};var r=(e,t)=>{let{pathExt:n=process.env.PATHEXT||``}=t,r=n.split(`;`);if(r.indexOf(``)!==-1)return!0;for(let t=0;te.isFile()&&r(t,n)})),oa=e.er((e=>{Object.defineProperty(e,`__esModule`,{value:!0})})),sa=e.er((e=>{var t=e&&e.__createBinding||(Object.create?(function(e,t,n,r){r===void 0&&(r=n);var i=Object.getOwnPropertyDescriptor(t,n);(!i||(`get`in i?!t.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,r,i)}):(function(e,t,n,r){r===void 0&&(r=n),e[r]=t[n]})),n=e&&e.__setModuleDefault||(Object.create?(function(e,t){Object.defineProperty(e,`default`,{enumerable:!0,value:t})}):function(e,t){e.default=t}),r=e&&e.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(e!=null)for(var i in e)i!==`default`&&Object.prototype.hasOwnProperty.call(e,i)&&t(r,e,i);return n(r,e),r},i=e&&e.__exportStar||function(e,n){for(var r in e)r!==`default`&&!Object.prototype.hasOwnProperty.call(n,r)&&t(n,e,r)};Object.defineProperty(e,`__esModule`,{value:!0}),e.sync=e.isexe=e.posix=e.win32=void 0;var a=r(ia());e.posix=a;var o=r(aa());e.win32=o,i(oa(),e);var s=(process.env._ISEXE_TEST_PLATFORM_||process.platform)===`win32`?o:a;e.isexe=s.isexe,e.sync=s.sync})),ca=e.er(((e,t)=>{var{isexe:n,sync:r}=sa(),{join:i,delimiter:a,sep:o,posix:s}=require(`path`),c=process.platform===`win32`,l=new RegExp(`[${s.sep}${o===s.sep?``:o}]`.replace(/(\\)/g,`\\$1`)),u=RegExp(`^\\.${l.source}`),d=e=>Object.assign(Error(`not found: ${e}`),{code:`ENOENT`}),f=(e,{path:t=process.env.PATH,pathExt:n=process.env.PATHEXT,delimiter:r=a})=>{let i=e.match(l)?[``]:[...c?[process.cwd()]:[],...(t||``).split(r)];if(c){let t=n||[`.EXE`,`.CMD`,`.BAT`,`.COM`].join(r),a=t.split(r).flatMap(e=>[e,e.toLowerCase()]);return e.includes(`.`)&&a[0]!==``&&a.unshift(``),{pathEnv:i,pathExt:a,pathExtExe:t}}return{pathEnv:i,pathExt:[``]}},p=(e,t)=>{let n=/^".*"$/.test(e)?e.slice(1,-1):e;return(!n&&u.test(t)?t.slice(0,2):``)+i(n,t)},m=async(e,t={})=>{let{pathEnv:r,pathExt:i,pathExtExe:a}=f(e,t),o=[];for(let s of r){let r=p(s,e);for(let e of i){let i=r+e;if(await n(i,{pathExt:a,ignoreErrors:!0})){if(!t.all)return i;o.push(i)}}}if(t.all&&o.length)return o;if(t.nothrow)return null;throw d(e)};t.exports=m,m.sync=(e,t={})=>{let{pathEnv:n,pathExt:i,pathExtExe:a}=f(e,t),o=[];for(let s of n){let n=p(s,e);for(let e of i){let i=n+e;if(r(i,{pathExt:a,ignoreErrors:!0})){if(!t.all)return i;o.push(i)}}}if(t.all&&o.length)return o;if(t.nothrow)return null;throw d(e)}})),la=e.ir(ca()),ua=process.platform===`darwin`,da=process.platform===`win32`;function B(e){let t=la.default.sync(e,{nothrow:!0});return typeof t==`string`&&(0,a.existsSync)(t)?t:da?ha(e):null}function fa(e){let t=e;for(;;){if((0,a.existsSync)(t))return t;let e=(0,r.dirname)(t);if(e===t)return null;t=e}}function pa(e){if(ua)return[`-R`,e];if(da){let t=fa(e);return t?(0,a.statSync)(t).isFile()?[`/select,`,t]:[t]:[e]}return[e]}function ma(e,t,n,r){return e===`win32`&&/\.(cmd|bat)$/i.test(t)?{command:r??`cmd.exe`,args:[`/d`,`/s`,`/c`,t,...n]}:{command:t,args:n}}function V(e,t,n){return new Promise((r,i)=>{let a=ma(process.platform,e,t,process.env.ComSpec),s=(0,o.spawn)(a.command,a.args,{stdio:`ignore`,env:n?.env});s.on(`error`,i),s.on(`close`,n=>{if(!n){r();return}i(Error(`${e} exited with code ${n??`unknown`} (${t.join(` `)})`))})})}function ha(e){let t=(0,o.spawnSync)(`where.exe`,[e],{windowsHide:!0,encoding:`utf8`});return t.status!==0||typeof t.stdout!=`string`?null:t.stdout.split(/\r?\n/).map(e=>e.trim()).find(e=>e.length>0)??null}var ga=process.env.LOCALAPPDATA??(0,r.join)((0,n.homedir)(),`AppData`,`Local`),_a=[(0,r.join)(ga,`Programs`),process.env.ProgramFiles,process.env[`ProgramFiles(x86)`]].flatMap(e=>e?[e]:[]),va=(0,r.join)(ga,`Microsoft`,`WindowsApps`),ya=[process.env.APPDATA?(0,r.join)(process.env.APPDATA,`Microsoft`,`Windows`,`Start Menu`,`Programs`):null,process.env.ProgramData?(0,r.join)(process.env.ProgramData,`Microsoft`,`Windows`,`Start Menu`,`Programs`):null].flatMap(e=>e?[e]:[]);function H(e){for(let t of _a)for(let n of e){let e=(0,r.join)(t,...n);if((0,a.existsSync)(e))return e}return null}function U(e){if(!(0,a.existsSync)(e))return null;if((0,r.extname)(e))return e;for(let t of[`.cmd`,`.bat`,`.exe`]){let n=`${e}${t}`;if((0,a.existsSync)(n))return n}return e}function ba(e,t){let n=(0,r.dirname)(e);if((0,r.basename)(n).toLowerCase()===`bin`){let e=(0,r.join)((0,r.dirname)(n),t);if((0,a.existsSync)(e))return e}let i=(0,r.join)(n,t);return(0,a.existsSync)(i)?i:null}function xa(e){return U((0,r.join)(va,e))}function Sa(){return va}function Ca({pathCommand:e,executableName:t,installDirName:n}){if(e){let n=U(e);if(n)return ba(n,t)??((0,r.basename)(n).toLowerCase()===t.toLowerCase()?n:null)}return H([[n,t]])}function wa(e){let t=new Set(e.map(e=>e.toLowerCase()));for(let e of ya){let n=Ta(e,t);if(n)return n}return null}function Ta(e,t){if(!(0,a.existsSync)(e))return null;for(let n of(0,a.readdirSync)(e,{withFileTypes:!0})){let i=(0,r.join)(e,n.name);if(n.isFile()&&t.has(n.name.toLowerCase()))return i;if(!n.isDirectory())continue;let a=Ta(i,t);if(a)return a}return null}var Ea=e.sn(`open-in-targets`);const Da={id:`terminal`,platforms:{...Oa({id:`terminal`,label:`Terminal`,icon:`apps/terminal.png`,appPaths:[`/System/Applications/Utilities/Terminal.app`],appName:`Terminal`}).platforms,win32:{label:`Terminal`,icon:`apps/microsoft-terminal.png`,kind:`terminal`,detect:Fa,iconPath:()=>null,args:Ia,open:({command:e,path:t})=>La(e,Ia(t))}}};function Oa({id:e,label:t,icon:n,appPaths:r,appName:i}){return{id:e,platforms:{darwin:{label:t,icon:n,kind:`terminal`,detect:()=>z(r)?`open`:null,args:e=>[`-a`,i,ja(e)],open:async({command:t,path:n})=>{await Wa(e,n)||await V(t,[`-a`,i,ja(n)])}}}}}function ka(e){let t=process.env.VISUAL?.trim();if(t)return t;let n=process.env.EDITOR?.trim();if(n)return n;for(let t of Ma){let n=e(t);if(n)return n}return null}function Aa(e){return[`-na`,`Ghostty.app`,`--args`,`-e`,process.env.SHELL?.trim()||`/bin/zsh`,`-lc`,e]}function ja(e){return(0,a.existsSync)(e)&&(0,a.statSync)(e).isDirectory()?e:(0,r.dirname)(e)}var Ma=[`nvim`,`vim`,`nano`,`less`],Na=`wt.exe`,Pa=[`Terminal.lnk`,`Windows Terminal.lnk`];function Fa(){if(B(Na)||xa(Na))return Na;let e=wa(Pa);return e?U(t.shell.readShortcutLink(e).target)??Na:null}function Ia(e){return[`-d`,ja(e)]}async function La(e,t){await V(`cmd.exe`,[`/d`,`/s`,`/c`,`start`,``,e,...t],{env:Ra()})}function Ra(){let e=Sa(),t=process.env.Path==null?`PATH`:`Path`,n=(process.env[t]??``).split(`;`).filter(Boolean);return n.some(t=>t.toLowerCase()===e.toLowerCase())||n.unshift(e),{...process.env,[t]:n.join(`;`)}}function za(){return ka(B)}function Ba(e){return`'${e.replace(/'/g,`'\\''`)}'`}function Va(e){return e.replace(/\\/g,`\\\\`).replace(/"/g,`\\"`)}function Ha(e,t){return`cd ${Ba((0,r.dirname)(t))} && ${e} ${Ba(t)}`}function Ua(e,t){let n=Va(t);return e===`terminal`?`tell application "Terminal" to do script "${n}"`:e===`iterm2`?[`tell application "iTerm"`,`create window with default profile`,`tell current session of current window to write text "`+n+`"`,`end tell`].join(` -`):null}async function Wa(e,t){let n=za();if(!n||!(0,a.existsSync)(t)||(0,a.statSync)(t).isDirectory())return!1;let r=Ha(n,t);if(e===`ghostty`)try{return await V(`open`,Aa(r)),!0}catch(e){return Ea().warning(`Failed to run $EDITOR command for Ghostty; falling back to opening directory`,{safe:{},sensitive:{error:e,path:t}}),!1}let i=Ua(e,r);if(!i)return!1;try{return await V(`osascript`,[`-e`,i]),!0}catch(n){return Ea().warning(`Failed to run $EDITOR command for terminal target; falling back to opening directory`,{safe:{targetId:e},sensitive:{error:n,path:t}}),!1}}const Ga=ea({id:`cmder`,label:`Cmder`,icon:`apps/cmder.png`,kind:`terminal`,win32:{detect:Ka,args:e=>[`/START`,ja(e)]}});function Ka(){let e=process.env.CMDER_ROOT?.trim();if(e){let t=(0,r.join)(e,`Cmder.exe`);if((0,a.existsSync)(t))return t}let n=B(`cmder.exe`)??B(`cmder`);if(n)return U(n);let i=wa([`Cmder.lnk`]);return i?U(t.shell.readShortcutLink(i).target):null}const qa=ta({id:`cursor`,label:`Cursor`,icon:`apps/cursor.png`,darwinDetect:()=>Ja()?.electronBin??null,win32Detect:Ya,darwinEnv:()=>{let e={...process.env};return e.VSCODE_NODE_OPTIONS=e.NODE_OPTIONS,e.VSCODE_NODE_REPL_EXTERNAL_MODULE=e.NODE_REPL_EXTERNAL_MODULE,delete e.NODE_OPTIONS,delete e.NODE_REPL_EXTERNAL_MODULE,e.ELECTRON_RUN_AS_NODE=`1`,e},darwinArgs:(e,t)=>{let n=Ja();if(!n)throw Error(`Cursor CLI entrypoint not available`);return[n.cliJs,...Wi(e,t)]}});function Ja(){if(process.platform!==`darwin`)return null;let e=[`/Applications/Cursor.app`,`/Applications/Cursor Preview.app`,`/Applications/Cursor Nightly.app`],t=Ui(`Cursor`);t&&e.push(t);for(let t of e)for(let e of Vi(t)){let t=(0,r.join)(e,`Contents`,`MacOS`,`Cursor`),n=(0,r.join)(e,`Contents`,`Resources`,`app`,`out`,`cli.js`);if(!(!(0,a.existsSync)(t)||!(0,a.existsSync)(n)))return{electronBin:t,cliJs:n}}return null}function Ya(){let e=B(`cursor`);if(e){let t=U(e);if(t){if((0,r.basename)(t).toLowerCase()===`cursor.exe`)return t;let e=(0,r.dirname)(t);if((0,r.basename)(e).toLowerCase()===`bin`){let t=(0,r.dirname)(e);if((0,r.basename)(t).toLowerCase()===`app`){let e=(0,r.dirname)(t);if((0,r.basename)(e).toLowerCase()===`resources`){let t=(0,r.join)((0,r.dirname)(e),`Cursor.exe`);if((0,a.existsSync)(t))return t}}}}}return H([[`Cursor`,`Cursor.exe`]])}const Xa=ea({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>pa(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:Za,args:e=>pa(e),open:async({path:e})=>Qa(e)}});function Za(){let e=process.env.SystemRoot??process.env.windir;if(e){let t=(0,r.join)(e,`explorer.exe`);if((0,a.existsSync)(t))return t}return`explorer.exe`}async function Qa(e){let n=$a(e);if(n&&(0,a.statSync)(n).isFile()){t.shell.showItemInFolder(n);return}let r=n??e,i=await t.shell.openPath(r);if(i)throw Error(i)}function $a(e){let t=e;for(;;){if((0,a.existsSync)(t))return t;let e=(0,r.dirname)(t);if(e===t)return null;t=e}}const eo=Oa({id:`ghostty`,label:`Ghostty`,icon:`apps/ghostty.png`,appPaths:[`/Applications/Ghostty.app`],appName:`Ghostty`}),to=ea({id:`gitBash`,label:`Git Bash`,icon:`apps/vscode.png`,kind:`terminal`,win32:{detect:no,iconPath:ro,args:e=>[`--cd=${ja(e)}`]}});function no(){let e=B(`git-bash.exe`)??B(`git-bash`);if(e){let t=U(e);if(t)return ba(t,`git-bash.exe`)??t}return H([[`Git`,`git-bash.exe`],[`Git`,`bin`,`bash.exe`]])}function ro(e){return wa([`Git Bash.lnk`])||H([[`Git`,`git-bash.exe`]])||(e?ba(e,`git-bash.exe`)??e:null)}const io=ea({id:`githubDesktop`,label:`GitHub Desktop`,icon:`apps/vscode.png`,kind:`editor`,win32:{detect:ao,args:e=>[ja(e)]}});function ao(){let e=B(`github.exe`)??B(`github`);if(e){let t=U(e);if(t)return ba(t,`GitHubDesktop.exe`)??t}let t=process.env.LOCALAPPDATA??`${(0,n.homedir)()}\\AppData\\Local`;for(let e of[[`GitHubDesktop`,`GitHubDesktop.exe`],[`GitHub Desktop`,`GitHubDesktop.exe`]]){let n=(0,r.join)(t,...e);if((0,a.existsSync)(n))return n}return H([[`GitHub Desktop`,`GitHubDesktop.exe`],[`GitHubDesktop`,`GitHubDesktop.exe`]])}const oo=Oa({id:`iterm2`,label:`iTerm2`,icon:`apps/iterm2.png`,appPaths:[`/Applications/iTerm.app`,`/Applications/iTerm2.app`],appName:`iTerm`});var so=e.sn(`open-in-targets`),co=(0,r.join)((0,n.homedir)(),`Library`,`Application Support`,`JetBrains`,`Toolbox`,`apps`),lo=[process.env.ProgramFiles,process.env[`ProgramFiles(x86)`]].flatMap(e=>e?[(0,r.join)(e,`JetBrains`)]:[]),uo=[{target:`androidStudio`,bundlePrefixes:[`android studio`],executable:`studio`},{target:`intellij`,bundlePrefixes:[`intellij idea`],executable:`idea`},{target:`rider`,bundlePrefixes:[`rider`],executable:`rider`},{target:`goland`,bundlePrefixes:[`goland`],executable:`goland`},{target:`rustrover`,bundlePrefixes:[`rustrover`],executable:`rustrover`},{target:`pycharm`,bundlePrefixes:[`pycharm`],executable:`pycharm`},{target:`webstorm`,bundlePrefixes:[`webstorm`],executable:`webstorm`},{target:`phpstorm`,bundlePrefixes:[`phpstorm`],executable:`phpstorm`}],fo=null;const po=So({id:`androidStudio`,label:`Android Studio`,icon:`apps/android-studio.png`,toolboxTarget:`androidStudio`,macExecutable:`studio`,windowsPathCommands:[`studio64.exe`,`studio.exe`,`studio`],windowsInstallDirPrefixes:[`android studio`],windowsInstallExecutables:[`studio64.exe`,`studio.exe`],windowsFallbackPaths:[[`Android`,`Android Studio`,`bin`,`studio64.exe`],[`Android`,`Android Studio`,`bin`,`studio.exe`]]}),mo=So({id:`intellij`,label:`IntelliJ IDEA`,icon:`apps/intellij.png`,toolboxTarget:`intellij`,macExecutable:`idea`,windowsPathCommands:[`idea64.exe`,`idea.exe`,`idea`],windowsInstallDirPrefixes:[`intellij idea`,`idea`],windowsInstallExecutables:[`idea64.exe`,`idea.exe`]}),ho=So({id:`rider`,label:`Rider`,icon:`apps/rider.png`,toolboxTarget:`rider`,macExecutable:`rider`,windowsPathCommands:[`rider64.exe`,`rider.exe`,`rider`],windowsInstallDirPrefixes:[`rider`],windowsInstallExecutables:[`rider64.exe`,`rider.exe`]}),go=So({id:`goland`,label:`GoLand`,icon:`apps/goland.png`,toolboxTarget:`goland`,macExecutable:`goland`}),_o=So({id:`rustrover`,label:`RustRover`,icon:`apps/rustrover.png`,toolboxTarget:`rustrover`,macExecutable:`rustrover`}),vo=So({id:`pycharm`,label:`PyCharm`,icon:`apps/pycharm.png`,toolboxTarget:`pycharm`,macExecutable:`pycharm`,windowsPathCommands:[`pycharm64.exe`,`pycharm.exe`,`pycharm`],windowsInstallDirPrefixes:[`pycharm`],windowsInstallExecutables:[`pycharm64.exe`,`pycharm.exe`]}),yo=So({id:`webstorm`,label:`WebStorm`,icon:`apps/webstorm.svg`,toolboxTarget:`webstorm`,macExecutable:`webstorm`,windowsPathCommands:[`webstorm64.exe`,`webstorm.exe`,`webstorm`],windowsInstallDirPrefixes:[`webstorm`],windowsInstallExecutables:[`webstorm64.exe`,`webstorm.exe`]}),bo=So({id:`phpstorm`,label:`PhpStorm`,icon:`apps/phpstorm.png`,toolboxTarget:`phpstorm`,macExecutable:`phpstorm`,windowsPathCommands:[`phpstorm64.exe`,`phpstorm.exe`,`phpstorm`],windowsInstallDirPrefixes:[`phpstorm`],windowsInstallExecutables:[`phpstorm64.exe`,`phpstorm.exe`]});function xo(e){let t=new Map;if(process.platform!==`darwin`)return t;let n;try{n=(0,a.readdirSync)(e)}catch{return t}for(let i of Co(n)){let n=(0,r.join)(e,i),o;try{o=(0,a.readdirSync)(n)}catch{continue}for(let e of Co(o)){let i=(0,r.join)(n,e),o;try{o=(0,a.readdirSync)(i)}catch{continue}for(let e of Co(o)){let n=(0,r.join)(i,e),o;try{o=(0,a.readdirSync)(n)}catch{continue}for(let e of Co(o)){let i=e.toLowerCase();if(i.endsWith(`.app`))for(let o of uo){if(t.has(o.target)||!o.bundlePrefixes.some(e=>i.startsWith(e)))continue;let s=(0,r.join)(n,e,`Contents`,`MacOS`,o.executable);(0,a.existsSync)(s)&&t.set(o.target,s)}}}}}return t}function So({id:e,label:t,icon:n,toolboxTarget:r,macExecutable:i,windowsPathCommands:a,windowsInstallDirPrefixes:o,windowsInstallExecutables:s,windowsFallbackPaths:c}){return{id:e,platforms:{darwin:{label:t,icon:n,kind:`editor`,detect:()=>Eo(r,[`/Applications/${t}.app/Contents/MacOS/${i}`],t,i),args:ko},win32:a&&o&&s?{label:t,icon:n,kind:`editor`,detect:()=>Do({pathCommands:a,installDirPrefixes:o,installExecutables:s,fallbackPaths:c}),args:ko}:void 0}}}function Co(e){return[...e].sort((e,t)=>t.localeCompare(e))}function wo(){if(fo)return fo;try{fo=xo(co)}catch(e){so().debug(`Failed to discover JetBrains Toolbox installations`,{safe:{},sensitive:{error:e}}),fo=new Map}return fo}function To(e,t,n){return z(e)??Hi(t,n)}function Eo(e,t,n,r){return To(t,n,r)??wo().get(e)??null}function Do({pathCommands:e,installDirPrefixes:t,installExecutables:n,fallbackPaths:r}){for(let t of e){let e=B(t);if(!e)continue;let n=U(e);if(n)return n}return Oo({installDirPrefixes:t,installExecutables:n})||(r?H(r):null)}function Oo({installDirPrefixes:e,installExecutables:t}){let n=e.map(e=>e.toLowerCase());for(let e of lo){let i;try{i=Co((0,a.readdirSync)(e))}catch{continue}for(let o of i){let i=o.toLowerCase();if(n.some(e=>i.startsWith(e)))for(let n of t){let t=(0,r.join)(e,o,`bin`,n);if((0,a.existsSync)(t))return t}}}return null}function ko(e,t){return t?[`--line`,t.line.toString(),`--column`,t.column.toString(),e]:[e]}const Ao=ea({id:`sublimeText`,label:`Sublime Text`,icon:`apps/sublime-text.png`,kind:`editor`,darwin:{detect:jo,args:Qi},win32:{detect:Mo,args:Qi}});function jo(){let e=B(`subl`);if(e)return e;let t=Ui(`Sublime Text`);if(t){let e=(0,r.join)(t,`Contents`,`SharedSupport`,`bin`,`subl`);if((0,a.existsSync)(e))return e}return z([`/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl`])}function Mo(){let e=B(`subl.exe`)??B(`subl`);if(e){let t=U(e);if(t)return ba(t,`sublime_text.exe`)??t}return H([[`Sublime Text`,`sublime_text.exe`],[`Sublime Text`,`subl.exe`]])}const No={id:`textmate`,platforms:{darwin:{label:`TextMate`,icon:`apps/textmate.png`,kind:`editor`,detect:()=>Ui(`TextMate`)||z([`/Applications/TextMate.app`])?`open`:null,args:Po}}};function Po(e,t){if(!t)return[`-a`,`TextMate`,e];let n=new URL(`txmt://open/`);return n.searchParams.set(`url`,(0,d.pathToFileURL)(e).toString()),n.searchParams.set(`line`,t.line.toString()),n.searchParams.set(`column`,t.column.toString()),[`-a`,`TextMate`,n.toString()]}var Fo=[`2022`,`2019`,`2017`],Io=[`Community`,`Professional`,`Enterprise`,`Preview`,`BuildTools`];const Lo=ea({id:`visualStudio`,label:`Visual Studio`,icon:`apps/vscode.png`,kind:`editor`,win32:{detect:Ro,args:e=>[e]}});function Ro(){let e=B(`devenv.exe`)??B(`devenv`);return e?U(e):H(Fo.flatMap(e=>Io.map(t=>[`Microsoft Visual Studio`,e,t,`Common7`,`IDE`,`devenv.exe`])))}const zo=ta({id:`vscode`,label:`VS Code`,icon:`apps/vscode.png`,darwinDetect:()=>z([`/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`,`/Applications/Code.app/Contents/Resources/app/bin/code`]),win32Detect:Bo});function Bo(){return Ca({pathCommand:B(`code`),executableName:`Code.exe`,installDirName:`Microsoft VS Code`})}const Vo=ta({id:`vscodeInsiders`,label:`VS Code Insiders`,icon:`apps/vscode-insiders.png`,darwinDetect:()=>z([`/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code`,`/Applications/Code - Insiders.app/Contents/Resources/app/bin/code`]),win32Detect:Ho});function Ho(){return Ca({pathCommand:B(`code-insiders`),executableName:`Code - Insiders.exe`,installDirName:`Microsoft VS Code Insiders`})}const Uo=Oa({id:`warp`,label:`Warp`,icon:`apps/warp.png`,appPaths:[`/Applications/Warp.app`],appName:`Warp`}),Wo=ta({id:`windsurf`,label:`Windsurf`,icon:`apps/windsurf.png`,darwinDetect:()=>z([`/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf`])});var Go=`wsl.exe`;const Ko=ea({id:`wsl`,label:`WSL`,icon:`apps/terminal.png`,kind:`terminal`,win32:{detect:Yo,iconPath:Jo,args:qo,open:({command:e,path:t})=>La(e,qo(t))}});function qo(t){let n=e.Zt(ja(t));return n.startsWith(`\\\\`)?[...Ia(t),Go]:[...Ia(t),Go,`--cd`,n]}function Jo(){return wa([`WSL.lnk`])??xa(Go)}function Yo(){return e.$t()==null?null:Fa()}const Xo={id:`xcode`,platforms:{darwin:{label:`Xcode`,icon:`apps/xcode.png`,kind:`editor`,detect:()=>{let e=Qo();return e?.xedPath??e?.appPath??null},args:e=>[e],open:async({path:e,location:t})=>{await es(e,t)}}}};var Zo=e.sn(`open-in-targets`);function Qo(){if(process.platform!==`darwin`)return null;let e=Ui(`Xcode`)??z([`/Applications/Xcode.app`]),t=null;try{let e=(0,o.spawnSync)(`xcode-select`,[`-p`],{encoding:`utf8`,timeout:1e3}),n=e.stdout?.trim();if(e.status===0&&n){let e=(0,r.join)(n,`usr`,`bin`,`xed`);(0,a.existsSync)(e)&&(t=e)}}catch(e){Zo().debug(`Failed to resolve Xcode via xcode-select`,{safe:{},sensitive:{error:e}})}if(t==null&&e){let n=(0,r.join)(e,`Contents`,`Developer`,`usr`,`bin`,`xed`);(0,a.existsSync)(n)&&(t=n)}return t==null&&e==null?null:{appPath:e,xedPath:t}}function $o(e){let t=e;(!(0,a.existsSync)(e)||!(0,a.statSync)(e).isDirectory())&&(t=(0,r.dirname)(e));let n=null;for(;;){let e;try{e=(0,a.readdirSync)(t)}catch{e=[]}let i=e.find(e=>e.endsWith(`.xcworkspace`));if(i)return(0,r.join)(t,i);let o=e.find(e=>e.endsWith(`.xcodeproj`));if(o)return(0,r.join)(t,o);n==null&&e.includes(`Package.swift`)&&(n=t);let s=(0,r.dirname)(t);if(s===t)return n;t=s}}async function es(e,t){let n=Qo();if(!n)throw Error(`Open target "xcode" is not available`);if(n.xedPath){let r=$o(e),i=[];r&&i.push(`--project`,r),t&&i.push(`--line`,t.line.toString()),i.push(e),Zo().debug(`Opening path in Xcode via xed`,{safe:{},sensitive:{xedPath:n.xedPath,args:i,xcodeContainerPath:r,path:e}}),await V(n.xedPath,i);return}if(n.appPath==null)throw Error(`Open target "xcode" is not available`);Zo().debug(`Opening path in Xcode via LaunchServices fallback`,{safe:{},sensitive:{appPath:n.appPath,path:e}}),await V(`open`,[`-a`,n.appPath,e])}const ts={id:`zed`,platforms:{darwin:{label:`Zed`,icon:`apps/zed.png`,kind:`editor`,detect:ns,args:Qi,open:async({command:e,path:t,location:n})=>{await os(e,t,n)}},win32:{label:`Zed`,icon:`apps/zed.png`,kind:`editor`,detect:rs,args:Qi}}};function ns(){return B(`zed`)??z([`/Applications/Zed.app/Contents/MacOS/zed`,`/Applications/Zed Preview.app/Contents/MacOS/zed`,`/Applications/Zed Nightly.app/Contents/MacOS/zed`])??Hi(`Zed`,`zed`)}function rs(){let e=B(`zed.exe`)??B(`zed`);return e?U(e):H([[`Zed`,`Zed.exe`]])}function is(){return Ui(`Zed`)??z([`/Applications/Zed.app`,`/Applications/Zed Preview.app`,`/Applications/Zed Nightly.app`])}function as(e){let t=e.indexOf(`.app/Contents/MacOS/`);return t===-1?null:e.slice(0,t+4)}async function os(e,t,n){let r=Qi(t,n),i=as(e)??is();if(i){if(await V(`open`,[`-a`,i,t]),!n)return;let e=B(`zed`);if(e)try{await V(e,r)}catch{}return}await V(e,r)}const ss=[zo,Vo,Lo,qa,ra,Ao,ts,Wo,na,io,Xa,Da,to,Ga,Ko,oo,eo,Uo,Xo,po,mo,ho,go,_o,vo,yo,bo,No];var cs=e.sn(`open-in-targets`);function ls(e){return ss.flatMap(t=>{let n=t.platforms[e];return n?[{id:t.id,...n}]:[]})}const us=ls(process.platform),ds=bs(us);var fs=new Set(us.filter(e=>e.kind===`editor`).map(e=>e.id)),ps=null,ms=null;function hs(e){return fs.has(e)}async function gs(){return ms||(process.platform===`win32`&&!t.app.isReady()?ds:(ms=await ys(process.platform,us,xs()),ms))}async function _s(){return Array.from(xs().keys())}async function vs(e,t,n,r,i,a){let o=us.find(t=>t.id===e);if(!o)throw Error(`Unknown open target "${e}"`);let s=o.detect();if(!s)throw Error(`Open target "${e}" is not available`);if(o.open){await o.open({command:s,path:t,location:n,hostConfig:r,remoteWorkspaceRoot:i,remotePath:a});return}await V(s,o.args(t,n,r,i,a),{env:o.env?.()})}async function ys(e,t,n){return e===`win32`?Promise.all(t.map(async e=>{let t=n?.get(e.id)??null,r=e.iconPath?e.iconPath(t):t;return{id:e.id,label:e.label,icon:await Ss(r,e.icon)}})):bs(t)}function bs(e){return e.map(({id:e,label:t,icon:n})=>({id:e,label:t,icon:n}))}function xs(){if(ps)return ps;let e=new Map;for(let t of us)try{let n=t.detect();n&&e.set(t.id,n)}catch(e){cs().error(`Failed to detect open target`,{safe:{},sensitive:{id:t.id,error:e}})}return ps=e,e}async function Ss(e,n){if(!e)return n;try{let r=e.toLowerCase().endsWith(`.lnk`)?await Cs(e):await t.app.getFileIcon(e,{size:`normal`});return!r||r.isEmpty()?n:r.toDataURL()}catch(e){return cs().warning(`Failed to resolve open target icon`,{safe:{},sensitive:{error:e}}),n}}async function Cs(e){let n=t.shell.readShortcutLink(e),r=ws(n.icon)??n.target??null;if(!r)return null;if(/\.(ico|png|jpe?g|bmp)$/i.test(r)){let e=t.nativeImage.createFromPath(r);if(!e.isEmpty())return e}let i=await t.app.getFileIcon(r,{size:`normal`});return i.isEmpty()?null:i}function ws(e){if(!e)return null;let t=e.trim().replace(/^"(.*)"$/,`$1`),n=t.lastIndexOf(`,`);if(n<0)return t;let r=Number(t.slice(n+1));return Number.isInteger(r)?t.slice(0,n):t}function Ts(e){return hs(e)}function Es(t){let n=t.get(e.Ln.OPEN_IN_TARGET_PREFERENCES)??{},r=Ds(n.global)??void 0,i=Object.fromEntries(Object.entries(n.perPath??{}).flatMap(([e,t])=>{let n=Ds(t);return n?[[e,n]]:[]}));return{global:r,perPath:Object.keys(i).length>0?i:void 0}}function Ds(e){return e===`finder`?`fileManager`:typeof e==`string`&&us.some(t=>t.id===e)?e:null}function Os(e,t,n){let r=Es(e),i=(t&&r.perPath?.[t])??r.global??null;return i&&n.has(i)?i:n.values().next().value??null}function ks(e,t){let n=Es(e);return!!((t&&n.perPath?.[t])??n.global??null)}function As(t,n,r){let i=Es(t);i.global=r,n&&(i.perPath=i.perPath??{},i.perPath[n]=r),t.set(e.Ln.OPEN_IN_TARGET_PREFERENCES,i)}async function js({prompt:t,cwd:n,serviceTier:r,appServerConnection:i}){return e._({prompt:t,cwd:n,serviceTier:r,client:{startThread:e=>i.startThread(e),startTurn:e=>i.startTurn(e),interruptTurn:e=>i.interruptTurn(e),onNotification:e=>i.registerInternalNotificationHandler(e)}})}function Ms(e){e===`light`||e===`dark`?t.nativeTheme.themeSource=e:t.nativeTheme.themeSource=`system`}var Ns=[`powershell`,`commandPrompt`],Ps=[`--login`,`-i`];function Fs(){if(process.platform!==`win32`)return[];let e=[...Ns];return Bs()!=null&&e.push(`gitBash`),Vs()!=null&&e.push(`wsl`),e}function Is(e){if(process.platform!==`win32`)return[process.env.SHELL||`/bin/bash`];if(e===`powershell`){let e=Rs();if(e!=null)return[e]}if(e===`commandPrompt`)return[zs()];if(e===`gitBash`){let e=Bs();if(e!=null)return[e,...Ps]}if(e===`wsl`){let e=Vs();if(e!=null)return[e]}let t=Rs();return t==null?[zs()]:[t]}function Ls(t){let n=t[0];if(n==null||n.length===0)return`Shell`;let r=n.split(/[/\\]/).at(-1)?.toLowerCase();if(process.platform===`win32`){if(r===`wsl`||r===`wsl.exe`)return e.mn.wsl;if(r===`pwsh`||r===`pwsh.exe`||r===`powershell`||r===`powershell.exe`)return e.mn.powershell;if(r===`cmd`||r===`cmd.exe`)return e.mn.commandPrompt;if(r===`git-bash.exe`||r===`bash.exe`&&/[\\/]Git[\\/]/i.test(n))return e.mn.gitBash}let i=r?.replace(/\.exe$/i,``)??n;return i!=null&&i.length>0?i:`Shell`}function Rs(){for(let e of[`pwsh.exe`,`powershell.exe`]){let t=B(e);if(t!=null)return t}return null}function zs(){let e=process.env.COMSPEC?.trim();return e!=null&&e.length>0?e:`cmd.exe`}function Bs(){let e=B(`git-bash.exe`);if(e!=null)return e;let t=B(`git.exe`);if(t!=null){let e=(0,r.dirname)(t),n=[(0,r.join)(e,`bash.exe`),(0,r.join)((0,r.dirname)(e),`bin`,`bash.exe`)];for(let e of n)if((0,a.existsSync)(e))return e}return H([[`Git`,`bin`,`bash.exe`]])}function Vs(){return B(`wsl.exe`)}var Hs=`THIRD_PARTY_NOTICES.txt`,Us=(0,i.promisify)(o.execFile),Ws=()=>{let e=process.cwd(),t=[];return process.resourcesPath&&t.push(r.default.join(process.resourcesPath,Hs)),t.push(r.default.join(e,`assets`,Hs)),t.push(r.default.join(e,`electron`,`assets`,Hs)),t};function Gs(){let e=process.cwd();for(;;){if((0,a.existsSync)(r.default.join(e,`pnpm-workspace.yaml`)))return e;let t=r.default.dirname(e);if(t===e)return null;e=t}}async function Ks(){let e=Gs();if(!e)return;let t=r.default.join(e,`electron`,`assets`,Hs);await a.promises.mkdir(r.default.dirname(t),{recursive:!0}),await Us(`node`,[`./scripts/generate-third-party-notices.mjs`,`--output-file`,t],{cwd:e,timeout:12e4})}async function qs(){for(let e of Ws())if((0,a.existsSync)(e))return a.promises.readFile(e,`utf8`);if(process.env.NODE_ENV!==`production`){try{await Ks()}catch{return null}for(let e of Ws())if((0,a.existsSync)(e))return a.promises.readFile(e,`utf8`)}return null}var Js=null,Ys=null;function Xs(){if(Js!=null)throw Error(`Trace recording start confirmation is already pending`);return new Promise(e=>{Js=e})}function Zs(){if(Js==null)return!1;let e=Js;return Js=null,e(!0),!0}function Qs(){if(Js==null)return!1;let e=Js;return Js=null,e(!1),!0}function $s(){if(Ys!=null)throw Error(`Trace recording upload details are already pending`);return new Promise(e=>{Ys=e})}function ec(e){if(Ys==null)return!1;let t=Ys;return Ys=null,t(e),!0}function tc(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`):[]}function nc(e){if(!e||typeof e!=`object`)return{};let t=Object.entries(e).filter(([,e])=>typeof e==`string`);return Object.fromEntries(t)}function rc(t){return{activeRoots:tc(t.get(e.Ln.ACTIVE_WORKSPACE_ROOTS)),roots:tc(t.get(e.Ln.WORKSPACE_ROOT_OPTIONS)),labels:nc(t.get(e.Ln.WORKSPACE_ROOT_LABELS))}}function W(e){return rc(e).activeRoots}function G(e){return rc(e).roots}function ic(e){return rc(e).labels}function K(t,n){t.set(e.Ln.ACTIVE_WORKSPACE_ROOTS,n)}function ac(t,n){t.set(e.Ln.WORKSPACE_ROOT_OPTIONS,n)}function oc(t,n){let r=n(t.get(e.Ln.WORKSPACE_ROOT_OPTIONS)??null);return t.set(e.Ln.WORKSPACE_ROOT_OPTIONS,r),r}function sc(t,n){let r=n(t.get(e.Ln.WORKSPACE_ROOT_LABELS)??null);return t.set(e.Ln.WORKSPACE_ROOT_LABELS,r),r}function cc(e,t){if(W(e).length>0)return;let n=G(e);if(n.length>0){K(e,[n[0]]);return}if(!t||t.length===0)return;let r=new Set,i=t.map(e=>e.path.trim()).filter(e=>e.length>0).filter(e=>r.has(e)?!1:(r.add(e),!0));i.length!==0&&(ac(e,i),K(e,[i[0]]),sc(e,(e={})=>{let n={...e};return t.forEach(e=>{let t=e.name.trim();t&&(n[e.path]??(n[e.path]=t))}),n}))}var lc=!!process.env.WSL_DISTRO_NAME,q=e.sn(`electron-fetch-handler`),uc=e=>({preferWsl:lc,hostConfig:e}),dc=new Set([`pdf`,`ppt`,`pptx`,`doc`,`docx`,`xls`,`xlsx`,`key`,`mov`,`mp4`,`pages`,`numbers`,`html`,`png`,`jpg`,`jpeg`,`gif`,`bmp`,`tiff`,`ico`,`webp`]);function fc(t,n){if(!(0,a.existsSync)(t)||!(0,a.statSync)(t).isFile())return!1;let r=e.qt(n).extname(t).toLowerCase();return r?dc.has(r.slice(1)):!1}function pc({persistPreferredTargetPath:e,hasExplicitTarget:t,hasExistingPreference:n}){return e?t?!0:!n:!1}var mc=class extends Error{};function hc(t){let n=t.get(e.Ln.PINNED_THREAD_IDS);return Array.isArray(n)?n.filter(e=>typeof e==`string`):[]}function gc({globalState:t,threadId:n,pinned:r}){let i=hc(t),a=r?[n,...i.filter(e=>e!==n)]:i.filter(e=>e!==n);return i.length!==a.length||i.some((e,t)=>e!==a[t])?(t.set(e.Ln.PINNED_THREAD_IDS,a),!0):!1}function _c({globalState:t,threadIds:n}){let r=hc(t);return r.length!==n.length||r.some((e,t)=>e!==n[t])?(t.set(e.Ln.PINNED_THREAD_IDS,n),!0):!1}var vc=class{bundledSkillsRoot;isMultiClientTransport(){return this.appServerConnectionRegistry.getConnection(this.hostConfig.id).getTransportKind()===`stdio`}constructor(t,n,r,i,a,o,s,c,l,u,d,f){this.globalState=t,this.getIpcClientForOrigin=n,this.appServerConnectionRegistry=r,this.gitManager=i,this.hostConfig=a,this.getHostConfigForHostId=o,this.refreshRemoteSshConnections=s,this.saveCodexManagedRemoteSshConnections=c,this.setRemoteSshConnectionAutoConnect=l,this.hotkeyWindowHotkeyController=u,this.windowManager=d,this.terminalManager=f,this.bundledSkillsRoot=yc(this.hostConfig),e.h({refresh:!1,preferWsl:lc,bundledRepoRoot:this.bundledSkillsRoot,hostConfig:this.hostConfig}).catch(e=>{q().warning(`Failed to warm recommended skills cache`,{safe:{},sensitive:{error:e}})})}handlers={"active-workspace-roots":async()=>({roots:ii(W(this.globalState),this.hostConfig,this.shouldUseWslPaths())}),"workspace-root-options":async()=>{let e=ai(ic(this.globalState),this.hostConfig,this.shouldUseWslPaths());return{roots:ii(G(this.globalState),this.hostConfig,this.shouldUseWslPaths()),labels:e}},"add-workspace-root-option":async({root:e,label:t,setActive:n,origin:r})=>{let i=ei(e,this.shouldUseWslPaths());return t&&sc(this.globalState,(e={})=>({...e,[i]:t})),oc(this.globalState,e=>[i,...(e??[]).filter(e=>e!==i)]),r.send(I,{type:`workspace-root-options-updated`}),n&&(K(this.globalState,[i]),r.send(I,{type:`active-workspace-roots-updated`})),{success:!0}},"electron-clone-workspace-repo":async()=>({success:!1,canceled:!1,error:`Not implemented in Electron.`}),"codex-home":async()=>{let t=e.qt(this.hostConfig),n=e.In(this.hostConfig)?await e.Yt(this.hostConfig):e.Jt({preferWsl:lc,hostConfig:this.hostConfig});return{codexHome:R(n,this.shouldUseWslPaths()),worktreesSegment:R(t.join(n,`worktrees`),this.shouldUseWslPaths())}},"fast-mode-rollout-metrics":async t=>e.In(this.hostConfig)?null:e.et({codexHome:e.Jt({preferWsl:lc,hostConfig:this.hostConfig}),params:t}),"refresh-remote-ssh-connections":async()=>({remoteConnections:await this.refreshRemoteSshConnections()}),"save-codex-managed-remote-ssh-connections":async({remoteConnections:e})=>({remoteConnections:await this.saveCodexManagedRemoteSshConnections(e)}),"set-remote-ssh-connection-auto-connect":async({hostId:e,autoConnect:t})=>({remoteConnections:await this.setRemoteSshConnectionAutoConnect(e,t)}),"app-server-connection-state":async({hostId:e})=>{let t=this.appServerConnectionRegistry.getMaybeConnection(e);return{state:t?.getConnectionState()??`disconnected`,errorMessage:t?.getErrorMessage()??null}},"has-custom-cli-executable":async({windowHostId:e})=>({hasCustomCliExecutable:this.appServerConnectionRegistry.getConnection(e).hasCustomCliExecutable()}),"account-info":async({windowHostId:e})=>{try{let t=await this.appServerConnectionRegistry.getConnection(e).getAuthToken({refreshToken:!1});if(!t)return{accountId:null,userId:null,plan:null,email:null};let[,n]=t.split(`.`);if(!n)throw Error(`Missing token payload`);let r=JSON.parse(Buffer.from(n,`base64url`).toString(`utf8`)),i=r[`https://api.openai.com/auth`],a=r[`https://api.openai.com/profile`],o=i&&typeof i==`object`&&i?i.chatgpt_account_id:null,s=i&&typeof i==`object`&&i?i.chatgpt_user_id:null,c=i&&typeof i==`object`&&i?i.chatgpt_plan_type:null,l=a&&typeof a==`object`&&a?a.email:null;if(typeof o==`string`&&typeof s==`string`&&typeof c==`string`)return{accountId:o,userId:s,plan:c,email:typeof l==`string`?l:null}}catch(e){q().error(`Unable to extract account id and plan from auth token`,{safe:{error:e}})}return{accountId:null,userId:null,plan:null,email:null}},"extension-info":async()=>({version:e.qn(t.app.getVersion()).version,buildNumber:e.a.value,buildFlavor:e.c.resolve()}),"third-party-notices":async()=>({text:await qs()}),"locale-info":async()=>({ideLocale:t.app.getLocale(),systemLocale:t.app.getSystemLocale()}),"os-info":async()=>({platform:process.platform,hasWsl:e.$t()!=null,isVsCodeRunningInsideWsl:!1}),"child-processes":async()=>{try{return{processes:await e.x(process.pid)}}catch(e){return q().error(`Failed to list child processes`,{safe:{},sensitive:{error:e}}),{processes:[]}}},"hotkey-window-hotkey-state":async()=>this.hotkeyWindowHotkeyController.getState(),"hotkey-window-set-hotkey":async({hotkey:e})=>this.hotkeyWindowHotkeyController.setHotkey(e),"hotkey-window-set-dev-hotkey-override":async({enabled:e})=>this.hotkeyWindowHotkeyController.setDevOverrideEnabled(e),"feedback-create-sentry-issue":async({classification:n,description:r,includeLogs:i,correlationId:a})=>(await yi({appVersion:t.app.getVersion(),buildFlavor:e.c.resolve(),buildNumber:e.a.value,classification:n,description:r,includeLogs:i,correlationId:a}),{reportId:a}),"openai-api-key":async()=>({value:process.env.OPENAI_API_KEY??null}),"recommended-skills":async({refresh:t})=>e.h({refresh:t,preferWsl:lc,bundledRepoRoot:this.bundledSkillsRoot,hostConfig:this.hostConfig}),"local-plugins":async({roots:t})=>e.y({roots:t??[],preferWsl:lc,hostConfig:this.hostConfig}),"install-recommended-skill":async({skillId:t,repoPath:n,installRoot:r})=>{try{let{destination:i}=await e.m({skillId:t,repoPath:n,installRoot:r,preferWsl:lc,bundledRepoRoot:this.bundledSkillsRoot,hostConfig:this.hostConfig});return{success:!0,destination:i,error:null}}catch(e){let t=e instanceof Error?e.message:String(e);return q().warning(`Failed to install recommended skill`,{safe:{},sensitive:{error:t}}),{success:!1,destination:null,error:t}}},"remove-skill":async({skillPath:t})=>{try{let{deletedPath:n}=await e.p({skillPath:t,preferWsl:lc,hostConfig:this.hostConfig});return{success:!0,deletedPath:n,error:null}}catch(e){let t=e instanceof Error?e.message:String(e);return q().warning(`Failed to remove skill`,{safe:{},sensitive:{error:t}}),{success:!1,deletedPath:null,error:t}}},"read-file":async({path:t})=>{let n=this.resolveOpenFilePath(this.mapAgentPathToLocalPath(t)??t,this.getWorkspaceRoot());return{contents:await e.Kt.readFile(n,this.hostConfig)}},"read-file-binary":async({path:t})=>{let n=this.resolveOpenFilePath(this.mapAgentPathToLocalPath(t)??t,this.getWorkspaceRoot());return{contentsBase64:(await e.Kt.readFileBase64(n,this.hostConfig)).toString(`base64`)}},"read-git-file-binary":async({cwd:t,path:n,ref:r})=>{let i=this.mapAgentPathToLocalPath(t);if(i==null)return{contentsBase64:null};try{let t=(await this.gitManager.getWorktreeRepository(e.zn(i),this.hostConfig))?.root;if(!t)return{contentsBase64:null};let a=e.qt(this.hostConfig),o=e.Y(t,i,n);if(a.isAbsolute(o)||o.startsWith(`..`))return{contentsBase64:null};let s=await e.Z(t,[`show`,r===`head`?`HEAD:${o}`:`:${o}`],this.hostConfig);return s.code!==0||s.stdout.length===0?{contentsBase64:null}:{contentsBase64:s.process?.getStdout().toString(`base64`)??null}}catch{return{contentsBase64:null}}},"codex-agents-md":async()=>e.Wt(uc(this.hostConfig)),"codex-agents-md-save":async({contents:t})=>e.Gt(t,uc(this.hostConfig)),"generate-thread-title":async({prompt:t,cwd:n,windowHostId:r})=>{try{return{title:await te({prompt:t,cwd:n??null,serviceTier:e.hn(this.globalState.get(y)),appServerConnection:this.appServerConnectionRegistry.getConnection(r)})}}catch(e){return q().warning(`Failed to generate thread title`,{safe:{},sensitive:{error:e}}),{title:null}}},"generate-pull-request-message":async({prompt:t,cwd:n,windowHostId:r})=>{try{let i=await js({prompt:t,cwd:n??null,serviceTier:e.hn(this.globalState.get(y)),appServerConnection:this.appServerConnectionRegistry.getConnection(r)});return{title:i?.title??null,body:i?.body??null}}catch{return{title:null,body:null}}},"inbox-items":async({limit:t})=>({items:e.O(t??200)}),"list-automations":async()=>({items:e.pt()}),"developer-instructions":async({baseInstructions:t})=>({instructions:e.st({baseInstructions:t,globalState:this.globalState})}),"list-pending-automation-run-threads":async()=>({threadIds:e.xt()}),"pending-automation-runs":async()=>({runs:e.St()}),"automation-run-archive":async({threadId:t,archivedAssistantMessage:n,archivedUserMessage:r,archivedReason:i,windowHostId:a})=>{if(n!=null||r!=null)e.Et(t,r??null,n??null);else try{await this.appServerConnectionRegistry.getConnection(a).captureAutomationArchiveMessages(t)}catch(e){q().warning(`Failed to capture automation archive messages`,{safe:{error:e instanceof Error?e.message:String(e)},sensitive:{}})}let o=e.wt(t,i??void 0);return o&&this.appServerConnectionRegistry.getConnection(a).notifyAutomationRunsUpdated(),{success:o}},"automation-run-delete":async({threadId:t,windowHostId:n})=>{let r=e.yt(t);return r&&this.appServerConnectionRegistry.getConnection(n).notifyAutomationRunsUpdated(),{success:r}},"list-pinned-threads":async()=>({threadIds:hc(this.globalState)}),"set-thread-pinned":async({threadId:e,pinned:t,origin:n})=>{let r=gc({globalState:this.globalState,threadId:e,pinned:t});return n.send(I,{type:`pinned-threads-updated`}),{success:r}},"set-pinned-threads-order":async({threadIds:e,origin:t})=>{let n=_c({globalState:this.globalState,threadIds:e});return t.send(I,{type:`pinned-threads-updated`}),{success:n}},"automation-create":async({name:t,prompt:n,cwds:r,executionEnvironment:i,rrule:a})=>{let o=e.lt({name:t,prompt:n,cwds:r,executionEnvironment:i,rrule:a});if(!o)throw Error(`Automation create failed.`);return{item:o}},"automation-update":async({id:t,name:n,prompt:r,status:i,cwds:a,executionEnvironment:o,rrule:s})=>{let c=e.gt({id:t,name:n,prompt:r,status:i,cwds:a,executionEnvironment:o,rrule:s});if(!c)throw Error(`Automation update failed.`);return{item:c}},"automation-delete":async({id:t,windowHostId:n})=>{let r=e.ut(t);return r&&(e.bt(t),this.appServerConnectionRegistry.getConnection(n).notifyAutomationRunsUpdated()),{success:r}},"automation-run-now":async({id:e,windowHostId:t})=>(await pe({automationId:e,appServerConnection:this.appServerConnectionRegistry.getConnection(t),gitManager:this.gitManager,globalState:this.globalState,hostConfig:this.hostConfig}),{success:!0}),"find-files":async({query:e,cwd:t})=>{let n=typeof t==`string`&&t.trim().length>0?this.mapAgentPathToLocalPath(t):null;return{files:(await Pi({query:e??``,cwd:n??this.getWorkspaceRoot()})).map(e=>({...e,path:R(e.path,this.shouldUseWslPaths()),fsPath:R(e.fsPath,this.shouldUseWslPaths())}))}},"paths-exist":async({paths:t})=>({existingPaths:Qr(await e.P(ti(t??[],this.shouldUseWslPaths()),this.hostConfig),this.shouldUseWslPaths())}),"local-environment":async({configPath:t})=>{let n=await e.$(this.mapAgentPathToLocalPath(t)??t,this.hostConfig);return{environment:{...n,configPath:R(n.configPath,this.shouldUseWslPaths())}}},"local-environments":async({workspaceRoot:t})=>({environments:(await e.Q(this.mapAgentPathToLocalPath(t)??t,this.hostConfig)).map(e=>({...e,configPath:R(e.configPath,this.shouldUseWslPaths())}))}),"local-environment-config":async({configPath:t})=>{let n=this.mapAgentPathToLocalPath(t)??t;try{let t=await e.Kt.readFile(n,this.hostConfig);return{configPath:R(n,this.shouldUseWslPaths()),exists:!0,raw:t}}catch(e){let t=e;if(`code`in t&&t.code===`ENOENT`)return{configPath:R(n,this.shouldUseWslPaths()),exists:!1,raw:null};throw e}},"local-environment-config-save":async({configPath:t,raw:n})=>{let r=this.mapAgentPathToLocalPath(t)??t,i=e.qt(this.hostConfig);return await e.Kt.mkdir(i.dirname(r),{recursive:!0},this.hostConfig),await e.Kt.writeFile(r,n,this.hostConfig),{configPath:R(r,this.shouldUseWslPaths()),success:!0}},"is-copilot-api-available":async()=>({available:!1}),"get-copilot-api-proxy-info":async()=>null,"get-global-state":async({key:e})=>({value:this.globalState.get(e)}),"set-global-state":async({key:e,value:t,origin:n})=>(this.globalState.set(e,t),{success:!0}),"get-configuration":async({key:e})=>({value:this.globalState.get(e)}),"set-configuration":async({key:t,value:n})=>(this.globalState.set(t,n),t===e.$n.APPEARANCE_THEME&&Ms(n),(t===e.$n.APPEARANCE_THEME||t===e.$n.OPAQUE_WINDOWS)&&this.windowManager.refreshWindowBackdropForHost(this.hostConfig.id),{success:!0}),"apply-patch":async({origin:t,...n})=>e.J(n,this.gitManager,this.hostConfig,ni(this.shouldUseWslPaths())),"mcp-codex-config":async()=>({config:null}),"ide-context":async({workspaceRoot:e,origin:t})=>{if(!e)throw Error(`workspaceRoot is required`);if(!this.isMultiClientTransport())throw Error(`IPC is disabled when connected to a non-stdio app server transport`);let n=this.getIpcClientForOrigin(t);if(!n)throw Error(`Missing IPC client for window`);let r=await n.sendRequest(`ide-context`,{workspaceRoot:e});if(r.resultType===`error`)throw Error(`Failed to get ide context: ${r.error}`);return{ideContext:r.result.ideContext}},"ipc-request":async({method:e,params:t,targetClientId:n,origin:r})=>{if(!this.isMultiClientTransport())throw Error(`IPC is disabled when connected to a non-stdio app server transport`);let i=this.getIpcClientForOrigin(r);if(!i)throw Error(`Missing IPC client for window`);return await i.sendRequest(e,t,{targetClientId:n})},"open-in-targets":async({cwd:e})=>{let t=await _s(),n=await gs(),r=new Set(t),i=Os(this.globalState,e,r);return{preferredTarget:i,availableTargets:Array.from(r),targets:n.map(({id:e,label:t,icon:n})=>({id:e,label:t,icon:n,available:r.has(e),default:i===e||void 0}))}},"set-preferred-app":async({target:e})=>(As(this.globalState,null,e),{success:!0}),"terminal-shell-options":async()=>({availableShells:Fs()}),"thread-terminal-snapshot":async({threadId:e})=>({session:this.terminalManager.getSnapshotForConversationId(e)}),"pick-file":async({pickerTitle:e})=>({file:(await this.pickFilesFromDialog({allowMultiple:!1,title:e}))[0]??null}),"pick-files":async({pickerTitle:e})=>({files:await this.pickFilesFromDialog({allowMultiple:!0,title:e})}),"open-file":async({path:t,target:n,line:r,column:i,cwd:a,persistPreferredTargetPath:o})=>{try{let s=t.replace(/^([ab])[\\/]/,``),c=this.mapAgentPathToLocalPath(a),l=this.mapAgentPathToLocalPath(s)??s,u=c??this.getWorkspaceRoot(),d=e.In(this.hostConfig)?a:null,f=n!=null,p=n;if(!p){let e=await _s(),t=new Set(e);p=Os(this.globalState,u,t)}if(p=Li({target:p,hasExplicitTarget:f,platform:process.platform}),!p)throw Error(`No available open target`);let m=this.resolveOpenFilePath(l,u),h=d?this.resolveOpenFilePath(s,d):null,g=fc(m,this.hostConfig);pc({persistPreferredTargetPath:o,hasExplicitTarget:f,hasExistingPreference:o!=null&&ks(this.globalState,o)})&&As(this.globalState,o??null,p);let _=typeof r==`number`?{line:r,column:typeof i==`number`?i:1}:null;return g&&_==null&&!f&&Ts(p)&&(p=`fileManager`),await vs(p,m,_,this.hostConfig,d,h),{success:!0}}catch(e){return q().error(`Failed to open file`,{safe:{},sensitive:{error:e}}),{success:!1}}},"git-origins":async({dirs:t,hostId:r})=>{let i=r!=null&&r!==this.hostConfig.id?this.getHostConfigForHostId(r):this.hostConfig,a=e.rn(i),o=ti(t??[],a).map(t=>e.Bn(t)),s=R((0,n.homedir)(),a),c=G(this.globalState),l=W(this.globalState),u=c.length>0?c:l??[],d=o&&o.length>0?o:u??[],f=(await Promise.all(d.map(async t=>{try{let n=await this.gitManager.getWorktreeRepository(e.zn(t),i);return n?.root?(await e.H(n.root,i)).map(e=>e.root):[]}catch(e){return q().warning(`[git-origins] failed to list worktrees for`,{safe:{},sensitive:{dir:t,error:e}}),[]}}))).flat();return{origins:(await e.R(Array.from(new Set([...d,...f])),this.gitManager,i)).map(t=>({...t,dir:e.zn(R(t.dir,a)),root:e.Bn(R(t.root,a)),commonDir:t.commonDir==null?null:R(t.commonDir,a)})),homeDir:s}},"git-push":async t=>e.L(t,this.gitManager,this.hostConfig),"git-create-branch":async t=>e.B(t,this.gitManager,this.hostConfig),"git-checkout-branch":t=>e.V(t,this.hostConfig),"gh-cli-status":async()=>e.A(this.hostConfig),"gh-pr-create":async t=>e.j(t,this.gitManager,this.hostConfig),"gh-pr-status":t=>e.N(t,this.gitManager,this.hostConfig),"gh-pr-merge":async t=>e.M(t,this.gitManager,this.hostConfig),"git-merge-base":async({gitRoot:t,baseBranch:n})=>{let r=await this.resolveGitRootFromRequest(t);return!r||!n?{mergeBaseSha:null}:{mergeBaseSha:await e.U(r,n,this.hostConfig)}},"prepare-worktree-snapshot":async({gitRoot:t,snapshotBranch:n})=>{if(!t)throw Error(`gitRoot is required`);return e.I(t,n??null,this.hostConfig)},"upload-worktree-snapshot":async({tarballPath:t,uploadUrl:n,contentLength:r,contentType:i})=>{try{return await e.F({tarballPath:t,uploadUrl:n,contentLength:r,contentType:i}),{success:!0}}catch(e){return q().error(`Failed to upload worktree snapshot`,{safe:{},sensitive:{error:e}}),{success:!1}}},"confirm-trace-recording-start":async()=>({success:Zs()}),"cancel-trace-recording-start":async()=>({success:Qs()}),"submit-trace-recording-details":async({note:e,conversationId:t})=>({success:ec({note:e,conversationId:t})}),"set-vs-context":async()=>{throw new mc}};async pickFilesFromDialog({allowMultiple:n,title:r}){let i=await t.dialog.showOpenDialog({properties:[`openFile`,...n?[`multiSelections`]:[]],title:r});if(i.canceled||i.filePaths.length===0)return[];let a=e.qt(this.hostConfig);return i.filePaths.map(e=>({label:a.basename(e),path:e,fsPath:e}))}resolveOpenFilePath(t,n){let r=e.qt(this.hostConfig),i=t.replace(/^([ab])[\\/]/,``),o=r.normalize(i);if(e.T(o,r)||!n)return o;let s=o.split(/[\\/]+/).filter(Boolean);if(s.length===0)return n;let c=r.join(n,...s);if((0,a.existsSync)(c))return c;let l=r.basename(n),u=s.indexOf(l);if(u!==-1){let e=r.join(n,...s.slice(u+1));return(0,a.existsSync)(e),e}return c}mapRemoteHomeToLocalPath(t){if(!t)return t??null;if(!e.In(this.hostConfig))return t;let r=this.hostConfig.home_dir;if(!r)return t;let i=(0,n.homedir)();return t===r?i:t.startsWith(`${r}/`)?`${i}${t.slice(r.length)}`:t}mapAgentPathToLocalPath(e){let t=this.mapRemoteHomeToLocalPath(e);return t&&ei(t,this.shouldUseWslPaths())}shouldUseWslPaths(){return e.rn(this.hostConfig)}getWorkspaceRoot(){return(W(this.globalState)[0]??null)||(G(this.globalState)[0]??null??null)}async getSelectedGitRoot(){let t=W(this.globalState)[0];return t?(await this.gitManager.getWorktreeRepository(e.zn(t),this.hostConfig))?.root??null:null}async resolveGitRootFromRequest(t){if(t){let n=t.trim();if(n.length>0){let t=(await this.gitManager.getWorktreeRepository(e.zn(n),this.hostConfig))?.root;if(t)return t}}return this.getSelectedGitRoot()}handleVSCodeRequest(e,t,n,r){try{return this.handlers[t]({...r,origin:e,windowHostId:n})}catch(e){throw e instanceof mc?Error(`${t} not implemented in Electron.`):e}}};function yc(n){let r=e.qt(n),i=t.app.getAppPath();if(t.app.isPackaged)return r.join(i,`skills`);let o=r.join(i,`assets`,`skills`);if((0,a.existsSync)(o))return o;let s=r.join(i,`..`,`assets`,`skills`);return(0,a.existsSync)(s)?s:null}var bc=6,xc=new Set([`accounts`,`backend-api`,`by-repo`,`conversation`,`environments`,`account`,`announcement`,`data-controls`,`file-preview`,`first-run`,`general-settings`,`git-settings`,`local-environments`,`mcp-settings`,`open-source-licenses`,`personalization`,`plan-summary`,`select-workspace`,`settings`,`usage-analytics`,`worktree-init-v2`,`worktrees`]);function Sc(e){let t=e.length>0?e:`/`;return t===`/`?`/`:`/${t.split(`/`).filter(e=>e.length>0).map(e=>{let t=null;try{t=decodeURIComponent(e)}catch{t=null}let n=(t??e).toLowerCase();return n.length<=bc||xc.has(n)?n:`:param`}).join(`/`)}`}function Cc(e,t){try{let n=new URL(t);if(n.protocol!==`http:`&&n.protocol!==`https:`)return`${e} ${n.protocol}`;let r=Sc(n.pathname);return`${e} ${n.origin}${r}`}catch{return`${e} ${t}`}}function wc(e){if(!URL.canParse(e))return;let t=new URL(e);if(t.protocol!==`http:`&&t.protocol!==`https:`)return t.protocol;let n=Sc(t.pathname);return`${t.origin}${n}`}var Tc=e.sn(`electron-fetch-wrapper`),Ec=`vscode://codex/`,Dc=class{abortControllers=new Map;apiBaseUrl;constructor(e,t){this.fetchHandler=e,this.options=t,this.apiBaseUrl=this.resolveApiBaseUrl()}getAppServerConnection(e){return this.options.appServerConnectionRegistry.getConnection(e)}async handleRequest(e,n){if(this.isVsCodeFetchRequest(n.url)){await this.handleVsCodeFetchRequest(e,n);return}let r=new AbortController;this.abortControllers.set(n.requestId,r);let i=this.ensureAbsoluteUrl(n.url),a=Cc(n.method,i);await this.options.desktopSentry.startSpan({op:`codex.fetch`,name:a},async()=>{try{let{headers:o,body:s}=this.prepareFetchInit(n),c=this.shouldAttachAuth(i,o),l=()=>{if(typeof s==`string`)return s;if(s==null)return;let e=new Uint8Array(s.byteLength);return e.set(s),e.buffer},u=async e=>{let a=this.cloneHeaders(o);if(c){if(!e)throw Error(`Missing auth token`);this.applyDesktopAuthHeaders(a,e)}return t.net.fetch(i,{method:n.method,headers:a,body:l(),signal:r.signal})},d=null;if(c){try{d=await this.getAppServerConnection(n.hostId).getAuthToken({refreshToken:!1})}catch(t){Tc().error(`Failed to retrieve auth token`,{safe:{},sensitive:{error:t}}),e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`error`,requestId:n.requestId,status:432,error:`Failed to retrieve authentication token`});return}if(!d){e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`error`,requestId:n.requestId,status:401,error:`Cannot fetch ${n.url} without ChatGPT auth.`});return}}Tc().debug(`Fetch`,{safe:{target:a,body:n.body==null?``:``},sensitive:{}});let f=await u(d);if(c&&f.status===401){try{d=await this.getAppServerConnection(n.hostId).getAuthToken({refreshToken:!0})}catch(t){Tc().error(`Failed to refresh auth token`,{safe:{},sensitive:{error:t}}),e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`error`,requestId:n.requestId,status:401,error:`Failed to refresh authentication token`});return}if(!d){e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`error`,requestId:n.requestId,status:401,error:`Cannot fetch ${n.url} without ChatGPT auth.`});return}f=await u(d)}let p={};if(f.headers.forEach((e,t)=>{p[t]=e}),f.ok){let t=f.headers.get(`content-type`)??``,r=`null`;if(f.status!==204)if(t.includes(`application/json`)){let e=await f.text();try{r=JSON.stringify(JSON.parse(e))}catch(t){Tc().debug(`Failed to parse JSON response, falling back to text`,{safe:{},sensitive:{text:e,error:t}}),r=JSON.stringify(e)}}else{let e=await f.arrayBuffer(),n=Buffer.from(e).toString(`base64`);r=JSON.stringify({base64:n,contentType:t})}e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`success`,requestId:n.requestId,status:f.status,headers:p,bodyJsonString:r})}else{let t;try{t=await f.text()||f.statusText||`Request failed with status ${f.status}`}catch{t=f.statusText||`Request failed with status ${f.status}`}e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`error`,requestId:n.requestId,status:f.status,error:t})}}catch(t){let r=t instanceof Error&&t.name===`AbortError`;e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`error`,requestId:n.requestId,status:r?499:500,error:t instanceof Error?t.message:`Unknown error`})}finally{this.abortControllers.delete(n.requestId)}})}cancelRequest(e){let t=this.abortControllers.get(e.requestId);t&&(t.abort(),this.abortControllers.delete(e.requestId))}handleStreamRequest(e,t){Tc().warning(`Fetch-stream not implemented, requestId`,{safe:{requestId:t.requestId},sensitive:{}}),e.send(this.options.messageChannel,{type:`fetch-stream-error`,requestId:t.requestId,error:`Streaming fetch is not implemented in the Electron shell.`})}cancelStreamRequest(e){Tc().debug(`Cancel-fetch-stream received with requestId`,{safe:{requestId:e.requestId},sensitive:{}})}prepareFetchInit(e){let t={},n=e.body,r=!1;if(e.headers)for(let[n,i]of Object.entries(e.headers)){if(n.toLowerCase()===`x-codex-base64`){r=i===`1`;continue}t[n]=i}if(r&&typeof n==`string`)try{n=Buffer.from(n,`base64`)}catch(e){Tc().warning(`Failed to decode base64 request body`,{safe:{},sensitive:{body:n,error:e}})}if(!r&&typeof n==`string`&&!this.hasHeader(t,`Content-Type`)){let e=n.trim();if(e.startsWith(`{`)||e.startsWith(`[`))try{JSON.parse(n),this.setHeader(t,`Content-Type`,`application/json`)}catch{}}return{headers:t,body:n}}resolveApiBaseUrl(){let e=process.env.CODEX_API_BASE_URL;return e&&e.trim().length>0?e.replace(/\/+$/,``):(process.env.CODEX_API_ENDPOINT??``).toLowerCase()===`localhost`?this.options.devApiBaseUrl:this.options.prodApiBaseUrl}ensureAbsoluteUrl(e){return/^https?:\/\//i.test(e)||e.startsWith(`data:`)?e:`${this.apiBaseUrl}/${e.replace(/^\/+/,``)}`}cloneHeaders(e){let t={};for(let[n,r]of Object.entries(e))t[n]=r;return t}findHeaderKey(e,t){let n=t.toLowerCase();return Object.keys(e).find(e=>e.toLowerCase()===n)}hasHeader(e,t){return this.findHeaderKey(e,t)!=null}setHeader(e,t,n){let r=this.findHeaderKey(e,t);r&&delete e[r],e[t]=n}shouldAttachAuth(e,t){if(this.hasHeader(t,`authorization`))return!1;let n;try{n=new URL(e)}catch{return!1}if(n.protocol!==`http:`&&n.protocol!==`https:`)return!1;let r=n.host.toLowerCase();return!!(r===`localhost`||r===`localhost:8000`||r===`openai.com`||r.endsWith(`.openai.com`)||r===`chatgpt.com`||r.endsWith(`.chatgpt.com`)&&!r.startsWith(`ab.`))}applyDesktopAuthHeaders(e,t){this.setHeader(e,`Authorization`,`Bearer ${t}`);let n=this.extractChatGptAccountId(t);n&&!this.hasHeader(e,`ChatGPT-Account-Id`)&&this.setHeader(e,`ChatGPT-Account-Id`,n),this.hasHeader(e,`originator`)||this.setHeader(e,`originator`,this.options.desktopOriginator),this.hasHeader(e,`User-Agent`)||this.setHeader(e,`User-Agent`,this.buildDesktopUserAgent())}extractChatGptAccountId(e){let t=e.split(`.`);if(t.length<2)return null;try{let e=JSON.parse(Buffer.from(t[1],`base64url`).toString(`utf8`))[`https://api.openai.com/auth`];if(e&&typeof e==`object`&&e){let t=e.chatgpt_account_id;return typeof t==`string`?t:null}}catch{}return null}buildDesktopUserAgent(){return`Codex Desktop/${t.app.getVersion()} (${process.platform}; ${process.arch})`}isVsCodeFetchRequest(e){return e.startsWith(Ec)}async handleVsCodeFetchRequest(e,t){let n=t.url.slice(15);await this.options.desktopSentry.startSpan({op:`codex.vscode.request`,name:n},async()=>{try{let r=await this.fetchHandler.handleVSCodeRequest(e,n,t.hostId,t.body?JSON.parse(t.body):void 0);e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`success`,requestId:t.requestId,status:200,headers:{"content-type":`application/json`},bodyJsonString:JSON.stringify(r)})}catch(n){e.send(this.options.messageChannel,{type:`fetch-response`,responseType:`error`,requestId:t.requestId,status:432,error:n instanceof Error?n.message:`VS Code bridge request failed`})}})}},Oc=class{id;constructor(e){this.webContents=e,this.id=e.id}send(e,t){this.webContents.send(e,t)}isDestroyed(){return this.webContents.isDestroyed()}onDestroyed(e){let t=()=>{e()};return this.webContents.once(`destroyed`,t),{dispose:()=>{this.webContents.removeListener(`destroyed`,t)}}}},kc=e.sn(`export-logs`);function Ac(e){return e.toString().padStart(2,`0`)}function jc(e,t){return(0,r.join)(e,t.getUTCFullYear().toString(),Ac(t.getUTCMonth()+1),Ac(t.getUTCDate()))}function Mc(e){return new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()))}function Nc(e,t){return Array.from({length:t},(t,n)=>{let r=new Date(e);return r.setUTCDate(r.getUTCDate()-n),r})}function Pc(e,t){let n=Mc(e);switch(t){case`today`:return[n];case`last7days`:case`session`:return Nc(n,7)}}function Fc(e,t,n){switch(e){case`today`:case`last7days`:return!0;case`session`:return n.startsWith(`codex-desktop-${t}-`)}}async function Ic(e){let t=Pc(e.now,e.scope);return(await Promise.all(t.map(async t=>{let n=jc(e.rootDir,t);try{return(await(0,u.readdir)(n,{withFileTypes:!0})).filter(t=>t.isFile()&&Fc(e.scope,e.appSessionId,t.name)).map(e=>(0,r.join)(n,e.name))}catch(e){return kc().error(`Failed to read log dir`,{safe:{},sensitive:{dir:n,error:e}}),[]}}))).flat().sort()}function Lc(e,t){switch(e){case`today`:return`codex-logs-today.txt`;case`last7days`:return`codex-logs-last7days.txt`;case`session`:return`codex-logs-session-${t.slice(0,8)}.txt`}}async function Rc(e){let n={defaultPath:e.suggestedName,filters:[{name:`Text`,extensions:[`txt`]}]},i=e.parentWindow?await t.dialog.showSaveDialog(e.parentWindow,n):await t.dialog.showSaveDialog(n);return i.canceled||!i.filePath?null:(0,r.extname)(i.filePath).toLowerCase()===`.txt`?i.filePath:`${i.filePath}.txt`}async function zc(e){try{return await(0,u.readFile)(e,`utf8`)}catch(e){return kc().error(`Failed to read log file`,{safe:{},sensitive:{error:e}}),``}}function Bc(e){let t=e.indexOf(` `);if(t<=0)return null;let n=e.slice(0,t),r=Date.parse(n);return Number.isFinite(r)?r:null}async function Vc(e){let t=await Promise.all(e.sort().map(e=>zc(e))),n=[],r=0;for(let e of t)for(let t of e.split(/\r?\n/))t.length!==0&&(n.push({order:r,timestampMs:Bc(t),text:t}),r+=1);return n.sort((e,t)=>{let n=e.timestampMs,r=t.timestampMs;return n!=null&&r!=null?n-r||e.order-t.order:n==null?r==null?e.order-t.order:1:-1}),n.length>0?`${n.map(e=>e.text).join(` -`)}\n`:``}async function Hc(e){let t=new Date,n=await Ic({rootDir:ve(),now:t,scope:e.scope,appSessionId:e.appSessionId}),r=await Rc({parentWindow:e.parentWindow,suggestedName:Lc(e.scope,e.appSessionId)});r&&await(0,u.writeFile)(r,await Vc(n),`utf8`)}var Uc=3e4,Wc=5e3,Gc=6e4,Kc=class{logger=e.sn(`sampler-manager`);appStateSnapshotRequestById=new e.Wn({ttlMs:Gc});appStateTriggerMinIntervalGate=new e.jn({minIntervalMs:Wc});constructor(e,t,n,r,i){this.hostId=e,this.windowManager=t,this.desktopSentry=n,this.electronSampler=r,this.options=i,setInterval(()=>{this.requestAppStateSnapshot(`heartbeat`)},Uc).unref()}handleRendererReady(e){this.requestAppStateSnapshot(`heartbeat`,e)}async handleMessage(e,t){switch(t.type){case`electron-app-state-snapshot-trigger`:this.requestAppStateSnapshot(t.reason,e);break;case`electron-app-state-snapshot-response`:await this.handleAppStateSnapshotResponse(e,t);break}}async handleAppStateSnapshotResponse(e,t){let n=h.performance.now(),r=this.appStateSnapshotRequestById.get(t.requestId,n);if(!r||r.webContentsId!==e.id)return;this.appStateSnapshotRequestById.delete(t.requestId);let i=this.electronSampler.collectSnapshotFields(e),a={...t.fields,...i},o=this.options.getStdioIoStatsSnapshot();o!=null&&(a.app_server_stdio_bytes_read_total=o.bytesReadTotal,a.app_server_stdio_bytes_written_total=o.bytesWrittenTotal,a.app_server_stdio_bytes_read_last_30s=o.bytesReadLast30s,a.app_server_stdio_bytes_written_last_30s=o.bytesWrittenLast30s),this.logger().debug(`app_state_snapshot`,{safe:a,sensitive:{}}),this.desktopSentry.addBreadcrumb({category:`app_state`,level:`info`,message:`app_state_snapshot`,data:a})}requestAppStateSnapshot(e,t){let n=h.performance.now();if(e!==`heartbeat`&&!this.appStateTriggerMinIntervalGate.canPass(n))return;let r=t==null?this.windowManager.getPrimaryWindow(this.hostId):null;if(e===`heartbeat`&&t==null&&r!=null&&!r.isFocused())return;let i=t??r?.webContents;if(!i||i.isDestroyed()||!this.windowManager.isWebContentsReady(i.id))return;this.appStateTriggerMinIntervalGate.mark(n);let a=(0,c.randomUUID)();this.appStateSnapshotRequestById.set(a,{webContentsId:i.id},n),this.windowManager.sendMessageToWebContents(i,{type:`electron-app-state-snapshot-request`,hostId:this.windowManager.getHostIdForWebContents(i)??this.hostId,requestId:a,reason:e})}},qc=e.sn(`content-tracing`);async function Jc(e){let n=t.app.getAppMetrics().map(e=>({pid:e.pid,type:e.type,memory:e.memory==null?null:{workingSetSize:e.memory.workingSetSize,peakWorkingSetSize:e.memory.peakWorkingSetSize,privateBytes:e.memory.privateBytes??0,sharedBytes:0}})),r=e==null||e.isDestroyed()?null:await(async()=>{let t=e.webContents;if(typeof t.getProcessMemoryInfo==`function`)return t.getProcessMemoryInfo().then(e=>({private:e.private,shared:e.shared,residentSet:e.residentSet})).catch(()=>null);let r=t.getOSProcessId();if(r<=0)return null;let i=n.find(e=>e.pid===r);return i?.memory==null?null:{private:i.memory.privateBytes,shared:i.memory.sharedBytes,residentSet:i.memory.workingSetSize}})(),i=Object.fromEntries(Object.entries(t.app.getGPUFeatureStatus())),a=null,o=null;try{let e=Yc(await t.app.getGPUInfo(`basic`)),n=e?.gpuDevice??[],r=n.find(e=>e.active)??n[0];a=r?.vendorString??(r?.vendorId==null?null:String(r.vendorId));let i=e?.auxAttributes??{},s=i.glRenderer??i.gl_renderer;o=typeof s==`string`?s:null}catch(e){qc().warning(`Failed to collect GPU info for trace upload.`,{safe:{},sensitive:{error:e}})}return{appMetrics:n,rendererMemory:r,gpuFeatureStatus:i,gpuVendor:a,gpuRenderer:o,hardwareAccelerationEnabled:!t.app.commandLine.hasSwitch(`disable-gpu`)}}function Yc(e){return typeof e!=`object`||!e?null:e}var Xc=(0,i.promisify)(p.gzip),Zc=`application/x-sentry-envelope`,Qc=`application/gzip`,$c=`Desktop content trace recording`,el=`content-tracing`,tl=Buffer.from(` -`);async function nl({tracePath:t,buildFlavor:n,buildNumber:i,appVersion:a,traceRecordingNote:o,recordingDurationMs:s,correlation:l,runtimeHealth:d}){let f=await(0,u.readFile)(t),p=await Xc(f),m=f.byteLength,h=p.byteLength,g=ol(f),_=`${(0,r.basename)(t)}.gz`,v=new Date,y=rl({buildFlavor:n,buildNumber:i,appVersion:a,eventId:(0,c.randomUUID)().replaceAll(`-`,``),now:v,traceFileName:_,gzippedTraceContents:p,traceRecordingNote:o?.trim()??null,recordingDurationMs:s??null,correlation:l??null,runtimeHealth:d??null,traceByteLength:m,traceGzipByteLength:h,traceEventCount:g}),b=al(e.gn),x=new Blob([Uint8Array.from(y)],{type:Zc}),S=await fetch(b,{method:`POST`,headers:{"Content-Type":Zc},body:x});if(S.ok)return;let C=await S.text();throw Error(`Trace upload failed with status ${S.status} ${S.statusText}${C?`: ${C}`:``}`)}function rl({buildFlavor:t,buildNumber:r,appVersion:i,eventId:a,now:o,traceFileName:s,gzippedTraceContents:c,traceRecordingNote:l,recordingDurationMs:u,correlation:d,runtimeHealth:f,traceByteLength:p,traceGzipByteLength:m,traceEventCount:h}){let g=e.qn(i),_=t!==`prod`,v=o.toISOString(),y=n.default.cpus(),b=y[0]?.model,x=typeof n.default.version==`function`?n.default.version():void 0,S=process.versions.electron==null?`Node.js`:`Electron`,C=process.versions.electron??process.versions.node??`unknown`,w={event_id:a,timestamp:v,platform:`javascript`,level:`info`,logger:el,message:$c,environment:t,release:e._n(g.version),server_name:n.default.hostname(),contexts:{app:{app_name:`Codex`,app_version:g.version,...r==null?{}:{app_build:r}},os:{name:n.default.type(),version:n.default.release(),...x==null?{}:{kernel_version:x}},runtime:{name:S,version:C},device:{arch:process.arch,processor_count:y.length,memory_size:n.default.totalmem(),free_memory:n.default.freemem(),...b!=null&&b.length>0?{model:b}:{}},trace_recording:{duration_ms:u,trace_size_bytes:p,trace_gzip_size_bytes:m,trace_event_count:h,conversation_id:d?.conversationId??null,host_id:d?.hostId??null,window_id:d?.windowId??null},runtime_health:{load_average:n.default.loadavg(),uptime_seconds:n.default.uptime(),app_metrics:f?.appMetrics??[],renderer_memory:f?.rendererMemory??null,gpu_feature_status:f?.gpuFeatureStatus??{},gpu_vendor:f?.gpuVendor??null,gpu_renderer:f?.gpuRenderer??null,hardware_acceleration_enabled:f?.hardwareAccelerationEnabled??null}},tags:{trace_recording:`content-trace`,trace_filename:s,platform:process.platform,sessionId:e.o,preRelease:_,...d?.conversationId==null?{}:{conversation_id:d.conversationId},...d?.hostId==null?{}:{host_id:d.hostId},...d?.windowId==null?{}:{window_id:String(d.windowId)},...f?.gpuVendor==null?{}:{gpu_vendor:f.gpuVendor},...f==null?{}:{hardware_acceleration:f.hardwareAccelerationEnabled?`enabled`:`disabled`}},...r==null?{}:{dist:r},extra:{...l!=null&&l.length>0?{trace_recording_note:l}:{},trace_recording_duration_ms:u,trace_size_bytes:p,trace_gzip_size_bytes:m,trace_event_count:h}},T={type:`attachment`,length:c.byteLength,filename:s,content_type:Qc,attachment_type:`event.attachment`};return Buffer.concat([il({event_id:a,sent_at:v,dsn:e.gn}),il({type:`event`}),il(w),il(T),c,tl])}function il(e){return Buffer.from(`${JSON.stringify(e)}\n`,`utf8`)}function al(e){let t=new URL(e),n=t.pathname.split(`/`).filter(Boolean).at(-1);if(n==null||n.length===0)throw Error(`Sentry DSN is missing a project id`);return`${t.protocol}//${t.host}/api/${n}/envelope/`}function ol(e){try{let t=JSON.parse(e.toString(`utf8`));return Array.isArray(t.traceEvents)?t.traceEvents.length:null}catch{return null}}var sl=e.sn(`content-tracing`),cl=`Start Trace Recording`,ll=`Stop Trace Recording`,ul=`Waiting to Start Trace…`,dl=`Saving Trace…`,fl=`Waiting for Trace Details…`,pl=`Uploading Trace…`,ml=`toggle-trace-recording`,hl=process.env.CODEX_TRACE_SHORTCUT!=null,gl=[`*`,`disabled-by-default-v8.cpu_profiler`,`disabled-by-default-v8.cpu_profiler.hires`],J=`idle`,_l=null,vl=null;function yl({allowDevtools:i,windowManager:a,traceRecordingSentryUploadOptions:o}){let s=async e=>{if(!(J===`awaiting-start-confirmation`||J===`saving`||J===`awaiting-upload-details`||J===`uploading`))try{let i=!1;if(J===`idle`){if(Sl(a,e,`awaiting-start-confirmation`),!await Xs()){Sl(a,e,`idle`);return}await t.contentTracing.startRecording({included_categories:gl,recording_mode:`record-continuously`}),_l=Date.now(),Sl(a,e,`recording`),sl().info(`Started content trace recording.`);return}Sl(a,e,`saving`);let s=await t.contentTracing.stopRecording((0,r.join)((0,n.tmpdir)(),`codex-trace-${Date.now()}.json`)),c=_l==null?null:Date.now()-_l;if(sl().info(`Stopped content trace recording.`,{safe:{tracePath:s,recordingDurationMs:c},sensitive:{}}),o!=null)try{let n=t.BrowserWindow.getFocusedWindow(),r=n!=null&&!n.isDestroyed()?n:null,l=r?.id??null,u=r==null?null:a.getHostIdForWebContents(r.webContents),d=Jc(r);Sl(a,e,`awaiting-upload-details`);let[f,p]=await Promise.all([$s(),d]);Sl(a,e,`uploading`),await nl({...o,tracePath:s,recordingDurationMs:c,correlation:{conversationId:f.conversationId,hostId:u,windowId:l},runtimeHealth:p,traceRecordingNote:f.note.length>0?f.note:null}),sl().info(`Uploaded content trace recording to Sentry.`,{safe:{tracePath:s,sourceWindowId:l,sourceHostId:u,traceConversationId:f.conversationId},sensitive:{}}),i=!0}finally{await wl(s)}_l=null,Sl(a,e,`idle`),i&&a.sendMessageToAllRegisteredWindows({type:`trace-recording-uploaded`})}catch(t){let n=Tl();n===`saving`?Sl(a,e,`recording`):(n===`awaiting-start-confirmation`||n===`awaiting-upload-details`||n===`uploading`)&&(_l=null,Sl(a,e,`idle`)),sl().warning(`Failed to toggle content trace recording.`,{safe:{},sensitive:{error:t}})}};return vl=s,{id:ml,label:Cl(J),accelerator:i&&hl?e.Kn.toggleTraceRecording:void 0,click:async e=>{await s(e)}}}function bl(e){e.sendMessageToAllRegisteredWindows({type:`trace-recording-state-changed`,state:J})}async function xl(){let e=t.Menu.getApplicationMenu()?.getMenuItemById(ml);e==null||vl==null||await vl(e)}function Sl(e,t,n){J=n,t.label=Cl(n),bl(e)}function Cl(e){return e===`recording`?ll:e===`awaiting-start-confirmation`?ul:e===`saving`?dl:e===`awaiting-upload-details`?fl:e===`uploading`?pl:cl}async function wl(e){try{await(0,u.rm)(e,{force:!0})}catch(t){sl().warning(`Failed to remove temporary trace recording file.`,{safe:{tracePath:e},sensitive:{error:t}})}}function Tl(){return J}function El(e){try{let t=new URL(e);return t.protocol===`http:`||t.protocol===`https:`}catch{return!1}}var Y=e.sn(`electron-message-handler`),Dl=560,Ol=100,kl=`New project`,Al=5e3,jl={"thread-follower-start-turn":`thread-follower-start-turn-request`,"thread-follower-steer-turn":`thread-follower-steer-turn-request`,"thread-follower-interrupt-turn":`thread-follower-interrupt-turn-request`,"thread-follower-set-model-and-reasoning":`thread-follower-set-model-and-reasoning-request`,"thread-follower-set-collaboration-mode":`thread-follower-set-collaboration-mode-request`,"thread-follower-edit-last-user-turn":`thread-follower-edit-last-user-turn-request`,"thread-follower-command-approval-decision":`thread-follower-command-approval-decision-request`,"thread-follower-file-approval-decision":`thread-follower-file-approval-decision-request`,"thread-follower-submit-user-input":`thread-follower-submit-user-input-request`,"thread-follower-set-queued-follow-ups-state":`thread-follower-set-queued-follow-ups-state-request`},Ml=class{sharedObjectSubscribers=new Map;host;addSshHost;primaryWindowRestoreBounds=null;primaryWindowMode=null;datadogLogger;maxLogLevel;powerSaveBlockerId=null;powerSaveBlockingWebContentsIds=new Set;powerSaveTrackedWebContentsIds=new Set;pendingThreadFollowerStartTurnRequests=new Map;pendingThreadFollowerSteerTurnRequests=new Map;pendingThreadFollowerInterruptTurnRequests=new Map;pendingThreadFollowerSetModelAndReasoningRequests=new Map;pendingThreadFollowerSetCollaborationModeRequests=new Map;pendingThreadFollowerEditLastUserTurnRequests=new Map;pendingThreadFollowerCommandApprovalDecisionRequests=new Map;pendingThreadFollowerFileApprovalDecisionRequests=new Map;pendingThreadFollowerSubmitUserInputRequests=new Map;pendingThreadFollowerSetQueuedFollowUpsStateRequests=new Map;pendingThreadRoleRequests=new Map;samplerManager;constructor(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v,y,b,x){this.hostId=e,this.windowManager=n,this.filePreviewManager=r,this.threadOverlayManager=i,this.appServerConnectionRegistry=a,this.fetchWrapper=o,this.globalState=s,this.sharedObjectRepository=c,this.terminalManager=l,this.gitManager=u,this.desktopNotificationManager=d,this.errorReporter=f,this.desktopSentry=p,this.updateWorkerSentryUser=m,this.getIpcClientForWebContents=g,this.inboxManager=_,this.sparkleManager=b,this.host=t,this.addSshHost=h,this.sharedObjectRepository.addSubscriber((e,t)=>{this.sharedObjectSubscribers.get(e)?.forEach((n,r)=>{this.sendSharedObjectUpdate(r,e,t)})}),this.datadogLogger=v,this.maxLogLevel=y,this.samplerManager=new Kc(this.hostId,this.windowManager,this.desktopSentry,x,{getStdioIoStatsSnapshot:()=>this.getAppServerConnection(this.hostId).getStdioIoStatsSnapshot()})}getAppServerConnection(e){return this.appServerConnectionRegistry.getConnection(e)}async handleMessage(n,r){switch(r.type){case`export-logs`:{let i=t.BrowserWindow.fromWebContents(n);try{await Hc({parentWindow:i,appSessionId:e.o,scope:r.scope})}catch(e){Y().error(`Failed to export logs`,{safe:{},sensitive:{error:e}})}break}case`set-telemetry-user`:{this.updateWorkerSentryUser(r.authMethod,r.userId,r.email);let e=r.authMethod==null?null:{id:r.userId??void 0,authMethod:r.authMethod};this.datadogLogger.setUserInfo(e),this.desktopSentry.setUser(e);break}case`ready`:this.windowManager.markWebContentsReady(n),this.filePreviewManager.handleRendererReady(n.id),this.threadOverlayManager.handleRendererReady(n.id),this.terminalManager.track(n),this.sendCustomPrompts(n).catch(e=>{this.errorReporter.reportNonFatal(e,{kind:`send-custom-prompts`})}),this.sendPersistedAtomState(n),n.send(I,{type:`app-update-ready-changed`,isUpdateReady:this.sparkleManager.getIsUpdateReady()}),this.samplerManager.handleRendererReady(n),Y().info(`Handled 'ready' message, sent ide-context-updated`);break;case`open-in-main-window`:case`open-in-hotkey-window`:case`hotkey-window-collapse-to-home`:case`hotkey-window-dismiss`:case`hotkey-window-enabled-changed`:case`hotkey-window-transition-done`:case`hotkey-window-home-pointer-interaction-changed`:break;case`power-save-blocker-set`:this.updatePowerSaveBlocker(n,r.shouldBlock);break;case`electron-set-window-mode`:this.setPrimaryWindowMode(n,r.mode);break;case`electron-request-microphone-permission`:if(process.platform!==`darwin`)break;try{await t.systemPreferences.askForMediaAccess(`microphone`)}catch(e){Y().error(`Microphone permission request failed`,{safe:{},sensitive:{error:e}})}break;case`electron-add-ssh-host`:try{await this.addSshHost(r.host,r.openWindow)}catch(e){Y().error(`Failed to add SSH host`,{safe:{},sensitive:{error:e}})}break;case`inbox-item-set-read-state`:if(r.isRead){let e=Date.now();this.inboxManager?.markRead(r.id,e);break}this.inboxManager?.markUnread(r.id);break;case`inbox-items-create`:{if(!this.inboxManager)break;let e=Date.now(),t=r.conversationId||r.turnId||(0,c.randomUUID)(),n=r.items.map((n,i)=>({id:r.items.length>1&&!n.id?`${t}-${i+1}`:n.id||t,automationId:null,automationName:null,title:n.title??null,description:n.description??null,archivedAssistantMessage:null,archivedUserMessage:null,archivedReason:null,sourceCwd:null,threadId:r.conversationId??null,readAt:null,createdAt:e,status:null}));this.inboxManager.persistAndNotify(n);break}case`archive-thread`:this.getAppServerConnection(r.hostId).registerArchiveThread(r.conversationId,{cwd:r.cwd,cleanupWorktree:r.cleanupWorktree});break;case`unarchive-thread`:this.getAppServerConnection(r.hostId).registerUnarchiveThread(r.conversationId);break;case`thread-archived`:{let e=this.getIpcClientForWebContents(n);e&&await e.sendBroadcast(`thread-archived`,{hostId:r.hostId,conversationId:r.conversationId,cwd:r.cwd});break}case`thread-unarchived`:{let e=this.getIpcClientForWebContents(n);e&&await e.sendBroadcast(`thread-unarchived`,{hostId:r.hostId,conversationId:r.conversationId});break}case`thread-queued-followups-changed`:{let e=this.getIpcClientForWebContents(n);e&&await e.sendBroadcast(`thread-queued-followups-changed`,{conversationId:r.conversationId,messages:r.messages});break}case`thread-stream-state-changed`:{let e=this.getIpcClientForWebContents(n);e&&await e.sendBroadcast(`thread-stream-state-changed`,r);break}case`open-thread-overlay`:await this.threadOverlayManager.open(n,{conversationId:r.conversationId});break;case`thread-overlay-set-always-on-top`:this.threadOverlayManager.setAlwaysOnTop(n.id,r.shouldFloat);break;case`thread-follower-start-turn-response`:this.handleThreadFollowerStartTurnResponse(n,r);break;case`thread-follower-steer-turn-response`:this.handleThreadFollowerSteerTurnResponse(n,r);break;case`thread-follower-interrupt-turn-response`:this.handleThreadFollowerInterruptTurnResponse(n,r);break;case`thread-follower-set-model-and-reasoning-response`:this.handleThreadFollowerSetModelAndReasoningResponse(n,r);break;case`thread-follower-set-collaboration-mode-response`:this.handleThreadFollowerSetCollaborationModeResponse(n,r);break;case`thread-follower-edit-last-user-turn-response`:this.handleThreadFollowerEditLastUserTurnResponse(n,r);break;case`thread-follower-command-approval-decision-response`:this.handleThreadFollowerCommandApprovalDecisionResponse(n,r);break;case`thread-follower-file-approval-decision-response`:this.handleThreadFollowerFileApprovalDecisionResponse(n,r);break;case`thread-follower-submit-user-input-response`:this.handleThreadFollowerSubmitUserInputResponse(n,r);break;case`thread-follower-set-queued-follow-ups-state-response`:this.handleThreadFollowerSetQueuedFollowUpsStateResponse(n,r);break;case`thread-role-response`:this.handleThreadRoleResponse(n,r);break;case`persisted-atom-sync-request`:this.sendPersistedAtomState(n);break;case`persisted-atom-update`:this.updatePersistedAtomState(r.key,r.deleted?void 0:r.value,n);break;case`persisted-atom-reset`:this.resetPersistedAtomState(n);break;case`codex-app-server-restart`:await this.getAppServerConnection(r.hostId).restart(),Y().info(`Codex app-server restart requested`);break;case`install-app-update`:this.sparkleManager.installUpdatesIfAvailable();break;case`open-debug-window`:await this.windowManager.openDebugWindow();break;case`toggle-trace-recording`:await xl();break;case`log-message`:{let{level:t}=r;if(!e.w(t,this.maxLogLevel))break;Y().log(t,r.message,r.tags);break}case`electron-app-state-snapshot-trigger`:case`electron-app-state-snapshot-response`:await this.samplerManager.handleMessage(n,r);break;case`shared-object-subscribe`:{let e=this.sharedObjectSubscribers.get(r.key)??new Map;e.set(n,(e.get(n)??0)+1),this.sharedObjectSubscribers.set(r.key,e);let t=this.sharedObjectRepository.get(r.key);this.sendSharedObjectUpdate(n,r.key,t);break}case`shared-object-unsubscribe`:{let e=this.sharedObjectSubscribers.get(r.key);if(e==null)break;let t=(e.get(n)??0)-1;if(t>0){e.set(n,t);break}e.delete(n),e.size===0&&this.sharedObjectSubscribers.delete(r.key);break}case`shared-object-set`:this.sharedObjectRepository.set(r.key,r.value);break;case`mcp-request`:Y().debug(`app_server.bridge_received`,{safe:{messageType:`mcp-request`,requestId:String(r.request.id),method:r.request.method,originWebcontentsId:n.id,originHostId:r.hostId},sensitive:{}}),await this.getAppServerConnection(r.hostId).handleClientRequest(new Oc(n),r.request);break;case`mcp-notification`:Y().debug(`app_server.bridge_received`,{safe:{messageType:`mcp-notification`,method:r.request.method,originWebcontentsId:n.id,originHostId:r.hostId},sensitive:{}}),await this.getAppServerConnection(r.hostId).handleClientNotification(r.request);break;case`mcp-response`:Y().debug(`app_server.bridge_received`,{safe:{messageType:`mcp-response`,requestId:String(r.response.id),originWebcontentsId:n.id,originHostId:r.hostId},sensitive:{}}),await this.getAppServerConnection(r.hostId).handleClientResponse(r.response);break;case`fetch`:await this.fetchWrapper.handleRequest(n,r);break;case`cancel-fetch`:this.fetchWrapper.cancelRequest(r);break;case`fetch-stream`:this.fetchWrapper.handleStreamRequest(n,r);break;case`cancel-fetch-stream`:this.fetchWrapper.cancelStreamRequest(r);break;case`desktop-notification-show`:Y().info(`[desktop-notifications] forward show`,{safe:{notificationId:r.notification.id,kind:r.notification.kind},sensitive:{}}),this.desktopNotificationManager.showNotification(r.notification,n,e=>{n.isDestroyed()||(e.actionType===`open`&&this.focusNotificationOriginWindow(n),Y().info(`[desktop-notifications] emit action`,{safe:{notificationId:e.notificationId,actionType:e.actionType,actionId:e.actionId??`none`},sensitive:{}}),n.send(I,{type:`desktop-notification-action`,hostId:this.windowManager.getHostIdForWebContents?.(n)??this.hostId,notificationId:e.notificationId,actionId:e.actionId,actionType:e.actionType,conversationId:r.notification.conversationId??null,requestId:r.notification.requestId??null,reply:e.reply??null}))});break;case`desktop-notification-hide`:this.desktopNotificationManager.dismissByConversationId(r.conversationId??null);break;case`show-diff`:this.windowManager.sendMessageToWebContents(n,{type:`toggle-diff-panel`,open:!0});break;case`show-plan-summary`:break;case`update-diff-if-open`:break;case`electron-add-new-workspace-root-option`:await this.addWorkspaceRootOption(n,!0,r.root);break;case`electron-pick-workspace-root-option`:await this.addWorkspaceRootOption(n,!1);break;case`electron-onboarding-skip-workspace`:await this.skipOnboardingWorkspace(n,r.projectName);break;case`electron-onboarding-pick-workspace-or-create-default`:await this.pickOnboardingWorkspaceOrCreateDefault(n,r.defaultProjectName);break;case`electron-update-workspace-root-options`:{let e=si(r.roots,this.host);ac(this.globalState,e),sc(this.globalState,t=>{let n=ci(t,this.host);if(Object.keys(n).length===0)return;let r={};return e.forEach(e=>{n[e]!==void 0&&(r[e]=n[e])}),r});let t=W(this.globalState),i=t.filter(t=>e.includes(t));i.length===0&&e.length>0&&(i=[e[0]]),(t.length!==i.length||t.some((e,t)=>e!==i[t]))&&(K(this.globalState,i),n.send(I,{type:`active-workspace-roots-updated`})),n.send(I,{type:`workspace-root-options-updated`});break}case`electron-rename-workspace-root-option`:{let e=oi(r.root,this.host),t=r.label.trim();if(!si(G(this.globalState),this.host).includes(e))break;sc(this.globalState,n=>{let r=ci(n,this.host);return t?r[e]=t:delete r[e],r}),n.send(I,{type:`workspace-root-options-updated`});break}case`electron-set-active-workspace-root`:try{let e=oi(r.root,this.host),t=si(G(this.globalState),this.host),i=t.includes(e)?t:[e,...t];K(this.globalState,[e]),ac(this.globalState,i),sc(this.globalState,e=>ci(e,this.host)),n.send(I,{type:`active-workspace-roots-updated`})}catch(e){Y().error(`Failed to set active workspace root`,{safe:{},sensitive:{error:e}})}break;case`open-in-browser`:{let{url:e}=r;if(typeof e==`string`&&El(e))try{await t.shell.openExternal(e)}catch(e){Y().error(`Open-in-browser failed`,{safe:{},sensitive:{error:e}})}else Y().warning(`Open-in-browser received invalid url`);break}case`electron-set-badge-count`:t.app.setBadgeCount(r.count);break;case`view-focused`:break;case`electron-window-focus-request`:{let e=t.BrowserWindow.fromWebContents(n);this.windowManager.sendMessageToWebContents(n,{type:`electron-window-focus-changed`,isFocused:e?.isFocused()??!1});break}case`worker-request`:case`worker-request-cancel`:break;case`navigate-in-new-editor-tab`:case`open-vscode-command`:case`open-extension-settings`:case`open-keyboard-shortcuts`:case`open-config-toml`:case`show-settings`:case`install-wsl`:throw Error(`"${r.type}" is not implemented in Electron.`);case`terminal-create`:case`terminal-attach`:await this.terminalManager.createOrAttach(n,r);break;case`terminal-write`:this.terminalManager.write(n,r.sessionId,r.data);break;case`terminal-run-action`:this.terminalManager.runAction(n,r.sessionId,{cwd:r.cwd,command:r.command});break;case`terminal-resize`:this.terminalManager.resize(n,r.sessionId,r.cols,r.rows);break;case`terminal-close`:this.terminalManager.close(n,r.sessionId);break}}setPrimaryWindowMode(e,n){let r=t.BrowserWindow.fromWebContents(e);if(!r||r.isDestroyed())return;let i=this.windowManager.getPrimaryWindow(this.hostId);if(!(!i||i.isDestroyed()||i.id!==r.id)&&this.primaryWindowMode!==n){if(this.primaryWindowMode=n,n===`onboarding`){this.primaryWindowRestoreBounds||={bounds:r.getNormalBounds(),wasMaximized:r.isMaximized(),wasFullScreen:r.isFullScreen()},r.isFullScreen()&&r.setFullScreen(!1),r.isMaximized()&&r.unmaximize(),r.setResizable(!1),r.setMaximizable(!1),r.setFullScreenable(!1),r.setMinimumSize(Dl,Dl),r.setSize(Dl,Dl),r.center(),this.showPrimaryWindow(r);return}if(r.setResizable(!0),r.setMaximizable(!0),r.setFullScreenable(!0),this.primaryWindowRestoreBounds){let{bounds:e,wasMaximized:t,wasFullScreen:n}=this.primaryWindowRestoreBounds;this.primaryWindowRestoreBounds=null;let i=this.windowManager.getPrimaryMinimumSize(this.hostId),a={...e,width:Math.max(e.width,i.width),height:Math.max(e.height,i.height)};r.setBounds(a),t&&r.maximize(),n&&r.setFullScreen(!0)}this.windowManager.syncPrimaryMinimumSize(this.hostId),this.showPrimaryWindow(r)}}showPrimaryWindow(e){e.isVisible()||(e.show(),e.focus())}flushTelemetry(){this.datadogLogger.flushNow()}sendMessageToOrigin(e,t){this.windowManager.sendMessageToWebContents(e,t)}async getThreadRole(t,n){return new Promise((r,i)=>{let a=(0,c.randomUUID)(),o=setTimeout(()=>{this.pendingThreadRoleRequests.delete(a),i(Error(`thread-role-timeout`))},Al);this.pendingThreadRoleRequests.set(a,{originId:t.id,resolve:r,reject:i,timeout:o});let s={type:`thread-role-request`,hostId:this.windowManager.getHostIdForWebContents?.(t)??this.hostId,requestId:e.Dn(a),conversationId:n};this.sendMessageToOrigin(t,s)})}forwardThreadFollowerRequest(t,n,r,i){return new Promise((a,o)=>{let s=setTimeout(()=>{r.delete(n.requestId),o(Error(i))},Al);r.set(n.requestId,{originId:t.id,resolve:a,reject:o,timeout:s});let c={type:jl[n.method],hostId:this.windowManager.getHostIdForWebContents?.(t)??this.hostId,requestId:e.Dn(n.requestId),params:n.params};this.sendMessageToOrigin(t,c)})}async handleThreadFollowerStartTurnRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerStartTurnRequests,`thread-follower-start-turn-timeout`)}async handleThreadFollowerSteerTurnRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerSteerTurnRequests,`thread-follower-steer-turn-timeout`)}async handleThreadFollowerInterruptTurnRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerInterruptTurnRequests,`thread-follower-interrupt-turn-timeout`)}async handleThreadFollowerSetModelAndReasoningRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerSetModelAndReasoningRequests,`thread-follower-set-model-and-reasoning-timeout`)}async handleThreadFollowerSetCollaborationModeRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerSetCollaborationModeRequests,`thread-follower-set-collaboration-mode-timeout`)}async handleThreadFollowerEditLastUserTurnRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerEditLastUserTurnRequests,`thread-follower-edit-last-user-turn-timeout`)}async handleThreadFollowerCommandApprovalDecisionRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerCommandApprovalDecisionRequests,`thread-follower-command-approval-decision-timeout`)}async handleThreadFollowerFileApprovalDecisionRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerFileApprovalDecisionRequests,`thread-follower-file-approval-decision-timeout`)}async handleThreadFollowerSubmitUserInputRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerSubmitUserInputRequests,`thread-follower-submit-user-input-timeout`)}async handleThreadFollowerSetQueuedFollowUpsStateRequest(e,t){return this.forwardThreadFollowerRequest(e,t,this.pendingThreadFollowerSetQueuedFollowUpsStateRequests,`thread-follower-set-queued-follow-ups-state-timeout`)}rejectPendingThreadFollowerActionRequestsForOrigin(e){for(let[t,n]of this.pendingThreadFollowerStartTurnRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerStartTurnRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerSteerTurnRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerSteerTurnRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerInterruptTurnRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerInterruptTurnRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerSetModelAndReasoningRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerSetModelAndReasoningRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerSetCollaborationModeRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerSetCollaborationModeRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerEditLastUserTurnRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerEditLastUserTurnRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerCommandApprovalDecisionRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerCommandApprovalDecisionRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerFileApprovalDecisionRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerFileApprovalDecisionRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerSubmitUserInputRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerSubmitUserInputRequests.delete(t));for(let[t,n]of this.pendingThreadFollowerSetQueuedFollowUpsStateRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadFollowerSetQueuedFollowUpsStateRequests.delete(t));for(let[t,n]of this.pendingThreadRoleRequests.entries())n.originId===e.id&&(clearTimeout(n.timeout),n.reject(Error(`webcontents-destroyed`)),this.pendingThreadRoleRequests.delete(t))}handleThreadFollowerStartTurnResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerStartTurnRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower start-turn response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerStartTurnRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower start-turn response`));return}r.resolve(t.result)}handleThreadFollowerSteerTurnResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerSteerTurnRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower steer-turn response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerSteerTurnRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower steer-turn response`));return}r.resolve(t.result)}handleThreadFollowerInterruptTurnResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerInterruptTurnRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower interrupt-turn response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerInterruptTurnRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower interrupt-turn response`));return}r.resolve(t.result)}handleThreadFollowerSetModelAndReasoningResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerSetModelAndReasoningRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower set-model-and-reasoning response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerSetModelAndReasoningRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower set-model-and-reasoning response`));return}r.resolve(t.result)}handleThreadFollowerSetCollaborationModeResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerSetCollaborationModeRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower set-collaboration-mode response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerSetCollaborationModeRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower set-collaboration-mode response`));return}r.resolve(t.result)}handleThreadFollowerEditLastUserTurnResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerEditLastUserTurnRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower edit-last-user-turn response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerEditLastUserTurnRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower edit-last-user-turn response`));return}r.resolve(t.result)}handleThreadFollowerCommandApprovalDecisionResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerCommandApprovalDecisionRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower command-approval response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerCommandApprovalDecisionRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower command-approval response`));return}r.resolve(t.result)}handleThreadFollowerFileApprovalDecisionResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerFileApprovalDecisionRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower file-approval response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerFileApprovalDecisionRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower file-approval response`));return}r.resolve(t.result)}handleThreadFollowerSubmitUserInputResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerSubmitUserInputRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower submit-user-input response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerSubmitUserInputRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower submit-user-input response`));return}r.resolve(t.result)}handleThreadFollowerSetQueuedFollowUpsStateResponse(e,t){let n=String(t.requestId),r=this.pendingThreadFollowerSetQueuedFollowUpsStateRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread follower queued-follow-ups response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadFollowerSetQueuedFollowUpsStateRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}if(!t.result){r.reject(Error(`Missing thread follower queued-follow-ups response`));return}r.resolve(t.result)}handleThreadRoleResponse(e,t){let n=String(t.requestId),r=this.pendingThreadRoleRequests.get(n);if(!r||r.originId!==e.id){Y().warning(`Unknown thread role response requestId`,{safe:{requestId:n},sensitive:{}});return}if(this.pendingThreadRoleRequests.delete(n),clearTimeout(r.timeout),t.error){r.reject(Error(t.error));return}r.resolve(t.role)}sendPersistedAtomState(e){let t=this.globalState.get(`electron-persisted-atom-state`)??{};this.sendMessageToView(e,{type:`persisted-atom-sync`,state:t})}updatePersistedAtomState(e,t,n){let r={...this.globalState.get(`electron-persisted-atom-state`)??{}};t===void 0?delete r[e]:r[e]=t,this.globalState.set(`electron-persisted-atom-state`,r),this.broadcastPersistedAtomUpdate(e,t,n),e===`review-open`&&this.windowManager.syncPrimaryMinimumSize(this.hostId)}broadcastPersistedAtomUpdate(e,t,n){let r=t===void 0;this.windowManager.sendMessageToAllWindows(this.hostId,{type:`persisted-atom-updated`,key:e,value:r?null:t,deleted:r})}resetPersistedAtomState(e){this.globalState.set(`electron-persisted-atom-state`,{}),this.windowManager.sendMessageToAllWindows(this.hostId,{type:`persisted-atom-sync`,state:{}})}async skipOnboardingWorkspace(t,n){let r=null;try{r=await this.getSkipWorkspaceRoot(n??kl),await e.Kt.mkdir(r,{recursive:!0},this.host),await this.initializeSkipWorkspace(r),this.setOnboardingWorkspaceAndNavigate(t,r),t.send(I,{type:`electron-onboarding-skip-workspace-result`,success:!0,root:r})}catch(e){Y().error(`Failed to skip onboarding workspace selection`,{safe:{},sensitive:{error:e}});let n=e instanceof Error?e.message:String(e);t.send(I,{type:`electron-onboarding-skip-workspace-result`,success:!1,root:r??void 0,error:n})}}async pickOnboardingWorkspaceOrCreateDefault(t,n){let r=null,i=`picked`;try{r=await this.pickWorkspaceRootForOnboarding(),r??(i=`created_default`,r=await this.getSkipWorkspaceRoot(n),await e.Kt.mkdir(r,{recursive:!0},this.host),await this.initializeSkipWorkspace(r)),this.setOnboardingWorkspaceAndNavigate(t,r),t.send(I,{type:`electron-onboarding-pick-workspace-or-create-default-result`,success:!0,source:i,root:r})}catch(e){Y().error(`Failed onboarding workspace pick/create`,{safe:{},sensitive:{error:e}});let n=e instanceof Error?e.message:String(e);t.send(I,{type:`electron-onboarding-pick-workspace-or-create-default-result`,success:!1,source:i,root:r??void 0,error:n})}}setOnboardingWorkspaceAndNavigate(e,t){let n=si([t,...G(this.globalState)],this.host);ac(this.globalState,n),sc(this.globalState,e=>ci(e,this.host)),K(this.globalState,[oi(t,this.host)]),e.send(I,{type:`workspace-root-options-updated`}),e.send(I,{type:`active-workspace-roots-updated`}),e.send(I,{type:`navigate-to-route`,path:`/`,state:{focusComposerNonce:Date.now()}})}async pickWorkspaceRootForOnboarding(){if(this.host.id!==`local`)throw Error(`Onboarding workspace picker is only supported for local hosts`);let e=[`openDirectory`,`createDirectory`];await this.shouldShowHiddenFilesInPicker()&&e.push(`showHiddenFiles`);let n=await t.dialog.showOpenDialog({properties:e,title:`Select Project Root`}),r=n.filePaths?.[0];return n.canceled||!r?null:this.resolveWorkspaceRoot(r)}async getSkipWorkspaceRoot(n){if(e.In(this.host))throw Error(`Skipping onboarding workspace selection is only supported for local hosts`);let r=t.app.getPath(`documents`),i=this.sanitizeOnboardingProjectName(n);return this.findAvailableSkipWorkspaceRoot(r,i,void 0)}sanitizeOnboardingProjectName(t){let n=e.qt(this.host).basename(t).trim();return n.length===0||n===`.`||n===`..`?kl:n}async findAvailableSkipWorkspaceRoot(t,n,r){let i=e.qt(this.host),a=r==null?n:`${n} ${r}`,o=i.join(t,a);if(!await this.folderExists(o))return o;let s=r==null?2:r+1;return this.findAvailableSkipWorkspaceRoot(t,n,s)}async folderExists(t){try{return await e.Kt.stat(t,this.host),!0}catch{return!1}}async initializeSkipWorkspace(t){let n=await e.Z(t,[`init`],this.host);if(n.success){this.gitManager.invalidateStableMetadata();return}let r=n.stderr||n.stdout||`Failed to run git init`;throw Error(r)}async addWorkspaceRootOption(e,n=!0,r){if(this.host.id!==`local`){this.windowManager.sendMessageToWebContents(e,{type:`remote-workspace-root-requested`,mode:n?`add`:`pick`,host:this.host.display_name,hostId:this.host.id,setActive:n?!0:void 0});return}let i=null;if(r)i=await this.resolveWorkspaceRoot(r);else{let e=[`openDirectory`,`createDirectory`];await this.shouldShowHiddenFilesInPicker()&&e.push(`showHiddenFiles`);let n=await t.dialog.showOpenDialog({properties:e,title:`Select Project Root`}),r=n.filePaths?.[0];!n.canceled&&r&&(i=await this.resolveWorkspaceRoot(r))}if(i==null)return;if(!n){e.send(I,{type:`workspace-root-option-picked`,root:R(i,this.shouldUseWslPaths())});return}let a=si(G(this.globalState),this.host),o=a.includes(i)?a:[i,...a];o.length!==a.length&&(ac(this.globalState,o),e.send(I,{type:`workspace-root-options-updated`})),K(this.globalState,[i]),e.send(I,{type:`active-workspace-roots-updated`}),e.send(I,{type:`workspace-root-option-added`,root:R(i,this.shouldUseWslPaths())}),e.send(I,{type:`navigate-to-route`,path:`/`,state:{focusComposerNonce:Date.now()}})}async resolveWorkspaceRoot(e){try{let t=oi(e,this.host);return(await(0,u.stat)(t)).isDirectory()?(0,r.resolve)(t):null}catch(e){return Y().warning(`Failed to stat workspace root`,{safe:{},sensitive:{error:e}}),null}}async shouldShowHiddenFilesInPicker(){return await this.readHiddenFilesPreference()??!1}async readHiddenFilesPreference(){return process.platform===`darwin`?this.readMacHiddenFilesPreference():null}async readMacHiddenFilesPreference(){let e=await this.readCommandOutput([`defaults`,`read`,`com.apple.finder`,`AppleShowAllFiles`]);if(e==null)return null;let t=e.trim().toLowerCase();return t===`1`||t===`true`||t===`yes`?!0:t===`0`||t===`false`||t===`no`?!1:null}async readCommandOutput(t){let n=new AbortController,r=!1,i=setTimeout(()=>{r=!0,n.abort()},Ol);try{let{stdout:i,code:a}=await e.Xt({args:t,signal:n.signal}).wait();return r||a!==0?null:i}catch(e){return r||Y().warning(`Failed to read hidden files preference`,{safe:{},sensitive:{error:e}}),null}finally{clearTimeout(i)}}focusNotificationOriginWindow(e){let n=this.windowManager.getPrimaryWindow(this.hostId)??t.BrowserWindow.fromWebContents(e);!n||n.isDestroyed()||(n.isMinimized()&&n.restore(),n.show(),n.focus())}updatePowerSaveBlocker(e,t){let n=e.id;this.powerSaveTrackedWebContentsIds.has(n)||(this.powerSaveTrackedWebContentsIds.add(n),e.once(`destroyed`,()=>{this.powerSaveTrackedWebContentsIds.delete(n),this.powerSaveBlockingWebContentsIds.delete(n),this.syncPowerSaveBlocker()})),t?this.powerSaveBlockingWebContentsIds.add(n):this.powerSaveBlockingWebContentsIds.delete(n),this.syncPowerSaveBlocker()}syncPowerSaveBlocker(){let e=this.powerSaveBlockingWebContentsIds.size>0;if(e&&this.powerSaveBlockerId==null){this.powerSaveBlockerId=t.powerSaveBlocker.start(`prevent-app-suspension`);return}!e&&this.powerSaveBlockerId!=null&&(t.powerSaveBlocker.stop(this.powerSaveBlockerId),this.powerSaveBlockerId=null)}async sendCustomPrompts(t){let n=await e.b();this.sendMessageToView(t,{type:`custom-prompts-updated`,prompts:n})}sendMessageToView(e,t){e.isDestroyed()||e.send(I,t)}sendSharedObjectUpdate(e,t,n){e.isDestroyed()||this.sendMessageToView(e,{type:`shared-object-updated`,key:t,value:n})}shouldUseWslPaths(){return e.rn(this.host)}},Nl=e.sn(`electron-sampler`),Pl=class{constructor(e){this.options=e}collectSnapshotFields(e){let r=process.memoryUsage(),i={main_process_rss_bytes:r.rss,main_process_heap_total_bytes:r.heapTotal,main_process_heap_used_bytes:r.heapUsed,main_process_external_bytes:r.external,main_process_array_buffers_bytes:r.arrayBuffers,system_free_memory_bytes:(0,n.freemem)(),renderer_webcontents_id:e.id},a=e.getOSProcessId();try{let e=t.app.getAppMetrics().find(e=>e.pid===process.pid),n=t.app.getAppMetrics().find(e=>e.pid===a);e&&this.addMainProcessFields(i,e),n&&this.addRendererMemoryFields(i,n)}catch(e){Nl().warning(`Failed to collect renderer process memory snapshot`,{safe:{},sensitive:{error:e}})}let o=this.options.gitWorkerInvocationSampler.getSnapshot();return i.git_worker_invocations_total=o.totalInvocations,i.git_worker_invocations_last_30s=o.invocationsLast30s,i}addMainProcessFields(e,t){let n=t.cpu;n&&`percentCPUUsage`in n&&(e.main_process_cpu_percent=n.percentCPUUsage)}addRendererMemoryFields(e,t){let n=t.cpu;n&&`percentCPUUsage`in n&&(e.renderer_process_cpu_percent=n.percentCPUUsage);let r=t.memory;r&&(`workingSetSize`in r&&(e.renderer_process_working_set_kb=r.workingSetSize),`peakWorkingSetSize`in r&&(e.renderer_process_peak_working_set_kb=r.peakWorkingSetSize),`privateBytes`in r&&(e.renderer_process_private_bytes_kb=r.privateBytes),`sharedBytes`in r&&(e.renderer_process_shared_bytes_kb=r.sharedBytes))}},Fl=`remote-ssh-v0.toml`,Il=`(remote) `;const Ll=2e4;var Rl=`Codex Desktop`;const zl=9234;function Bl(e){return`ws://127.0.0.1:${e}/rpc`}function Vl(){return[`RUST_LOG=${e.un(process.env.RUST_LOG??`warn`)}`,`CODEX_INTERNAL_ORIGINATOR_OVERRIDE=${e.un(Rl)}`,Ye(`codex app-server`)].join(` `)}function Hl(e){let t=typeof e.host_id==`string`?e.host_id.trim():``,n=typeof e.display_name==`string`?e.display_name.trim():``,r=typeof e.local_port==`number`?e.local_port:null,i=typeof e.ssh_host==`string`?e.ssh_host.trim():``,a=typeof e.ssh_port==`number`?e.ssh_port:null,o=typeof e.identity==`string`&&e.identity.trim()||null,s=typeof e.remote_port==`number`?e.remote_port:zl;return!t||!n||r==null||!i?null:{hostId:t,displayName:n,localPort:r,sshHost:i,sshPort:a,identity:o,remotePort:s}}function Ul(){return r.default.join(e.Jt({hostConfig:null}),Fl)}function Wl(){let t=e.an(),n=Ul();if(!a.default.existsSync(n))return null;let r;try{r=a.default.readFileSync(n,`utf8`)}catch(e){return t.warning(`[ssh-websocket-v0] failed to read config`,{safe:{configPath:n},sensitive:{error:e}}),null}let i;try{i=e._t(r)}catch(e){return t.warning(`[ssh-websocket-v0] failed to parse TOML`,{safe:{configPath:n},sensitive:{error:e}}),null}return Hl(i)||(t.warning(`[ssh-websocket-v0] invalid config`,{safe:{configPath:n},sensitive:{}}),null)}function Gl(t){let n=Vl(),r=e.Fn({sshAlias:null,sshHost:t.sshHost,sshPort:t.sshPort,identity:t.identity});return Kl({id:t.hostId,displayName:`${Il}${t.displayName}`,localPort:t.localPort,sshAlias:null,sshHost:t.sshHost,sshPort:t.sshPort,identity:t.identity,remotePort:t.remotePort,codexCliCommand:[`ssh`,`-o`,`BatchMode=yes`,`-o`,`ConnectTimeout=10`,...r,n],terminalCommand:[`ssh`,...r],defaultWorkspaces:[]})}function Kl(e){let t={id:e.id,display_name:e.displayName,kind:`ssh`,codex_cli_command:e.codexCliCommand,terminal_command:e.terminalCommand,default_workspaces:e.defaultWorkspaces??[],[rr]:{sshAlias:e.sshAlias??null,sshHost:e.sshHost,sshPort:e.sshPort,identity:e.identity,remotePort:e.remotePort??9234}};return e.localPort!=null&&(t.websocket_url=Bl(e.localPort)),e.homeDir&&(t.home_dir=e.homeDir),t}function ql(t,n){let r=e.Fn(t);return Kl({id:t.hostId,displayName:`${Il}${t.displayName}`,localPort:n,sshAlias:t.sshAlias,sshHost:t.sshHost,sshPort:t.sshPort,identity:t.identity,remotePort:zl,codexCliCommand:[],terminalCommand:[`ssh`,...r],defaultWorkspaces:[]})}function Jl(){let e=Wl();return e?Gl(e):null}function Yl(e){let t=``;for(let n=0;n=`@`&&t<=`~`)break;r+=1}if(r>=e.length){t+=e.slice(n);break}if(e[r]===`n`){n=r+1;continue}t+=e.slice(n,r+1),n=r+1;continue}if(r===`]`){let r=n+2,i=0,a=-1;for(;r{this.trackedOrigins.delete(e.id),this.cleanupForOrigin(e.id)}))}async createOrAttach(e,t){let n=this.getExistingSessionId(t);return n?this.attach(e,n,{nextSessionId:t.sessionId,conversationId:t.conversationId,hostId:t.hostId,cwd:t.cwd,forceCwdSync:t.forceCwdSync,cols:t.cols,rows:t.rows}):this.create(e,t)}write(e,t,n){let r=this.sessions.get(t);if(!r){this.sendError(e,t,`Session missing`);return}if(r.ownerId!==e.id){this.sendError(e,t,`Session owned by another window`);return}this.performPtyAction({origin:e,session:r,sessionId:t,action:()=>{r.pty.write(n)}})}runAction(e,t,n){let r=this.sessions.get(t);if(!r){this.sendError(e,t,`Session missing`);return}if(r.ownerId!==e.id){this.sendError(e,t,`Session owned by another window`);return}r.cwd=n.cwd,this.performPtyAction({origin:e,session:r,sessionId:t,action:()=>{r.pty.write(eu(r.shellKind,this.resolveShellCwd(r.shellKind,n.cwd),n.command))}})}resize(e,t,n,r){let i=this.sessions.get(t);if(!i){this.sendError(e,t,`Session missing`);return}if(i.ownerId!==e.id){this.sendError(e,t,`Session owned by another window`);return}this.performPtyAction({origin:e,session:i,sessionId:t,action:()=>{i.pty.resize(n,r)}})}close(e,t){let n=this.sessions.get(t);if(n){if(n.ownerId!==e.id){this.sendError(e,t,`Session owned by another window`);return}this.destroySession(n,{code:null,signal:null})}}appendOutput(e,t){let n=this.sessions.get(e);n&&(n.buffer=`${n.buffer}${t}`.slice(-Xl),n.attached&&this.send(n.owner,{type:`terminal-data`,sessionId:n.id,data:t}))}closeSession(e){let t=this.sessions.get(e);t&&this.destroySession(t,{code:null,signal:null})}getSnapshotForConversationId(e){let t=this.sessionsByConversation.get(e);if(!t)return null;let n=this.sessions.get(t);return n?{cwd:n.cwd,shell:n.shell,buffer:n.buffer,truncated:n.buffer.length>=Xl}:null}associateConversation(e,t){let n=this.sessions.get(e);n&&(n.conversationId=t,this.sessionsByConversation.set(String(t),e))}async create(e,t){let n=t.sessionId??(0,c.randomUUID)(),r=this.resolveRequestedCwd(t.cwd),i=this.resolveLocalCwd(r),a=this.resolveTerminalCommand(t.hostId),o=$l(a),[s,...l]=a,u=this.buildTerminalEnv();try{let{spawn:c}=await import(`node-pty`),d=c(s,l,{cols:t.cols??80,rows:t.rows??24,cwd:i,env:u}),f=Ls(a),p={id:n,pty:d,owner:e,ownerId:e.id,buffer:``,cwd:r,shell:f,shellKind:o,attached:!1,conversationId:t.conversationId,preserveOnOwnerDestroy:t.preserveOnOwnerDestroy??!1};return this.sessions.set(n,p),t.conversationId&&this.sessionsByConversation.set(String(t.conversationId),n),this.bindSessionEvents(p),r!==i&&i===process.cwd()&&this.seedShellCwd(p,r),this.flushInit(p),this.sendAttached(p),n}catch(t){return this.sendError(e,n,String(t)),null}}async attach(e,t,n){let r=this.sessions.get(t);if(!r)return this.sendError(e,t,`Session missing`),null;let i=r.owner.isDestroyed(),a=n.conversationId&&r.conversationId&&String(n.conversationId)===String(r.conversationId);if(i||a)r.owner=e,r.ownerId=e.id;else if(r.ownerId!==e.id)return this.sendError(e,t,`Session owned by another window`),null;if(n.conversationId&&(r.conversationId=n.conversationId,this.sessionsByConversation.set(String(n.conversationId),r.id)),n.cols&&n.rows&&r.pty.resize(n.cols,n.rows),n.forceCwdSync&&n.cwd){let e=this.resolveRequestedCwd(n.cwd);this.resolveLocalCwd(e)!==process.cwd()&&(r.cwd=e,this.seedShellCwd(r,e))}return n.nextSessionId&&n.nextSessionId!==r.id&&this.rekeySession(r,n.nextSessionId),n.preserveOnOwnerDestroy!=null&&(r.preserveOnOwnerDestroy=n.preserveOnOwnerDestroy),this.flushInit(r),this.sendAttached(r),r.id}bindSessionEvents(e){e.pty.onData(t=>{e.buffer=`${e.buffer}${t}`.slice(-Xl),e.attached&&this.send(e.owner,{type:`terminal-data`,sessionId:e.id,data:t})}),e.pty.onExit(({exitCode:t,signal:n})=>{this.destroySession(e,{code:t,signal:n??null})})}destroySession(e,t){if(this.sessions.get(e.id)!==e)return;this.sessions.delete(e.id),e.conversationId&&this.sessionsByConversation.delete(String(e.conversationId));try{e.pty.kill()}catch(t){e.owner.isDestroyed()||this.sendError(e.owner,e.id,String(t))}let n={type:`terminal-exit`,sessionId:e.id,code:t?.code??null,signal:t?.signal??null};e.owner.isDestroyed()||this.send(e.owner,n)}performPtyAction(e){try{e.action()}catch(t){this.sendError(e.origin,e.sessionId,String(t)),this.destroySession(e.session,{code:null,signal:null})}}flushInit(e){if(e.buffer.length>0){let t=Yl(e.buffer);this.send(e.owner,{type:`terminal-init-log`,sessionId:e.id,log:t})}e.attached=!0}sendAttached(e){this.send(e.owner,{type:`terminal-attached`,sessionId:e.id,cwd:e.cwd,shell:e.shell})}send(e,t){this.windowManager.sendMessageToWebContents(e,t)}sendError(e,t,n){this.windowManager.sendMessageToWebContents(e,{type:`terminal-error`,sessionId:t,message:n})}resolveRequestedCwd(e){if(e)return e;let t=W(this.globalState);return t[0]?t[0]:process.cwd()}resolveLocalCwd(e){let t=$r(e);return(0,a.existsSync)(t)?t:(0,a.existsSync)(e)?e:process.cwd()}resolveShellCwd(e,t){if(e===`wsl`)return R(t,!0);if(e===`posix`)return t;let n=$r(t);return(0,a.existsSync)(n)?n:t}seedShellCwd(t,n){t.pty.write(`cd ${e.un(n)}\n`)}getExistingSessionId(e){if(e.sessionId&&this.sessions.has(e.sessionId))return e.sessionId;if(e.conversationId){let t=this.sessionsByConversation.get(String(e.conversationId));if(t)return t}}cleanupForOrigin(e){for(let t of Array.from(this.sessions.values()))if(t.ownerId===e){if(t.conversationId||t.preserveOnOwnerDestroy){t.attached=!1;continue}this.destroySession(t,{code:null,signal:null})}}rekeySession(e,t){this.sessions.get(e.id)===e&&(this.sessions.delete(e.id),e.id=t,this.sessions.set(t,e),e.conversationId&&this.sessionsByConversation.set(String(e.conversationId),t))}resolveTerminalCommand(t){let n=null;return t!=null&&(n=this.getHostConfigForHostId?.(t)?.terminal_command??null),n&&n.length>0?n:this.terminalCommand&&this.terminalCommand.length>0?this.terminalCommand:Is(this.globalState.get(e.$n.INTEGRATED_TERMINAL_SHELL))}buildTerminalEnv(){let e={...process.env};return process.platform===`win32`?e:(e.TERM=Zl,delete e.TERMINFO,delete e.TERMINFO_DIRS,e)}};function $l(e){if(process.platform!==`win32`)return`posix`;let t=ou(e);return t===`wsl`||t===`wsl.exe`?`wsl`:t===`pwsh`||t===`pwsh.exe`||t===`powershell`||t===`powershell.exe`?`powershell`:t===`cmd`||t===`cmd.exe`?`commandPrompt`:`posix`}function eu(e,t,n){if(e===`powershell`){let r=n.replace(/\r\n|\r|\n/g,`\r -`);return`${ru(e,t)}; ${r}\r -`}if(e===`commandPrompt`){let r=n.replace(/\r\n|\r|\n/g,`\r -`);return`${ru(e,t)} && ${r}\r -`}return e===`wsl`?nu(t,n,` -`):tu(t,n)}function tu(e,t){return nu(e,t,process.platform===`win32`?`\r -`:` -`)}function nu(e,t,n){let r=t.replace(/\r\n|\r|\n/g,n);return`cd ${JSON.stringify(e)} && ${r}${n}`}function ru(e,t){return e===`powershell`?`Set-Location -LiteralPath ${iu(t)}`:e===`commandPrompt`?`cd /d ${au(t)}`:`cd ${JSON.stringify(t)}`}function iu(e){return`'${e.replace(/'/g,`''`)}'`}function au(e){return`"${e.replace(/"/g,`""`)}"`}function ou(e){let t=(e[0]??``).split(/[/\\]/);return t[t.length-1]?.toLowerCase()??``}var su=e.on(`WindowContext`),cu=class{hostId;host;appServerConnectionRegistry;messageHandler;sharedObjectRepository;fetchHandler;errorReporter;options;scopedState;disposables=new e.at;registeredWindows=new Map;remoteConnectionsByHostId=new Map;ipcClientsByWebContentsId=new Map;constructor(t){this.options=t,this.host=t.host,this.hostId=t.host.id,this.errorReporter=t.errorReporter,this.scopedState=t.globalState,this.sharedObjectRepository=new e.g,this.sharedObjectRepository.set(`host_config`,t.host),this.sharedObjectRepository.set(`remote_connections`,[]);let n=this.createAppServerConnection(this.hostId);this.appServerConnectionRegistry=new Zn,this.appServerConnectionRegistry.addConnection(this.hostId,n);let r=new Ql(t.windowManager,this.scopedState,t.host.terminal_command??null,e=>this.getHostConfigForHostId(e));this.fetchHandler=new vc(this.scopedState,e=>this.getRegisteredWindowIpcClient(e),this.appServerConnectionRegistry,t.gitManager,this.host,e=>this.getHostConfigForHostId(e),()=>this.refreshRemoteConnectionsFromSshConfig(),e=>this.saveCodexManagedRemoteConnections(e),(e,t)=>this.setRemoteConnectionAutoConnect(e,t),t.hotkeyWindowHotkeyController,t.windowManager,r);let i=new Dc(this.fetchHandler,{desktopOriginator:t.desktopOriginator,devApiBaseUrl:t.devApiBaseUrl,messageChannel:t.messageChannel,appServerConnectionRegistry:this.appServerConnectionRegistry,repoRoot:t.repoRoot,prodApiBaseUrl:t.prodApiBaseUrl,desktopSentry:t.desktopSentry}),a=new Pl({gitWorkerInvocationSampler:t.gitWorkerInvocationSampler});this.messageHandler=new Ml(this.hostId,t.host,t.windowManager,t.filePreviewManager,t.threadOverlayManager,this.appServerConnectionRegistry,i,this.scopedState,this.sharedObjectRepository,r,t.gitManager,t.desktopNotificationManager,t.errorReporter,t.desktopSentry,t.updateWorkerSentryUser,t.addSshHost,e=>this.getRegisteredWindowIpcClient(e),t.inboxManager,t.datadogLogger,t.maxLogLevel,t.sparkleManager,a),this.disposables.add(this.sharedObjectRepository.addSubscriber(e=>{e===`remote_connections`&&this.reconcileRemoteConnections(this.sharedObjectRepository.get(`remote_connections`)??[])})),this.refreshRemoteConnectionsFromSshConfig().catch(e=>{su.warning(`failed to refresh remote SSH connections`,{safe:{},sensitive:{error:e}})})}registerWindow(e){let{webContents:t}=e,n=new Oc(t);this.registeredWindows.set(t.id,n),t.once(`destroyed`,()=>{this.registeredWindows.delete(t.id)});for(let e of this.appServerConnectionRegistry.getAllHostIds())this.appServerConnectionRegistry.getConnection(e).registerWebviewWindow(n);this.registerIpcClientForWebContents(e.webContents)}handleMessage(e,t){return this.messageHandler.handleMessage(e,t)}flushTelemetry(){this.messageHandler.flushTelemetry()}dispose(){this.disposables.dispose();for(let e of this.appServerConnectionRegistry.getAllHostIds())this.appServerConnectionRegistry.getMaybeConnection(e)?.dispose(),this.appServerConnectionRegistry.removeConnection(e);this.registeredWindows.clear(),this.remoteConnectionsByHostId.clear();for(let{dispose:e}of this.ipcClientsByWebContentsId.values())e();this.ipcClientsByWebContentsId.clear()}getMessageHandler(){return this.messageHandler}getSharedObject(e){return this.sharedObjectRepository.get(e)}getHostConfigForHostId(e){if(e===this.hostId)return this.options.host;let t=this.remoteConnectionsByHostId.get(e);if(!t)throw Error(`Host config for host ID ${e} not found`);return ql(t,Ll)}createAppServerConnection(e,t=this.options.host,n=!1){let r=this.ensureSshWebsocketLoopbackUrl(t);return new Qn(this.options.messageChannel,{repoRoot:this.options.repoRoot,errorReporter:this.options.errorReporter,desktopSentry:this.options.desktopSentry,sharedObjectRepository:this.sharedObjectRepository,hostId:e,hostConfig:t,threadOverlayManager:this.options.threadOverlayManager,gitManager:this.options.gitManager,requestGitWorker:this.options.requestGitWorker,transport:lu({hostConfig:r,repoRoot:this.options.repoRoot,resourcesPath:process.resourcesPath}),hostProcess:this.options.hostProcess,ensureAuth:n})}reconcileRemoteConnections(e){let t=new Map;for(let n of e)n.hostId!==this.hostId&&t.set(n.hostId,n);for(let e of t.values()){let t=e.hostId,n=this.remoteConnectionsByHostId.get(t),r=this.appServerConnectionRegistry.getMaybeConnection(t);if(!e.autoConnect){r!=null&&this.disposeRemoteConnection(t);continue}if(!(n&&(0,Rn.default)(n,e)&&r!=null)){r!=null&&this.disposeRemoteConnection(t);try{this.appServerConnectionRegistry.addConnection(t,this.createAndRegisterRemoteConnection(e))}catch(e){su.warning(`failed to create remote connection during reconcile`,{safe:{hostId:t},sensitive:{error:e}})}}}for(let e of this.remoteConnectionsByHostId.keys())t.has(e)||this.disposeRemoteConnection(e);this.remoteConnectionsByHostId.clear();for(let[e,n]of t)this.remoteConnectionsByHostId.set(e,n)}createAndRegisterRemoteConnection(e){let t=ql(e,Ll),n=this.createAppServerConnection(e.hostId,t,!0);for(let e of this.registeredWindows.values())n.registerWebviewWindow(e);return n}disposeRemoteConnection(e){let t=this.appServerConnectionRegistry.getMaybeConnection(e);t&&(t.dispose(),this.appServerConnectionRegistry.removeConnection(e))}ensureSshWebsocketLoopbackUrl(e){return e.websocket_url!=null||e.ssh_websocket_v0==null?e:{...e,websocket_url:`ws://127.0.0.1:${Ll}/rpc`}}async refreshRemoteConnectionsFromSshConfig(){let e=await this.buildRemoteConnectionsFromDefaultSshConfig();return this.sharedObjectRepository.set(`remote_connections`,e),e}async saveCodexManagedRemoteConnections(t){return this.scopedState.set(e.Ln.REMOTE_CONNECTION_AUTO_CONNECT_BY_HOST_ID,this.getNextRemoteConnectionAutoConnectByHostId(t)),Ar(t),this.refreshRemoteConnectionsFromSshConfig()}async setRemoteConnectionAutoConnect(t,n){let r=this.getRemoteConnectionAutoConnectByHostId();return this.scopedState.set(e.Ln.REMOTE_CONNECTION_AUTO_CONNECT_BY_HOST_ID,{...r,[t]:n}),this.refreshRemoteConnectionsFromSshConfig()}async buildRemoteConnectionsFromDefaultSshConfig(){let e=kr(),t=await Vr({excludedSshAliases:new Set(e.flatMap(e=>e.sshAlias==null?[]:[e.sshAlias]))});return this.applyAutoConnectPreferencesToRemoteConnections([...e,...t])}applyAutoConnectPreferencesToRemoteConnections(e){let t=this.getRemoteConnectionAutoConnectByHostId();return e.map(e=>({...e,autoConnect:t[e.hostId]??e.autoConnect}))}getRemoteConnectionAutoConnectByHostId(){return this.scopedState.get(e.Ln.REMOTE_CONNECTION_AUTO_CONNECT_BY_HOST_ID)??{}}getNextRemoteConnectionAutoConnectByHostId(e){let t={...this.getRemoteConnectionAutoConnectByHostId()};for(let n of e)t[n.hostId]=n.autoConnect;return t}registerIpcClientForWebContents(t){if(this.appServerConnectionRegistry.getConnection(this.hostId).getTransportKind()!==`stdio`||this.ipcClientsByWebContentsId.has(t.id))return;let n=new e.E(`desktop`,this.errorReporter),r=new e.at;r.add(n.addAnyBroadcastHandler(async e=>{let n={...e,type:`ipc-broadcast`};this.messageHandler.sendMessageToOrigin(t,n)}));let i=async e=>await this.messageHandler.getThreadRole(t,e.conversationId)===`owner`;r.add(n.addRequestHandler(`thread-follower-start-turn`,i,async e=>this.messageHandler.handleThreadFollowerStartTurnRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-steer-turn`,i,async e=>this.messageHandler.handleThreadFollowerSteerTurnRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-interrupt-turn`,i,async e=>this.messageHandler.handleThreadFollowerInterruptTurnRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-set-model-and-reasoning`,i,async e=>this.messageHandler.handleThreadFollowerSetModelAndReasoningRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-set-collaboration-mode`,i,async e=>this.messageHandler.handleThreadFollowerSetCollaborationModeRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-edit-last-user-turn`,i,async e=>this.messageHandler.handleThreadFollowerEditLastUserTurnRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-command-approval-decision`,i,async e=>this.messageHandler.handleThreadFollowerCommandApprovalDecisionRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-file-approval-decision`,i,async e=>this.messageHandler.handleThreadFollowerFileApprovalDecisionRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-submit-user-input`,i,async e=>this.messageHandler.handleThreadFollowerSubmitUserInputRequest(t,e))),r.add(n.addRequestHandler(`thread-follower-set-queued-follow-ups-state`,i,async e=>this.messageHandler.handleThreadFollowerSetQueuedFollowUpsStateRequest(t,e))),r.add(()=>n.dispose()),this.ipcClientsByWebContentsId.set(t.id,{client:n,dispose:()=>{r.dispose(),this.messageHandler.rejectPendingThreadFollowerActionRequestsForOrigin(t)}}),t.once(`destroyed`,()=>{this.disposeIpcClientForWebContents(t)})}disposeIpcClientForWebContents(e){let t=this.ipcClientsByWebContentsId.get(e.id);t&&(this.ipcClientsByWebContentsId.delete(e.id),t.dispose())}getRegisteredWindowIpcClient(e){return this.ipcClientsByWebContentsId.get(e.id)?.client??null}};function lu(t){let n=mr(t.hostConfig);if(n)return su.info(`[ssh-websocket-v0] selected app-server transport`,{safe:{hostId:t.hostConfig.id,websocketUrl:n.websocketUrl}}),new pr(n);let r=e.jt(t.hostConfig);return r?new e.At({hostConfig:t.hostConfig,websocketUrl:r}):new e.Nt({hostConfig:t.hostConfig,repoRoot:t.repoRoot,resourcesPath:t.resourcesPath,defaultOriginator:t.defaultOriginator})}var uu=e.on(`worktree-cleanup-service`),du=7200*60*1e3;async function fu(e,t){let n=Array.from(new Set(t)),r=await Promise.all(n.map(async t=>{try{let n=await e.readThread(t,{includeTurns:!0});if(n==null)return[t,null];let r=Number(n.updatedAt)*1e3,i=n.turns[n.turns.length-1]??null;return[t,{updatedAtMs:Number.isFinite(r)?r:null,isInProgress:i?.status===`inProgress`}]}catch(e){uu.error(`Failed to get thread metadata with turns`,{safe:{threadId:t},sensitive:{error:e}})}try{let n=await e.readThread(t,{includeTurns:!1});if(n==null)return[t,null];let r=Number(n.updatedAt)*1e3;return[t,{updatedAtMs:Number.isFinite(r)?r:null,isInProgress:!1}]}catch(e){return uu.error(`Failed to get thread metadata without turns`,{safe:{threadId:t},sensitive:{error:e}}),[t,{updatedAtMs:Date.now(),isInProgress:!0}]}}));return Object.fromEntries(r)}function pu(t){let n=t.get(e.Ln.PINNED_THREAD_IDS);return Array.isArray(n)?n.filter(e=>typeof e==`string`):[]}function mu(t){let n=t.get(e.Ln.WORKTREE_KEEP_COUNT);return typeof n!=`number`||!Number.isFinite(n)?15:Math.max(1,Math.trunc(n))}function hu(t){return t.get(e.Ln.WORKTREE_AUTO_CLEANUP_ENABLED)!==!1}function gu(e,t=Date.now()){let n=e.get(b),r;return typeof n==`number`&&Number.isFinite(n)&&n>0?r=n:(r=t,e.set(b,r)),t{i.sendMessageToAllWindows(g,{type:e})}}),w=new Map,T=new Map,E={id:g,display_name:_,kind:`local`};w.set(E.id,E);let D=null,O=null,k=async(e,t)=>{let r=et(n,e);if(!r)return;let i=tt(r);w.set(i.id,i),D?.(),!(t===!1||!O)&&await O(i.id)};return{localHost:E,getHostConfig:e=>w.get(e)??null,upsertHostConfigs:e=>{for(let t of e)w.set(t.id,t)},getOrCreateContext:t=>{let n=T.get(t.id);if(n)return n;let g=r(t.id);cc(g,t.default_workspaces);let _=new cu({hostProcess:x,host:t,windowManager:i.windowManager,globalState:g,repoRoot:a,errorReporter:o,desktopSentry:s,desktopNotificationManager:S,updateWorkerSentryUser:y,addSshHost:k,gitManager:m,inboxManager:t.id===`local`?C:null,datadogLogger:c,maxLogLevel:l,sparkleManager:u,gitWorkerInvocationSampler:v,desktopOriginator:Le,devApiBaseUrl:`http://localhost:8000/api`,prodApiBaseUrl:`https://chatgpt.com/backend-api`,messageChannel:I,filePreviewManager:d,threadOverlayManager:f,hotkeyWindowHotkeyController:p,requestGitWorker:h});return _.appServerConnectionRegistry.getConnection(_.hostId).registerInternalNotificationHandler(e=>{e.method===`turn/completed`&&b.emit(`turnComplete`)}),T.set(t.id,_),i.registerContext(t.id,_),Pe({appServerConnection:_.appServerConnectionRegistry.getConnection(_.hostId),globalState:g,hostId:t.id}).catch(n=>{e.an().warning(`App thread title backfill failed`,{safe:{},sensitive:{hostId:t.id,error:n}})}),_},getElectronMessageHandlerForWindow:e=>{let t=i.getContextForWebContents(e.webContents);return t?t.getMessageHandler():null},getWindowContextForHost:e=>T.get(e)??null,getWorktreeCleanupInputsForHost:async({hostKey:e,threadIds:t})=>{let n=T.get(e),i=r(e);return{threadMetadataById:n?await fu(n.appServerConnectionRegistry.getConnection(n.hostId),t):Object.fromEntries(t.map(e=>[e,{updatedAtMs:Date.now(),isInProgress:!0}])),pinnedThreadIds:pu(i),protectPreMigrationOwnerlessWorktrees:gu(i),autoCleanupEnabled:hu(i),keepCount:mu(i)}},flushAndDisposeContexts:()=>{for(let e of T.values())e.dispose(),e.flushTelemetry()},setHostActions:e=>{D=e.refreshApplicationMenu,O=e.ensureHostWindow}}}var vu=16;function yu(e){let{width:t,height:n}=e.getSize();return!t||!n||t<=vu&&n<=vu?e:e.resize({width:vu,height:vu,quality:`best`})}function bu(e,n){if(e)try{if(e.startsWith(`data:`))return yu(t.nativeImage.createFromDataURL(e));if(e.startsWith(`file://`))return yu(t.nativeImage.createFromPath((0,d.fileURLToPath)(e)));if(e.startsWith(`/`))return yu(t.nativeImage.createFromPath(e));for(let r of n){let n=new URL(e,`file://${r.endsWith(`/`)?r:`${r}/`}`).pathname;if((0,a.existsSync)(n)){let e=t.nativeImage.createFromPath(n);if(!e.isEmpty())return yu(e)}}}catch{}}function xu(e,n){return t.ipcMain.handle(Ie,async(r,i)=>{if(!n(r))return{id:null};let a=t.BrowserWindow.fromWebContents(r.sender);return new Promise(n=>{let r=!1,o=e=>{r||(r=!0,n({id:e}))},s=t=>t.map(t=>t.type===`separator`?{type:`separator`}:{id:t.id,label:t.label,role:t.role,enabled:t.enabled??!0,icon:bu(t.icon,e),click:()=>o(t.id),submenu:t.submenu?s(t.submenu):void 0});t.Menu.buildFromTemplate(s(i)).popup({window:a??void 0,callback:()=>o(null)})})}),()=>t.ipcMain.removeHandler(Ie)}function Su(){let e=!1,t=null;return{allowQuitTemporarilyForUpdateInstall:()=>{e=!0,t&&clearTimeout(t),t=setTimeout(()=>{e=!1,t=null},6e4),t.unref()},canQuitWithoutPrompt:()=>e,markQuitApproved:()=>{e=!0}}}function Cu({desktopSentry:n,hotkeyWindowLifecycleManager:r,codexHome:i,nativeContextMenuIconSearchRoots:a,getContextForWebContents:o,ensureHostWindow:s,navigateToRoute:c,isTrustedIpcEvent:l}){xu(a,l),t.ipcMain.handle(`codex_desktop:message-from-view`,async(t,n)=>{if(!l(t))return;if(n.type===`open-in-main-window`){if(!e.wn(n.path))return;r.hide();let t=await s(`local`);t&&(t.isMinimized()&&t.restore(),t.show(),t.focus(),c(t,n.path));return}if(await r.handleMessage(t,n))return;let i=o(t.sender);if(!i){e.an().warning(`Message received for unknown window context`);return}await i.handleMessage(t.sender,n)}),t.ipcMain.handle(`codex_desktop:get-fast-mode-rollout-metrics`,async(t,n)=>l(t)?e.et({codexHome:i,params:n}):null),t.ipcMain.handle(`codex_desktop:trigger-sentry-test`,e=>{if(!l(e))return;let t=Error(`Desktop Sentry test error (debug button)`);n.captureException(t,{tags:{trigger:`debug-button`}})})}function wu({buildFlavor:n}){t.ipcMain.on(`codex_desktop:get-sentry-init-options`,t=>{t.returnValue=e.i}),t.ipcMain.on(`codex_desktop:get-build-flavor`,e=>{e.returnValue=n})}function Tu({isWindows:e,disableQuitConfirmationPrompt:n,quitState:r,windows:i,applicationMenuManager:a,ensureHostWindow:o,hotkeyWindowLifecycleManager:s,globalStatesByHostId:c,flushAndDisposeContexts:l,disposables:u,appEvent:d,errorReporter:f}){t.app.on(`window-all-closed`,()=>{process.platform!==`darwin`&&t.app.quit()}),t.app.on(`before-quit`,a=>{if(e||r.canQuitWithoutPrompt()||n){i.markAppQuitting();return}let o=t.app.getName();if(t.dialog.showMessageBoxSync({type:`warning`,buttons:[`Quit`,`Cancel`],defaultId:0,cancelId:1,noLink:!0,title:`Quit ${o}?`,message:`Quit ${o}?`,detail:`Any local threads running on this machine will be interrupted and scheduled automations won't run`})!==0){a.preventDefault();return}r.markQuitApproved(),i.markAppQuitting()}),t.app.on(`child-process-gone`,(e,t)=>{t.reason!==`clean-exit`&&f.reportFatal(Error(`Child process gone (${t.type})`),{tags:{errorType:`child-process-gone`,processType:t.type,reason:t.reason},extra:{exitCode:t.exitCode,name:t.name,serviceName:t.serviceName}})}),t.app.on(`activate`,()=>{i.showPrimaryWindow(`local`)||o(`local`),a.refresh()}),t.app.on(`browser-window-blur`,()=>{t.BrowserWindow.getFocusedWindow()??d.emit(`background`)}),t.app.on(`browser-window-focus`,()=>{d.emit(`foreground`),a.refresh({triggerProviderRefresh:!0})}),t.app.on(`will-quit`,()=>{s.dispose();for(let e of c.values())e.flush();l(),u.dispose()})}var Eu=e.sn(`sparkle`),Du=`/`,Ou=`/settings`;function ku({isMacOS:n,allowDevtools:r,allowDebugMenu:i,allowWindowReload:a,enableSparkle:o,sparkleManager:s,ensurePrimaryWindowVisible:c,queueCodexDeepLinkUrl:l,navigateToRoute:u,getElectronMessageHandlerForWindow:d,windowManager:f,hostsMenuState:p,selectHost:m,traceRecordingSentryUploadOptions:h}){let g=async()=>{let e=t.BrowserWindow.getFocusedWindow();return e&&!e.isDestroyed()?e:c()},_={label:`Settings…`,accelerator:e.Kn.settings,click:async()=>{let e=await g();e&&u(e,Ou)}},v={label:`New Thread`,accelerator:e.Kn.newThread,click:async()=>{let e=await g();e&&u(e,Du)}},y={label:`New Thread`,accelerator:e.Kn.newThreadAlt,acceleratorWorksWhenHidden:!0,visible:!1,click:v.click},b={label:`Open Folder…`,accelerator:e.Kn.openFolder,click:async()=>{let e=await g();if(!e)return;let t=d(e);t&&await t.addWorkspaceRootOption(e.webContents)}},x={label:`Log Out`,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`log-out`})}},S=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`open-command-menu`})},C=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`copy-conversation-path`})},w=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`toggle-thread-pin`})},T=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`rename-thread`})},E=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`archive-thread`})},D=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`copy-working-directory`})},O=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`copy-session-id`})},k=async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`copy-deeplink`})},A={label:`Command Menu…`,accelerator:e.Kn.openCommandMenu,acceleratorWorksWhenHidden:!0,visible:!1,click:S},j={label:`Command Menu…`,accelerator:e.Kn.openCommandMenuAlt,acceleratorWorksWhenHidden:!0,visible:!1,click:S},M={label:`Copy conversation path`,accelerator:e.Kn.copyConversationPath,acceleratorWorksWhenHidden:!0,visible:!1,click:C},N={label:`Pin/unpin thread`,accelerator:e.Kn.toggleThreadPin,acceleratorWorksWhenHidden:!0,visible:!1,click:w},P={label:`Rename thread`,accelerator:e.Kn.renameThread,acceleratorWorksWhenHidden:!0,visible:!1,click:T},ee={label:`Archive thread`,accelerator:e.Kn.archiveThread,acceleratorWorksWhenHidden:!0,visible:!1,click:E},te={label:`Copy working directory`,accelerator:e.Kn.copyWorkingDirectory,acceleratorWorksWhenHidden:!0,visible:!1,click:D},ne={label:`Copy session id`,accelerator:e.Kn.copySessionId,acceleratorWorksWhenHidden:!0,visible:!1,click:O},re={label:`Copy deeplink`,accelerator:e.Kn.copyDeeplink,acceleratorWorksWhenHidden:!0,visible:!1,click:k},ie=n?{role:`about`}:{label:`About ${t.app.getName()}`,click:()=>{let e=t.app.getName();if(typeof t.app.showAboutPanel==`function`){t.app.showAboutPanel();return}t.dialog.showMessageBox({title:`About ${e}`,message:e,detail:`Version ${t.app.getVersion()}\n© OpenAI`,type:`info`})}},ae={label:`Toggle Sidebar`,accelerator:e.Kn.toggleSidebar,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`toggle-sidebar`})}},oe={label:`Toggle Terminal`,accelerator:e.Kn.toggleTerminal,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`toggle-terminal`})}},se={label:`Reload Window`,accelerator:`Alt+Command+R`,click:async()=>{let e=await g();e&&e.reload()}},ce={label:`Toggle Diff Panel`,accelerator:e.Kn.toggleDiffPanel,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`toggle-diff-panel`})}},le=yl({allowDevtools:r,windowManager:f,traceRecordingSentryUploadOptions:h}),ue={label:`Find`,accelerator:e.Kn.findInThread,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`find-in-thread`})}},de={label:`Previous Thread`,accelerator:e.Kn.previousThread,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`previous-thread`})}},fe={label:`Next Thread`,accelerator:e.Kn.nextThread,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`next-thread`})}},pe={label:`Open Debug Window`,accelerator:`Alt+D`,click:async()=>{await f.openDebugWindow()}},me={label:`Open Deeplink from Clipboard`,click:()=>{l(t.clipboard.readText().trim())||t.dialog.showMessageBox({type:`info`,title:`Invalid Deeplink`,message:`Clipboard does not contain a valid codex:// deeplink.`,detail:`Copy a codex:// URL to the clipboard and try again.`})}},he={label:`Toggle Query Devtools`,accelerator:`CmdOrCtrl+Alt+Y`,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`toggle-query-devtools`})}},ge={label:`Back`,accelerator:e.Kn.navigateBack,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`navigate-back`})}},_e={label:`Forward`,accelerator:e.Kn.navigateForward,click:async()=>{let e=await g();e&&f.sendMessageToWindow(e,{type:`navigate-forward`})}},ve={label:`Check for Updates…`,enabled:!0,click:()=>{Eu().info(`Check for updates requested via menu.`);let e=s.getUpdater();if(!e){let e=s.getDiagnostics(),n=e.reason??e.addonError??`unknown`;Eu().warning(`Sparkle updater unavailable; init likely skipped.`,{safe:{diagnostics:e},sensitive:{}}),t.dialog.showMessageBox({type:`info`,title:`Updates Unavailable`,message:`Automatic updates are unavailable right now.`,detail:`Sparkle initialization skipped: ${n}`});return}e.checkForUpdates()}},ye=Au(p,m,e=>f.getPrimaryWindow(e)!=null),be=[ie,{type:`separator`},_];ye&&be.push(ye),o&&be.push(ve,{type:`separator`}),be.push({role:`services`},{type:`separator`},{role:`hide`},{role:`hideOthers`},{role:`unhide`},{type:`separator`},x,{role:`quit`});let xe={role:`fileMenu`,id:e.Gn.file},Se={role:`appMenu`,submenu:be},Ce={label:`View`,id:e.Gn.view,submenu:[]},we=[A,j,...n?[M]:[],N,P,ee,te,ne,re,{type:`separator`},ae,oe,ce,ue,{type:`separator`},de,fe,ge,_e,...a?[se]:[]],Te=[...i?[pe,me]:[],...r?[{role:`toggleDevTools`}]:[],...r?[he]:[]];Te.length>0&&we.push({type:`separator`},...Te);let Ee=n?[]:[{role:`zoomIn`,accelerator:`Ctrl+=`,acceleratorWorksWhenHidden:!0,visible:!1}];we.push({type:`separator`},{role:`zoomIn`},...Ee,{role:`zoomOut`},{role:`resetZoom`},{type:`separator`},{role:`togglefullscreen`}),Ce.submenu=we;let De=[...n?[Se,xe]:[xe],{role:`editMenu`,id:e.Gn.edit},Ce,{role:`windowMenu`,id:e.Gn.window},{role:`help`,id:e.Gn.help,submenu:[{label:`Codex Documentation`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/app`)}},{label:`What's new`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/changelog`)}},{label:`Automations`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/app/automations`)}},{label:`Local Environments`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/app/local-environments`)}},{label:`Worktrees`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/app/worktrees`)}},{label:`Skills`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/skills`)}},{label:`Model Context Protocol`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/mcp`)}},{label:`Troubleshooting`,click:()=>{t.shell.openExternal(`https://developers.openai.com/codex/app/troubleshooting`)}},{type:`separator`},le,{type:`separator`},{label:`Keyboard Shortcuts`,click:S}]}],Oe=t.Menu.buildFromTemplate(De),F=Oe.getMenuItemById(e.Gn.file)?.submenu;if(F&&(F.insert(0,new t.MenuItem(v)),F.insert(1,new t.MenuItem(y)),F.insert(2,new t.MenuItem(b)),F.insert(3,new t.MenuItem({type:`separator`})),!n)){F.append(new t.MenuItem(_)),F.append(new t.MenuItem({type:`separator`})),F.append(new t.MenuItem(ie));let e=F.items.findIndex(e=>e.role===`quit`);e>=0?F.insert(e,new t.MenuItem(x)):(F.append(new t.MenuItem(x)),F.append(new t.MenuItem({role:`quit`})))}t.Menu.setApplicationMenu(Oe),bl(f)}function Au(e,t,n){if(e.status===`disabled`)return null;let r=[{label:_,type:`checkbox`,checked:n(g),click:async()=>{await t(g)}}];if(e.status===`ready`){for(let i of e.hosts)r.push({label:i.display_name,type:`checkbox`,checked:n(i.id),click:async()=>{await t(i.id)}});e.providerFailed&&e.hosts.length===0&&!e.isLoading&&r.push({label:`Failed to load more`,enabled:!1}),e.isLoading&&r.push({label:`Loading hosts…`,enabled:!1})}return{label:`Hosts`,submenu:r}}var ju=e.ir(e.dn()),Mu=24,Nu=e.sn(`host-provider`),Pu=[`brix`,`codex`,`provider`],Fu=r.default.join(n.default.homedir(),`.virtualenvs/openai/bin/codex-box`),Iu=`applied`,Lu=`applied_devbox`,Ru=1e4,X=e.Qn().transform(e=>e.trim()).pipe(e.Qn().min(1)),zu=e.Zn({name:X,path:X}),Bu=e.Yn(zu),Vu=e.Zn({id:X,display_name:X.optional(),kind:X.optional(),host:X.optional(),ssh_args:e.Yn(e.Qn()).optional(),devcontainer_auto_discover:e.Xn().optional(),repo_root:X.optional(),home_dir:X.optional(),default_workspaces:Bu.optional()}),Hu=e.Zn({boxes:e.Yn(Vu)}),Uu=e.Zn({id:X,display_name:X}),Wu=e.Zn({boxes:e.Yn(Uu)});function Gu(e){try{return a.default.readFileSync(e,`utf8`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return null;throw e}}function Ku(){return r.default.join(e.Jt({hostConfig:null}),`boxes.toml`)}function qu(e){let{host:t,sshArgs:n=[],repoRoot:r}=e;return[Fu,`app-server`,`--kind`,`ssh`,`--ssh-host`,t,...n.flatMap(e=>[`--ssh-arg`,e]),...r?[`--repo-root`,r]:[]]}function Ju(t){let n=Hu.parse(e._t(t)),r=[];for(let e of n.boxes){let t=e.id,n=e.kind,i=e.devcontainer_auto_discover??n===`ssh-devcontainer`,a=n??(i?`ssh-devcontainer`:`ssh`);if(i||a!==`ssh`){Nu().debug(`Skipping unsupported boxes.toml entry in electron host provider`,{safe:{boxId:t,kind:a,devcontainerAutoDiscover:i},sensitive:{}});continue}let o=e.host;if(!o)continue;let s=e.ssh_args??[],c=e.repo_root,l=e.home_dir,u={id:t,display_name:e.display_name??t,kind:`ssh`,codex_cli_command:qu({host:o,sshArgs:s,repoRoot:c}),terminal_command:[`ssh`,...s,o],default_workspaces:e.default_workspaces??[]};l&&(u.home_dir=l),r.push(u)}return r}function Yu(){let e=Gu(Ku());return e==null?[]:Ju(e)}function Xu(e){let t=Wu.parse(JSON.parse(e)),n=[];for(let e of t.boxes){let t={...e,id:e.id,display_name:e.display_name};n.push(t)}return n}async function Zu(t,n){let r=new AbortController,i=!1,a=n==null?null:setTimeout(()=>{i=!0,r.abort()},n);try{let{stdout:n,stderr:a,code:o}=await e.Xt({args:t,signal:r.signal}).wait();if(i||o!==0)throw Error(`Host provider exited with code ${o}: ${a.trim()}`);return a.trim().length>0&&Nu().debug(`Host provider emitted stderr`,{safe:{stderr:a.trim()}}),n}finally{a!=null&&clearTimeout(a)}}function Qu(e){let t=c.default.createHash(`sha1`).update(e,`utf8`).digest(),n=Number(t.readBigUInt64BE(0)%BigInt(Mu));return Math.trunc(n*360/Mu)}function $u(e,t,n){return`oklch(${e.toFixed(2)} ${t.toFixed(2)} ${n}deg)`}function ed(e){if(Object.prototype.hasOwnProperty.call(e,`accent_colors`))return e;let t=Qu(e.id);return{...e,accent_colors:{light:{background:$u(.9,.07,t)},dark:{background:$u(.35,.09,t)}}}}function td(e){let t=new Map;for(let n of e)for(let e of n)e.id.length!==0&&t.set(e.id,e);let n=[...t.values()].map(ed);return n.sort((e,t)=>e.id.localeCompare(t.id)),n}async function nd(){let e=[],t=[],n=null,r=null;try{e=Yu()}catch(e){n=e;let t=Ku();Nu().warning(`Failed to load boxes.toml hosts`,{safe:{boxesPath:t,error:e}})}let[i,a]=await Promise.allSettled([Zu([...Pu]).then(Xu),cd()]);if(i.status===`fulfilled`)t=i.value;else{let e=i.reason;e instanceof Error&&`code`in e&&e.code===`ENOENT`?Nu().warning(`Brix host provider unavailable from PATH; skipping brix discovery`,{safe:{error:e}}):(r=e,Nu().warning(`Brix host provider failed`,{safe:{error:e}}))}let o=a.status===`fulfilled`?a.value:[],s=td([t,e,o]);return s.length>0?{status:`success`,hosts:s}:n==null?r==null?{status:`success`,hosts:s}:{status:`failed`,error:r instanceof Error?r.message:`Host provider failed`}:{status:`failed`,error:n instanceof Error?n.message:typeof n==`string`?n:`Unknown error`}}var rd=[`BatchMode=yes`,`ConnectTimeout=10`,`ConnectionAttempts=1`,`ServerAliveInterval=15`,`ServerAliveCountMax=4`];function id(t){let n=t.trim();if(n.length===0)return[];let r=e.Yn(X).parse(JSON.parse(n)),i=new Set,a=[];for(let e of r)i.has(e)||(i.add(e),a.push(e));return a}function ad(){let e=[];for(let t of rd)e.push(`-o`,t);return e}function od(e){return[`ssh`,...ad(),e]}function sd(e){return e.map(e=>Kl({id:`ssh:${e}`,displayName:`(applied) ${e}`,sshHost:e,sshPort:null,identity:null,remotePort:zl,codexCliCommand:Ze({host:e,sshArgs:ad(),includeDefaultOptions:!1}),terminalCommand:od(e),defaultWorkspaces:[{name:`openai`,path:`/home/dev-user/code/openai`}],homeDir:`/home/dev-user`}))}async function cd(){try{let e;try{e=await Zu([Lu,`ls`,`--infer-from-local-ssh`,`--format`,`json`],Ru)}catch(t){if(!(t instanceof Error&&`code`in t&&t.code===`ENOENT`))throw t;e=await Zu([Iu,`devbox`,`ls`,`--infer-from-local-ssh`,`--format`,`json`],Ru)}return sd(id(e))}catch(e){return Nu().warning(`Applied local devbox discovery threw in Codex App`,{safe:{error:e}}),[]}}function ld(e){let t=new Map;for(let n of e)t.set(n.id,n);return[...t.values()]}var ud=class{logger=e.an();buildFlavor;globalState;upsertHostConfigs;buildMenu;isHostProviderEnabled;runHostProvider;coalescedProviderRefresh;providerHosts=null;providerFailed=!1;providerRefreshDisabled=!1;currentProviderRefresh=null;constructor({buildFlavor:t,globalState:n,upsertHostConfigs:r,buildMenu:i,runHostProvider:a=nd}){this.buildFlavor=t,this.globalState=n,this.upsertHostConfigs=r,this.buildMenu=i,this.runHostProvider=a,this.isHostProviderEnabled=e.c.isInternal(t),this.coalescedProviderRefresh=(0,ju.default)(async()=>this.runHostProvider(),{promise:!0})}async refresh(e){if(e?.triggerProviderRefresh){this.triggerProviderRefresh();return}this.renderMenu()}computeHostsMenuState(){let e=nt(this.globalState),t=Jl(),n=this.providerHosts??[],r=this.isHostProviderEnabled?ld([...e,...t?[t]:[],...n]):ld([...e,...t?[t]:[]]);if(this.upsertHostConfigs(r),!this.isHostProviderEnabled&&r.length===0)return{status:`disabled`};let i=this.providerFailed&&r.length===0;return{status:`ready`,hosts:r,isLoading:this.currentProviderRefresh!=null,providerFailed:i}}renderMenu(){this.buildMenu(this.computeHostsMenuState())}triggerProviderRefresh(){if(!this.isHostProviderEnabled){this.renderMenu();return}if(this.providerRefreshDisabled){this.renderMenu();return}let e=this.coalescedProviderRefresh();if(e===this.currentProviderRefresh){this.renderMenu();return}this.providerFailed=!1,this.currentProviderRefresh=e,this.renderMenu(),this.handleProviderRefresh(e)}async handleProviderRefresh(e){try{let t=await e;if(this.logger.debug(`Host provider result`,{safe:t,sensitive:{}}),t.status===`success`){this.providerHosts=t.hosts,this.providerFailed=!1;return}this.providerFailed=!0,this.providerHosts??(this.providerRefreshDisabled=!0),this.logger.warning(`Host provider failed`,{safe:{error:t.error}})}catch(e){this.providerFailed=!0,this.providerHosts??(this.providerRefreshDisabled=!0),this.logger.warning(`Host provider threw`,{safe:{error:e}})}finally{this.coalescedProviderRefresh.clear(),this.currentProviderRefresh=null,this.renderMenu()}}};async function dd({window:e,route:t,globalState:n,gitManager:r,hostConfig:i,windowManager:a,navigateToRoute:o}){switch(t.kind){case`settings`:o(e,`/settings`);return;case`skills`:o(e,`/skills`);return;case`automations`:o(e,`/inbox?automationMode=create`);return;case`connectorOAuthCallback`:a.sendMessageToWindow(e,{type:`connector-oauth-callback`,fullRedirectUrl:t.fullRedirectUrl,returnTo:t.returnTo??void 0});return;case`newThread`:{let s=await fd({route:t,globalState:n,gitManager:r,hostConfig:i});s!=null&&vd({globalState:n,hostId:i.id,workspaceRoot:s,windowManager:a}),o(e,`/`,{focusComposerNonce:Date.now(),prefillPrompt:t.prompt,prefillCwd:s??void 0});return}case`localConversation`:o(e,`/local/${t.conversationId}`);return}}async function fd({route:e,globalState:t,gitManager:n,hostConfig:r}){if(e.path!=null){let t=await pd(e.path);if(t!=null)return t}return e.originUrl==null?null:md({targetOriginUrl:e.originUrl,globalState:t,gitManager:n,hostConfig:r})}async function pd(e){let t=(0,r.resolve)(e);try{return(await(0,u.stat)(t)).isDirectory()?t:null}catch{return null}}async function md({targetOriginUrl:t,globalState:n,gitManager:r,hostConfig:i}){let a=hd(n);return a.length===0?null:gd({targetOriginUrl:t,workspaceRoots:a,origins:await e.R(a,r,i)})}function hd(e){let t=G(e);return t.length>0?t:W(e)}function gd({targetOriginUrl:t,workspaceRoots:n,origins:r}){let i=e.An(t),a=new Map;for(let e of r)e.originUrl!=null&&(a.has(e.dir)||a.set(e.dir,e.originUrl),a.has(e.root)||a.set(e.root,e.originUrl));for(let e of n){let n=a.get(e);if(n!=null&&_d({targetOriginUrl:t,candidateOriginUrl:n,targetRepo:i}))return e}return null}function _d({targetOriginUrl:t,candidateOriginUrl:n,targetRepo:r}){if(n===t)return!0;if(r==null)return!1;let i=e.An(n);return i==null?!1:i.host===r.host&&i.owner===r.owner&&i.repo===r.repo}function vd({globalState:e,hostId:t,workspaceRoot:n,windowManager:r}){let i=G(e);i.includes(n)||(ac(e,[n,...i]),r.sendMessageToAllWindows(t,{type:`workspace-root-options-updated`}));let a=W(e);a.length===1&&a[0]===n||(K(e,[n]),r.sendMessageToAllWindows(t,{type:`active-workspace-roots-updated`}))}function yd({buildFlavor:n,globalState:r,errorReporter:i,sparkleManager:a,gitManager:o,localHost:s,upsertHostConfigs:c,windowServices:l,ensureHostWindow:u,getElectronMessageHandlerForWindow:d,allowDevtools:f,allowDebugMenu:p,enableSparkle:m,isMacOS:h,appVersion:g}){let _=(e,t,n)=>{l.sendMessageToWindow(e,{type:`navigate-to-route`,path:t,state:n})},v=async()=>u(s.id),y=e.t({app:t.app,isMacOS:h,ensurePrimaryWindowVisible:v,navigateToRoute:(e,t)=>dd({window:e,route:t,globalState:r,gitManager:o,hostConfig:s,windowManager:l.windowManager,navigateToRoute:_}),initialArgv:process.argv,errorReporter:i}),b=new ud({buildFlavor:n,globalState:r,upsertHostConfigs:c,buildMenu:t=>{ku({isMacOS:h,allowDevtools:f,allowDebugMenu:p,allowWindowReload:n!==e.c.Prod,enableSparkle:m,sparkleManager:a,ensurePrimaryWindowVisible:v,queueCodexDeepLinkUrl:y.queueCodexDeepLinkUrl,navigateToRoute:_,getElectronMessageHandlerForWindow:d,windowManager:l.windowManager,hostsMenuState:t,selectHost:u,traceRecordingSentryUploadOptions:{appVersion:g,buildFlavor:n,buildNumber:e.a.value}})}});return{applicationMenuManager:b,deepLinks:y,navigateToRoute:_,refreshApplicationMenu:async e=>{await b.refresh(e)}}}var bd=`.codex-global-state.json`;function xd({moduleDir:t}){e.kt();let n=e.Jt({hostConfig:null}),i=(0,r.join)(t,`preload.js`),a=(0,r.join)(t,`..`,`..`),o=(0,r.join)(a,`..`),s=(0,r.join)(n,bd),c=new A(s,{hostId:g}),l=process.platform===`win32`&&process.env.WSL_DISTRO_NAME==null&&c.get(e.$n.RUN_CODEX_IN_WSL)===!0&&e.$t()!=null;e.tn(()=>l);let u=new Map;return u.set(g,c),Ms(c.get(e.$n.APPEARANCE_THEME)??`system`),{codexHome:n,preloadPath:i,desktopRoot:a,repoRoot:o,globalState:c,globalStatesByHostId:u,getGlobalStateForHost:e=>{let t=u.get(e);if(t)return t;let n=new A(s,{hostId:e});return u.set(e,n),n}}}var Sd=new Set([`INIT_CWD`,`npm_command`,`npm_execpath`,`npm_node_execpath`,`PNPM_PACKAGE_NAME`,`PNPM_SCRIPT_SRC_DIR`]),Cd=[`npm_config_`,`npm_lifecycle_`,`npm_package_`];function wd(e){for(let t of Object.keys(e)){let n=t.toLowerCase();(Sd.has(t)||Cd.some(e=>n.startsWith(e)))&&delete e[t]}}var Td=5e3,Ed=1500;async function Dd(){t.app.isPackaged||wd(process.env);let n=new AbortController,r=null,i=await Promise.race([e.It({interactive:!0,extraEnv:{[e.Lt]:`1`},signal:n.signal}).then(e=>({status:`loaded`,userEnv:e})).catch(e=>({status:`failed`,error:e})),new Promise(e=>{r=setTimeout(()=>{n.abort(),e({status:`timed_out`})},Td),r.unref()})]).finally(()=>{r!=null&&clearTimeout(r)});if(i.status===`loaded`){Object.assign(process.env,i.userEnv),t.app.isPackaged||wd(process.env);return}let a=i.status===`timed_out`?`Timed out after ${Td}ms.`:i.error instanceof Error?i.error.message:String(i.error);e.an().warning(`Failed to load shell env`,{safe:{status:i.status},sensitive:{detail:a}}),await new Promise(e=>{setTimeout(e,Ed).unref()})}function Od(e){return Number.isInteger(e)?e>=4352&&(e<=4447||e===9001||e===9002||11904<=e&&e<=12871&&e!==12351||12880<=e&&e<=19903||19968<=e&&e<=42182||43360<=e&&e<=43388||44032<=e&&e<=55203||63744<=e&&e<=64255||65040<=e&&e<=65049||65072<=e&&e<=65131||65281<=e&&e<=65376||65504<=e&&e<=65510||110592<=e&&e<=110593||127488<=e&&e<=127569||131072<=e&&e<=262141):!1}var kd=10,Ad=(e=0)=>t=>`\u001B[${t+e}m`,jd=(e=0)=>t=>`\u001B[${38+e};5;${t}m`,Md=(e=0)=>(t,n,r)=>`\u001B[${38+e};2;${t};${n};${r}m`,Z={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};Object.keys(Z.modifier);const Nd=Object.keys(Z.color),Pd=Object.keys(Z.bgColor);[...Nd,...Pd];function Fd(){let e=new Map;for(let[t,n]of Object.entries(Z)){for(let[t,r]of Object.entries(n))Z[t]={open:`\u001B[${r[0]}m`,close:`\u001B[${r[1]}m`},n[t]=Z[t],e.set(r[0],r[1]);Object.defineProperty(Z,t,{value:n,enumerable:!1})}return Object.defineProperty(Z,`codes`,{value:e,enumerable:!1}),Z.color.close=`\x1B[39m`,Z.bgColor.close=`\x1B[49m`,Z.color.ansi=Ad(),Z.color.ansi256=jd(),Z.color.ansi16m=Md(),Z.bgColor.ansi=Ad(kd),Z.bgColor.ansi256=jd(kd),Z.bgColor.ansi16m=Md(kd),Object.defineProperties(Z,{rgbToAnsi256:{value(e,t,n){return e===t&&t===n?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(t/255*5)+Math.round(n/255*5)},enumerable:!1},hexToRgb:{value(e){let t=/[a-f\d]{6}|[a-f\d]{3}/i.exec(e.toString(16));if(!t)return[0,0,0];let[n]=t;n.length===3&&(n=[...n].map(e=>e+e).join(``));let r=Number.parseInt(n,16);return[r>>16&255,r>>8&255,r&255]},enumerable:!1},hexToAnsi256:{value:e=>Z.rgbToAnsi256(...Z.hexToRgb(e)),enumerable:!1},ansi256ToAnsi:{value(e){if(e<8)return 30+e;if(e<16)return 90+(e-8);let t,n,r;if(e>=232)t=((e-232)*10+8)/255,n=t,r=t;else{e-=16;let i=e%36;t=Math.floor(e/36)/5,n=Math.floor(i/6)/5,r=i%6/5}let i=Math.max(t,n,r)*2;if(i===0)return 30;let a=30+(Math.round(r)<<2|Math.round(n)<<1|Math.round(t));return i===2&&(a+=60),a},enumerable:!1},rgbToAnsi:{value:(e,t,n)=>Z.ansi256ToAnsi(Z.rgbToAnsi256(e,t,n)),enumerable:!1},hexToAnsi:{value:e=>Z.ansi256ToAnsi(Z.hexToAnsi256(e)),enumerable:!1}}),Z}var Id=Fd(),Ld=/^[\uD800-\uDBFF][\uDC00-\uDFFF]$/,Rd=[`\x1B`,`›`],zd=e=>`${Rd[0]}[${e}m`,Bd=(e,t,n)=>{let r=[];e=[...e];for(let n of e){let i=n;n.includes(`;`)&&(n=n.split(`;`)[0][0]+`0`);let a=Id.codes.get(Number.parseInt(n,10));if(a){let n=e.indexOf(a.toString());n===-1?r.push(zd(t?a:i)):e.splice(n,1)}else if(t){r.push(zd(0));break}else r.push(zd(i))}if(t&&(r=r.filter((e,t)=>r.indexOf(e)===t),n!==void 0)){let e=zd(Id.codes.get(Number.parseInt(n,10)));r=r.reduce((t,n)=>n===e?[n,...t]:[...t,n],[])}return r.join(``)};function Vd(e,t,n){let r=[...e],i=[],a=typeof n==`number`?n:r.length,o=!1,s,c=0,l=``;for(let[u,d]of r.entries()){let r=!1;if(Rd.includes(d)){let t=/\d[^m]*/.exec(e.slice(u,u+18));s=t&&t.length>0?t[0]:void 0,ct&&c<=a)l+=d;else if(c===t&&!o&&s!==void 0)l=Bd(i);else if(c>=a){l+=Bd(i,!0,s);break}}return l}function Hd({onlyFirst:e=!1}={}){return RegExp(`(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))|[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]`,e?void 0:`g`)}var Ud=Hd();function Wd(e){if(typeof e!=`string`)throw TypeError(`Expected a \`string\`, got \`${typeof e}\``);return e.replace(Ud,``)}function Gd(e){return e===161||e===164||e===167||e===168||e===170||e===173||e===174||e>=176&&e<=180||e>=182&&e<=186||e>=188&&e<=191||e===198||e===208||e===215||e===216||e>=222&&e<=225||e===230||e>=232&&e<=234||e===236||e===237||e===240||e===242||e===243||e>=247&&e<=250||e===252||e===254||e===257||e===273||e===275||e===283||e===294||e===295||e===299||e>=305&&e<=307||e===312||e>=319&&e<=322||e===324||e>=328&&e<=331||e===333||e===338||e===339||e===358||e===359||e===363||e===462||e===464||e===466||e===468||e===470||e===472||e===474||e===476||e===593||e===609||e===708||e===711||e>=713&&e<=715||e===717||e===720||e>=728&&e<=731||e===733||e===735||e>=768&&e<=879||e>=913&&e<=929||e>=931&&e<=937||e>=945&&e<=961||e>=963&&e<=969||e===1025||e>=1040&&e<=1103||e===1105||e===8208||e>=8211&&e<=8214||e===8216||e===8217||e===8220||e===8221||e>=8224&&e<=8226||e>=8228&&e<=8231||e===8240||e===8242||e===8243||e===8245||e===8251||e===8254||e===8308||e===8319||e>=8321&&e<=8324||e===8364||e===8451||e===8453||e===8457||e===8467||e===8470||e===8481||e===8482||e===8486||e===8491||e===8531||e===8532||e>=8539&&e<=8542||e>=8544&&e<=8555||e>=8560&&e<=8569||e===8585||e>=8592&&e<=8601||e===8632||e===8633||e===8658||e===8660||e===8679||e===8704||e===8706||e===8707||e===8711||e===8712||e===8715||e===8719||e===8721||e===8725||e===8730||e>=8733&&e<=8736||e===8739||e===8741||e>=8743&&e<=8748||e===8750||e>=8756&&e<=8759||e===8764||e===8765||e===8776||e===8780||e===8786||e===8800||e===8801||e>=8804&&e<=8807||e===8810||e===8811||e===8814||e===8815||e===8834||e===8835||e===8838||e===8839||e===8853||e===8857||e===8869||e===8895||e===8978||e>=9312&&e<=9449||e>=9451&&e<=9547||e>=9552&&e<=9587||e>=9600&&e<=9615||e>=9618&&e<=9621||e===9632||e===9633||e>=9635&&e<=9641||e===9650||e===9651||e===9654||e===9655||e===9660||e===9661||e===9664||e===9665||e>=9670&&e<=9672||e===9675||e>=9678&&e<=9681||e>=9698&&e<=9701||e===9711||e===9733||e===9734||e===9737||e===9742||e===9743||e===9756||e===9758||e===9792||e===9794||e===9824||e===9825||e>=9827&&e<=9829||e>=9831&&e<=9834||e===9836||e===9837||e===9839||e===9886||e===9887||e===9919||e>=9926&&e<=9933||e>=9935&&e<=9939||e>=9941&&e<=9953||e===9955||e===9960||e===9961||e>=9963&&e<=9969||e===9972||e>=9974&&e<=9977||e===9979||e===9980||e===9982||e===9983||e===10045||e>=10102&&e<=10111||e>=11094&&e<=11097||e>=12872&&e<=12879||e>=57344&&e<=63743||e>=65024&&e<=65039||e===65533||e>=127232&&e<=127242||e>=127248&&e<=127277||e>=127280&&e<=127337||e>=127344&&e<=127373||e===127375||e===127376||e>=127387&&e<=127404||e>=917760&&e<=917999||e>=983040&&e<=1048573||e>=1048576&&e<=1114109}function Kd(e){return e===12288||e>=65281&&e<=65376||e>=65504&&e<=65510}function qd(e){return e>=4352&&e<=4447||e===8986||e===8987||e===9001||e===9002||e>=9193&&e<=9196||e===9200||e===9203||e===9725||e===9726||e===9748||e===9749||e>=9776&&e<=9783||e>=9800&&e<=9811||e===9855||e>=9866&&e<=9871||e===9875||e===9889||e===9898||e===9899||e===9917||e===9918||e===9924||e===9925||e===9934||e===9940||e===9962||e===9970||e===9971||e===9973||e===9978||e===9981||e===9989||e===9994||e===9995||e===10024||e===10060||e===10062||e>=10067&&e<=10069||e===10071||e>=10133&&e<=10135||e===10160||e===10175||e===11035||e===11036||e===11088||e===11093||e>=11904&&e<=11929||e>=11931&&e<=12019||e>=12032&&e<=12245||e>=12272&&e<=12287||e>=12289&&e<=12350||e>=12353&&e<=12438||e>=12441&&e<=12543||e>=12549&&e<=12591||e>=12593&&e<=12686||e>=12688&&e<=12773||e>=12783&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=42124||e>=42128&&e<=42182||e>=43360&&e<=43388||e>=44032&&e<=55203||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=94176&&e<=94180||e>=94192&&e<=94198||e>=94208&&e<=101589||e>=101631&&e<=101662||e>=101760&&e<=101874||e>=110576&&e<=110579||e>=110581&&e<=110587||e===110589||e===110590||e>=110592&&e<=110882||e===110898||e>=110928&&e<=110930||e===110933||e>=110948&&e<=110951||e>=110960&&e<=111355||e>=119552&&e<=119638||e>=119648&&e<=119670||e===126980||e===127183||e===127374||e>=127377&&e<=127386||e>=127488&&e<=127490||e>=127504&&e<=127547||e>=127552&&e<=127560||e===127568||e===127569||e>=127584&&e<=127589||e>=127744&&e<=127776||e>=127789&&e<=127797||e>=127799&&e<=127868||e>=127870&&e<=127891||e>=127904&&e<=127946||e>=127951&&e<=127955||e>=127968&&e<=127984||e===127988||e>=127992&&e<=128062||e===128064||e>=128066&&e<=128252||e>=128255&&e<=128317||e>=128331&&e<=128334||e>=128336&&e<=128359||e===128378||e===128405||e===128406||e===128420||e>=128507&&e<=128591||e>=128640&&e<=128709||e===128716||e>=128720&&e<=128722||e>=128725&&e<=128728||e>=128732&&e<=128735||e===128747||e===128748||e>=128756&&e<=128764||e>=128992&&e<=129003||e===129008||e>=129292&&e<=129338||e>=129340&&e<=129349||e>=129351&&e<=129535||e>=129648&&e<=129660||e>=129664&&e<=129674||e>=129678&&e<=129734||e===129736||e>=129741&&e<=129756||e>=129759&&e<=129770||e>=129775&&e<=129784||e>=131072&&e<=196605||e>=196608&&e<=262141}function Jd(e){if(!Number.isSafeInteger(e))throw TypeError(`Expected a code point, got \`${typeof e}\`.`)}function Yd(e,{ambiguousAsWide:t=!1}={}){return Jd(e),Kd(e)||qd(e)||t&&Gd(e)?2:1}var Xd=()=>/[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E-\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED8\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])))?))?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3C-\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE8A\uDE8E-\uDEC2\uDEC6\uDEC8\uDECD-\uDEDC\uDEDF-\uDEEA\uDEEF]|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/g,Zd=new Intl.Segmenter,Qd=/^\p{Default_Ignorable_Code_Point}$/u;function $d(e,t={}){if(typeof e!=`string`||e.length===0)return 0;let{ambiguousIsNarrow:n=!0,countAnsiEscapeCodes:r=!1}=t;if(r||(e=Wd(e)),e.length===0)return 0;let i=0,a={ambiguousAsWide:!n};for(let{segment:t}of Zd.segment(e)){let e=t.codePointAt(0);if(!(e<=31||e>=127&&e<=159)&&!(e>=8203&&e<=8207||e===65279)&&!(e>=768&&e<=879||e>=6832&&e<=6911||e>=7616&&e<=7679||e>=8400&&e<=8447||e>=65056&&e<=65071)&&!(e>=55296&&e<=57343)&&!(e>=65024&&e<=65039)&&!Qd.test(t)){if(Xd().test(t)){i+=2;continue}i+=Yd(e,a)}}return i}function ef(e,t,n){if(e.charAt(t)===` `)return t;let r=n?1:-1;for(let n=0;n<=3;n++){let i=t+n*r;if(e.charAt(i)===` `)return i}return t}function tf(e,t,n={}){let{position:r=`end`,space:i=!1,preferTruncationOnSpace:a=!1}=n,{truncationCharacter:o=`…`}=n;if(typeof e!=`string`)throw TypeError(`Expected \`input\` to be a string, got ${typeof e}`);if(typeof t!=`number`)throw TypeError(`Expected \`columns\` to be a number, got ${typeof t}`);if(t<1)return``;if(t===1)return o;let s=$d(e);if(s<=t)return e;if(r===`start`){if(a){let n=ef(e,s-t+1,!0);return o+Vd(e,n,s).trim()}return i===!0&&(o+=` `),o+Vd(e,s-t+$d(o),s)}if(r===`middle`){i===!0&&(o=` ${o} `);let n=Math.floor(t/2);if(a){let r=ef(e,n),i=ef(e,s-(t-n)+1,!0);return Vd(e,0,r)+o+Vd(e,i,s).trim()}return Vd(e,0,n)+o+Vd(e,s-(t-n)+$d(o),s)}if(r===`end`)return a?Vd(e,0,ef(e,t-1))+o:(i===!0&&(o=` ${o}`),Vd(e,0,t-$d(o))+o);throw Error(`Expected \`options.position\` to be either \`start\`, \`middle\` or \`end\`, got ${r}`)}function nf(e){try{return a.default.accessSync(e),!0}catch{return!1}}var rf=class extends Error{constructor(e,t){super(`Max tries reached.`),this.originalPath=e,this.lastTriedPath=t}},af=(e,t)=>{let n=e.match(/^(?.*)\((?\d+)\)$/),{filename:r,index:i}=n?n.groups:{filename:e,index:0};return r=r.trim(),[`${r}${t}`,`${r} (${++i})${t}`]},of=(e,t)=>{let n=r.default.extname(e),i=r.default.dirname(e),[a,o]=t(r.default.basename(e,n),n);return[r.default.join(i,a),r.default.join(i,o)]};function sf(e,{incrementer:t=af,maxTries:n=1/0}={}){let r=0,[i]=of(e,t),a=e;for(;;){if(!nf(a))return a;if(++r>n)throw new rf(i,a);[i,a]=of(a,t)}}var cf=e=>e.replace(/&/g,`&`).replace(/"/g,`"`).replace(/'/g,`'`).replace(//g,`>`);function lf(e,...t){if(typeof e==`string`)return cf(e);let n=e[0];for(let[r,i]of t.entries())n=n+cf(String(i))+e[r+1];return n}var uf=class extends Error{constructor(e){super(`Missing a value for ${e?`the placeholder: ${e}`:`a placeholder`}`,e),this.name=`MissingValueError`,this.key=e}},df=class extends Error{constructor(e){super(`Missing filter: ${e}`),this.name=`MissingFilterError`,this.filterName=e}};function ff(e,t,{ignoreMissing:n=!1,transform:r=({value:e})=>e,filters:i={}}={}){if(typeof e!=`string`)throw TypeError(`Expected a \`string\` in the first argument, got \`${typeof e}\``);if(typeof t!=`object`)throw TypeError(`Expected an \`object\` or \`Array\` in the second argument, got \`${typeof t}\``);let a=``,o=``;e=e.replace(/\\{/g,a),e=e.replace(/\\}/g,o);let s=e=>{let t=[],n=``;for(let r=0;r{let o=a.split(`|`).map(e=>e.trim()),c=o[0],l=o.slice(1),u=s(c),d=t;for(let e of u)d&&=d[e];for(let t of l){let r=i[t];if(!r){if(n)return e;throw new df(t)}d!==void 0&&(d=r(d))}let f=r({value:d,key:c});if(f===void 0){if(n)return e;throw new uf(c)}return String(f)},l=`((\\d+|[a-z$_][\\w\\-.$\\\\]*)\\s*(?:\\|\\s*[a-z$_][\\w$]*\\s*)*)`,u=RegExp(`{{${l}}}`,`gi`),d=RegExp(`{${l}}`,`gi`);return e=e.replace(u,(...e)=>lf(c(...e))),e=e.replace(d,c),e=e.replace(new RegExp(a,`g`),`{`),e=e.replace(new RegExp(o,`g`),`}`),e}var pf=e.nr({default:()=>mf}),mf,hf=e.tr((()=>{mf={"application/1d-interleaved-parityfec":{source:`iana`},"application/3gpdash-qoe-report+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/3gpp-ims+xml":{source:`iana`,compressible:!0},"application/3gpphal+json":{source:`iana`,compressible:!0},"application/3gpphalforms+json":{source:`iana`,compressible:!0},"application/a2l":{source:`iana`},"application/ace+cbor":{source:`iana`},"application/ace+json":{source:`iana`,compressible:!0},"application/ace-groupcomm+cbor":{source:`iana`},"application/ace-trl+cbor":{source:`iana`},"application/activemessage":{source:`iana`},"application/activity+json":{source:`iana`,compressible:!0},"application/aif+cbor":{source:`iana`},"application/aif+json":{source:`iana`,compressible:!0},"application/alto-cdni+json":{source:`iana`,compressible:!0},"application/alto-cdnifilter+json":{source:`iana`,compressible:!0},"application/alto-costmap+json":{source:`iana`,compressible:!0},"application/alto-costmapfilter+json":{source:`iana`,compressible:!0},"application/alto-directory+json":{source:`iana`,compressible:!0},"application/alto-endpointcost+json":{source:`iana`,compressible:!0},"application/alto-endpointcostparams+json":{source:`iana`,compressible:!0},"application/alto-endpointprop+json":{source:`iana`,compressible:!0},"application/alto-endpointpropparams+json":{source:`iana`,compressible:!0},"application/alto-error+json":{source:`iana`,compressible:!0},"application/alto-networkmap+json":{source:`iana`,compressible:!0},"application/alto-networkmapfilter+json":{source:`iana`,compressible:!0},"application/alto-propmap+json":{source:`iana`,compressible:!0},"application/alto-propmapparams+json":{source:`iana`,compressible:!0},"application/alto-tips+json":{source:`iana`,compressible:!0},"application/alto-tipsparams+json":{source:`iana`,compressible:!0},"application/alto-updatestreamcontrol+json":{source:`iana`,compressible:!0},"application/alto-updatestreamparams+json":{source:`iana`,compressible:!0},"application/aml":{source:`iana`},"application/andrew-inset":{source:`iana`,extensions:[`ez`]},"application/appinstaller":{compressible:!1,extensions:[`appinstaller`]},"application/applefile":{source:`iana`},"application/applixware":{source:`apache`,extensions:[`aw`]},"application/appx":{compressible:!1,extensions:[`appx`]},"application/appxbundle":{compressible:!1,extensions:[`appxbundle`]},"application/at+jwt":{source:`iana`},"application/atf":{source:`iana`},"application/atfx":{source:`iana`},"application/atom+xml":{source:`iana`,compressible:!0,extensions:[`atom`]},"application/atomcat+xml":{source:`iana`,compressible:!0,extensions:[`atomcat`]},"application/atomdeleted+xml":{source:`iana`,compressible:!0,extensions:[`atomdeleted`]},"application/atomicmail":{source:`iana`},"application/atomsvc+xml":{source:`iana`,compressible:!0,extensions:[`atomsvc`]},"application/atsc-dwd+xml":{source:`iana`,compressible:!0,extensions:[`dwd`]},"application/atsc-dynamic-event-message":{source:`iana`},"application/atsc-held+xml":{source:`iana`,compressible:!0,extensions:[`held`]},"application/atsc-rdt+json":{source:`iana`,compressible:!0},"application/atsc-rsat+xml":{source:`iana`,compressible:!0,extensions:[`rsat`]},"application/atxml":{source:`iana`},"application/auth-policy+xml":{source:`iana`,compressible:!0},"application/automationml-aml+xml":{source:`iana`,compressible:!0,extensions:[`aml`]},"application/automationml-amlx+zip":{source:`iana`,compressible:!1,extensions:[`amlx`]},"application/bacnet-xdd+zip":{source:`iana`,compressible:!1},"application/batch-smtp":{source:`iana`},"application/bdoc":{compressible:!1,extensions:[`bdoc`]},"application/beep+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/bufr":{source:`iana`},"application/c2pa":{source:`iana`},"application/calendar+json":{source:`iana`,compressible:!0},"application/calendar+xml":{source:`iana`,compressible:!0,extensions:[`xcs`]},"application/call-completion":{source:`iana`},"application/cals-1840":{source:`iana`},"application/captive+json":{source:`iana`,compressible:!0},"application/cbor":{source:`iana`},"application/cbor-seq":{source:`iana`},"application/cccex":{source:`iana`},"application/ccmp+xml":{source:`iana`,compressible:!0},"application/ccxml+xml":{source:`iana`,compressible:!0,extensions:[`ccxml`]},"application/cda+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/cdfx+xml":{source:`iana`,compressible:!0,extensions:[`cdfx`]},"application/cdmi-capability":{source:`iana`,extensions:[`cdmia`]},"application/cdmi-container":{source:`iana`,extensions:[`cdmic`]},"application/cdmi-domain":{source:`iana`,extensions:[`cdmid`]},"application/cdmi-object":{source:`iana`,extensions:[`cdmio`]},"application/cdmi-queue":{source:`iana`,extensions:[`cdmiq`]},"application/cdni":{source:`iana`},"application/ce+cbor":{source:`iana`},"application/cea":{source:`iana`},"application/cea-2018+xml":{source:`iana`,compressible:!0},"application/cellml+xml":{source:`iana`,compressible:!0},"application/cfw":{source:`iana`},"application/cid-edhoc+cbor-seq":{source:`iana`},"application/city+json":{source:`iana`,compressible:!0},"application/city+json-seq":{source:`iana`},"application/clr":{source:`iana`},"application/clue+xml":{source:`iana`,compressible:!0},"application/clue_info+xml":{source:`iana`,compressible:!0},"application/cms":{source:`iana`},"application/cnrp+xml":{source:`iana`,compressible:!0},"application/coap-eap":{source:`iana`},"application/coap-group+json":{source:`iana`,compressible:!0},"application/coap-payload":{source:`iana`},"application/commonground":{source:`iana`},"application/concise-problem-details+cbor":{source:`iana`},"application/conference-info+xml":{source:`iana`,compressible:!0},"application/cose":{source:`iana`},"application/cose-key":{source:`iana`},"application/cose-key-set":{source:`iana`},"application/cose-x509":{source:`iana`},"application/cpl+xml":{source:`iana`,compressible:!0,extensions:[`cpl`]},"application/csrattrs":{source:`iana`},"application/csta+xml":{source:`iana`,compressible:!0},"application/cstadata+xml":{source:`iana`,compressible:!0},"application/csvm+json":{source:`iana`,compressible:!0},"application/cu-seeme":{source:`apache`,extensions:[`cu`]},"application/cwl":{source:`iana`,extensions:[`cwl`]},"application/cwl+json":{source:`iana`,compressible:!0},"application/cwl+yaml":{source:`iana`},"application/cwt":{source:`iana`},"application/cybercash":{source:`iana`},"application/dart":{compressible:!0},"application/dash+xml":{source:`iana`,compressible:!0,extensions:[`mpd`]},"application/dash-patch+xml":{source:`iana`,compressible:!0,extensions:[`mpp`]},"application/dashdelta":{source:`iana`},"application/davmount+xml":{source:`iana`,compressible:!0,extensions:[`davmount`]},"application/dca-rft":{source:`iana`},"application/dcd":{source:`iana`},"application/dec-dx":{source:`iana`},"application/dialog-info+xml":{source:`iana`,compressible:!0},"application/dicom":{source:`iana`,extensions:[`dcm`]},"application/dicom+json":{source:`iana`,compressible:!0},"application/dicom+xml":{source:`iana`,compressible:!0},"application/dii":{source:`iana`},"application/dit":{source:`iana`},"application/dns":{source:`iana`},"application/dns+json":{source:`iana`,compressible:!0},"application/dns-message":{source:`iana`},"application/docbook+xml":{source:`apache`,compressible:!0,extensions:[`dbk`]},"application/dots+cbor":{source:`iana`},"application/dpop+jwt":{source:`iana`},"application/dskpp+xml":{source:`iana`,compressible:!0},"application/dssc+der":{source:`iana`,extensions:[`dssc`]},"application/dssc+xml":{source:`iana`,compressible:!0,extensions:[`xdssc`]},"application/dvcs":{source:`iana`},"application/eat+cwt":{source:`iana`},"application/eat+jwt":{source:`iana`},"application/eat-bun+cbor":{source:`iana`},"application/eat-bun+json":{source:`iana`,compressible:!0},"application/eat-ucs+cbor":{source:`iana`},"application/eat-ucs+json":{source:`iana`,compressible:!0},"application/ecmascript":{source:`apache`,compressible:!0,extensions:[`ecma`]},"application/edhoc+cbor-seq":{source:`iana`},"application/edi-consent":{source:`iana`},"application/edi-x12":{source:`iana`,compressible:!1},"application/edifact":{source:`iana`,compressible:!1},"application/efi":{source:`iana`},"application/elm+json":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/elm+xml":{source:`iana`,compressible:!0},"application/emergencycalldata.cap+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/emergencycalldata.comment+xml":{source:`iana`,compressible:!0},"application/emergencycalldata.control+xml":{source:`iana`,compressible:!0},"application/emergencycalldata.deviceinfo+xml":{source:`iana`,compressible:!0},"application/emergencycalldata.ecall.msd":{source:`iana`},"application/emergencycalldata.legacyesn+json":{source:`iana`,compressible:!0},"application/emergencycalldata.providerinfo+xml":{source:`iana`,compressible:!0},"application/emergencycalldata.serviceinfo+xml":{source:`iana`,compressible:!0},"application/emergencycalldata.subscriberinfo+xml":{source:`iana`,compressible:!0},"application/emergencycalldata.veds+xml":{source:`iana`,compressible:!0},"application/emma+xml":{source:`iana`,compressible:!0,extensions:[`emma`]},"application/emotionml+xml":{source:`iana`,compressible:!0,extensions:[`emotionml`]},"application/encaprtp":{source:`iana`},"application/entity-statement+jwt":{source:`iana`},"application/epp+xml":{source:`iana`,compressible:!0},"application/epub+zip":{source:`iana`,compressible:!1,extensions:[`epub`]},"application/eshop":{source:`iana`},"application/exi":{source:`iana`,extensions:[`exi`]},"application/expect-ct-report+json":{source:`iana`,compressible:!0},"application/express":{source:`iana`,extensions:[`exp`]},"application/fastinfoset":{source:`iana`},"application/fastsoap":{source:`iana`},"application/fdf":{source:`iana`,extensions:[`fdf`]},"application/fdt+xml":{source:`iana`,compressible:!0,extensions:[`fdt`]},"application/fhir+json":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/fhir+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/fido.trusted-apps+json":{compressible:!0},"application/fits":{source:`iana`},"application/flexfec":{source:`iana`},"application/font-sfnt":{source:`iana`},"application/font-tdpfr":{source:`iana`,extensions:[`pfr`]},"application/font-woff":{source:`iana`,compressible:!1},"application/framework-attributes+xml":{source:`iana`,compressible:!0},"application/geo+json":{source:`iana`,compressible:!0,extensions:[`geojson`]},"application/geo+json-seq":{source:`iana`},"application/geopackage+sqlite3":{source:`iana`},"application/geopose+json":{source:`iana`,compressible:!0},"application/geoxacml+json":{source:`iana`,compressible:!0},"application/geoxacml+xml":{source:`iana`,compressible:!0},"application/gltf-buffer":{source:`iana`},"application/gml+xml":{source:`iana`,compressible:!0,extensions:[`gml`]},"application/gnap-binding-jws":{source:`iana`},"application/gnap-binding-jwsd":{source:`iana`},"application/gnap-binding-rotation-jws":{source:`iana`},"application/gnap-binding-rotation-jwsd":{source:`iana`},"application/gpx+xml":{source:`apache`,compressible:!0,extensions:[`gpx`]},"application/grib":{source:`iana`},"application/gxf":{source:`apache`,extensions:[`gxf`]},"application/gzip":{source:`iana`,compressible:!1,extensions:[`gz`]},"application/h224":{source:`iana`},"application/held+xml":{source:`iana`,compressible:!0},"application/hjson":{extensions:[`hjson`]},"application/hl7v2+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/http":{source:`iana`},"application/hyperstudio":{source:`iana`,extensions:[`stk`]},"application/ibe-key-request+xml":{source:`iana`,compressible:!0},"application/ibe-pkg-reply+xml":{source:`iana`,compressible:!0},"application/ibe-pp-data":{source:`iana`},"application/iges":{source:`iana`},"application/im-iscomposing+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/index":{source:`iana`},"application/index.cmd":{source:`iana`},"application/index.obj":{source:`iana`},"application/index.response":{source:`iana`},"application/index.vnd":{source:`iana`},"application/inkml+xml":{source:`iana`,compressible:!0,extensions:[`ink`,`inkml`]},"application/iotp":{source:`iana`},"application/ipfix":{source:`iana`,extensions:[`ipfix`]},"application/ipp":{source:`iana`},"application/isup":{source:`iana`},"application/its+xml":{source:`iana`,compressible:!0,extensions:[`its`]},"application/java-archive":{source:`iana`,compressible:!1,extensions:[`jar`,`war`,`ear`]},"application/java-serialized-object":{source:`apache`,compressible:!1,extensions:[`ser`]},"application/java-vm":{source:`apache`,compressible:!1,extensions:[`class`]},"application/javascript":{source:`apache`,charset:`UTF-8`,compressible:!0,extensions:[`js`]},"application/jf2feed+json":{source:`iana`,compressible:!0},"application/jose":{source:`iana`},"application/jose+json":{source:`iana`,compressible:!0},"application/jrd+json":{source:`iana`,compressible:!0},"application/jscalendar+json":{source:`iana`,compressible:!0},"application/jscontact+json":{source:`iana`,compressible:!0},"application/json":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`json`,`map`]},"application/json-patch+json":{source:`iana`,compressible:!0},"application/json-seq":{source:`iana`},"application/json5":{extensions:[`json5`]},"application/jsonml+json":{source:`apache`,compressible:!0,extensions:[`jsonml`]},"application/jsonpath":{source:`iana`},"application/jwk+json":{source:`iana`,compressible:!0},"application/jwk-set+json":{source:`iana`,compressible:!0},"application/jwk-set+jwt":{source:`iana`},"application/jwt":{source:`iana`},"application/kpml-request+xml":{source:`iana`,compressible:!0},"application/kpml-response+xml":{source:`iana`,compressible:!0},"application/ld+json":{source:`iana`,compressible:!0,extensions:[`jsonld`]},"application/lgr+xml":{source:`iana`,compressible:!0,extensions:[`lgr`]},"application/link-format":{source:`iana`},"application/linkset":{source:`iana`},"application/linkset+json":{source:`iana`,compressible:!0},"application/load-control+xml":{source:`iana`,compressible:!0},"application/logout+jwt":{source:`iana`},"application/lost+xml":{source:`iana`,compressible:!0,extensions:[`lostxml`]},"application/lostsync+xml":{source:`iana`,compressible:!0},"application/lpf+zip":{source:`iana`,compressible:!1},"application/lxf":{source:`iana`},"application/mac-binhex40":{source:`iana`,extensions:[`hqx`]},"application/mac-compactpro":{source:`apache`,extensions:[`cpt`]},"application/macwriteii":{source:`iana`},"application/mads+xml":{source:`iana`,compressible:!0,extensions:[`mads`]},"application/manifest+json":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`webmanifest`]},"application/marc":{source:`iana`,extensions:[`mrc`]},"application/marcxml+xml":{source:`iana`,compressible:!0,extensions:[`mrcx`]},"application/mathematica":{source:`iana`,extensions:[`ma`,`nb`,`mb`]},"application/mathml+xml":{source:`iana`,compressible:!0,extensions:[`mathml`]},"application/mathml-content+xml":{source:`iana`,compressible:!0},"application/mathml-presentation+xml":{source:`iana`,compressible:!0},"application/mbms-associated-procedure-description+xml":{source:`iana`,compressible:!0},"application/mbms-deregister+xml":{source:`iana`,compressible:!0},"application/mbms-envelope+xml":{source:`iana`,compressible:!0},"application/mbms-msk+xml":{source:`iana`,compressible:!0},"application/mbms-msk-response+xml":{source:`iana`,compressible:!0},"application/mbms-protection-description+xml":{source:`iana`,compressible:!0},"application/mbms-reception-report+xml":{source:`iana`,compressible:!0},"application/mbms-register+xml":{source:`iana`,compressible:!0},"application/mbms-register-response+xml":{source:`iana`,compressible:!0},"application/mbms-schedule+xml":{source:`iana`,compressible:!0},"application/mbms-user-service-description+xml":{source:`iana`,compressible:!0},"application/mbox":{source:`iana`,extensions:[`mbox`]},"application/media-policy-dataset+xml":{source:`iana`,compressible:!0,extensions:[`mpf`]},"application/media_control+xml":{source:`iana`,compressible:!0},"application/mediaservercontrol+xml":{source:`iana`,compressible:!0,extensions:[`mscml`]},"application/merge-patch+json":{source:`iana`,compressible:!0},"application/metalink+xml":{source:`apache`,compressible:!0,extensions:[`metalink`]},"application/metalink4+xml":{source:`iana`,compressible:!0,extensions:[`meta4`]},"application/mets+xml":{source:`iana`,compressible:!0,extensions:[`mets`]},"application/mf4":{source:`iana`},"application/mikey":{source:`iana`},"application/mipc":{source:`iana`},"application/missing-blocks+cbor-seq":{source:`iana`},"application/mmt-aei+xml":{source:`iana`,compressible:!0,extensions:[`maei`]},"application/mmt-usd+xml":{source:`iana`,compressible:!0,extensions:[`musd`]},"application/mods+xml":{source:`iana`,compressible:!0,extensions:[`mods`]},"application/moss-keys":{source:`iana`},"application/moss-signature":{source:`iana`},"application/mosskey-data":{source:`iana`},"application/mosskey-request":{source:`iana`},"application/mp21":{source:`iana`,extensions:[`m21`,`mp21`]},"application/mp4":{source:`iana`,extensions:[`mp4`,`mpg4`,`mp4s`,`m4p`]},"application/mpeg4-generic":{source:`iana`},"application/mpeg4-iod":{source:`iana`},"application/mpeg4-iod-xmt":{source:`iana`},"application/mrb-consumer+xml":{source:`iana`,compressible:!0},"application/mrb-publish+xml":{source:`iana`,compressible:!0},"application/msc-ivr+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/msc-mixer+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/msix":{compressible:!1,extensions:[`msix`]},"application/msixbundle":{compressible:!1,extensions:[`msixbundle`]},"application/msword":{source:`iana`,compressible:!1,extensions:[`doc`,`dot`]},"application/mud+json":{source:`iana`,compressible:!0},"application/multipart-core":{source:`iana`},"application/mxf":{source:`iana`,extensions:[`mxf`]},"application/n-quads":{source:`iana`,extensions:[`nq`]},"application/n-triples":{source:`iana`,extensions:[`nt`]},"application/nasdata":{source:`iana`},"application/news-checkgroups":{source:`iana`,charset:`US-ASCII`},"application/news-groupinfo":{source:`iana`,charset:`US-ASCII`},"application/news-transmission":{source:`iana`},"application/nlsml+xml":{source:`iana`,compressible:!0},"application/node":{source:`iana`,extensions:[`cjs`]},"application/nss":{source:`iana`},"application/oauth-authz-req+jwt":{source:`iana`},"application/oblivious-dns-message":{source:`iana`},"application/ocsp-request":{source:`iana`},"application/ocsp-response":{source:`iana`},"application/octet-stream":{source:`iana`,compressible:!0,extensions:[`bin`,`dms`,`lrf`,`mar`,`so`,`dist`,`distz`,`pkg`,`bpk`,`dump`,`elc`,`deploy`,`exe`,`dll`,`deb`,`dmg`,`iso`,`img`,`msi`,`msp`,`msm`,`buffer`]},"application/oda":{source:`iana`,extensions:[`oda`]},"application/odm+xml":{source:`iana`,compressible:!0},"application/odx":{source:`iana`},"application/oebps-package+xml":{source:`iana`,compressible:!0,extensions:[`opf`]},"application/ogg":{source:`iana`,compressible:!1,extensions:[`ogx`]},"application/ohttp-keys":{source:`iana`},"application/omdoc+xml":{source:`apache`,compressible:!0,extensions:[`omdoc`]},"application/onenote":{source:`apache`,extensions:[`onetoc`,`onetoc2`,`onetmp`,`onepkg`,`one`,`onea`]},"application/opc-nodeset+xml":{source:`iana`,compressible:!0},"application/oscore":{source:`iana`},"application/oxps":{source:`iana`,extensions:[`oxps`]},"application/p21":{source:`iana`},"application/p21+zip":{source:`iana`,compressible:!1},"application/p2p-overlay+xml":{source:`iana`,compressible:!0,extensions:[`relo`]},"application/parityfec":{source:`iana`},"application/passport":{source:`iana`},"application/patch-ops-error+xml":{source:`iana`,compressible:!0,extensions:[`xer`]},"application/pdf":{source:`iana`,compressible:!1,extensions:[`pdf`]},"application/pdx":{source:`iana`},"application/pem-certificate-chain":{source:`iana`},"application/pgp-encrypted":{source:`iana`,compressible:!1,extensions:[`pgp`]},"application/pgp-keys":{source:`iana`,extensions:[`asc`]},"application/pgp-signature":{source:`iana`,extensions:[`sig`,`asc`]},"application/pics-rules":{source:`apache`,extensions:[`prf`]},"application/pidf+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/pidf-diff+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/pkcs10":{source:`iana`,extensions:[`p10`]},"application/pkcs12":{source:`iana`},"application/pkcs7-mime":{source:`iana`,extensions:[`p7m`,`p7c`]},"application/pkcs7-signature":{source:`iana`,extensions:[`p7s`]},"application/pkcs8":{source:`iana`,extensions:[`p8`]},"application/pkcs8-encrypted":{source:`iana`},"application/pkix-attr-cert":{source:`iana`,extensions:[`ac`]},"application/pkix-cert":{source:`iana`,extensions:[`cer`]},"application/pkix-crl":{source:`iana`,extensions:[`crl`]},"application/pkix-pkipath":{source:`iana`,extensions:[`pkipath`]},"application/pkixcmp":{source:`iana`,extensions:[`pki`]},"application/pls+xml":{source:`iana`,compressible:!0,extensions:[`pls`]},"application/poc-settings+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/postscript":{source:`iana`,compressible:!0,extensions:[`ai`,`eps`,`ps`]},"application/ppsp-tracker+json":{source:`iana`,compressible:!0},"application/private-token-issuer-directory":{source:`iana`},"application/private-token-request":{source:`iana`},"application/private-token-response":{source:`iana`},"application/problem+json":{source:`iana`,compressible:!0},"application/problem+xml":{source:`iana`,compressible:!0},"application/provenance+xml":{source:`iana`,compressible:!0,extensions:[`provx`]},"application/provided-claims+jwt":{source:`iana`},"application/prs.alvestrand.titrax-sheet":{source:`iana`},"application/prs.cww":{source:`iana`,extensions:[`cww`]},"application/prs.cyn":{source:`iana`,charset:`7-BIT`},"application/prs.hpub+zip":{source:`iana`,compressible:!1},"application/prs.implied-document+xml":{source:`iana`,compressible:!0},"application/prs.implied-executable":{source:`iana`},"application/prs.implied-object+json":{source:`iana`,compressible:!0},"application/prs.implied-object+json-seq":{source:`iana`},"application/prs.implied-object+yaml":{source:`iana`},"application/prs.implied-structure":{source:`iana`},"application/prs.mayfile":{source:`iana`},"application/prs.nprend":{source:`iana`},"application/prs.plucker":{source:`iana`},"application/prs.rdf-xml-crypt":{source:`iana`},"application/prs.vcfbzip2":{source:`iana`},"application/prs.xsf+xml":{source:`iana`,compressible:!0,extensions:[`xsf`]},"application/pskc+xml":{source:`iana`,compressible:!0,extensions:[`pskcxml`]},"application/pvd+json":{source:`iana`,compressible:!0},"application/qsig":{source:`iana`},"application/raml+yaml":{compressible:!0,extensions:[`raml`]},"application/raptorfec":{source:`iana`},"application/rdap+json":{source:`iana`,compressible:!0},"application/rdf+xml":{source:`iana`,compressible:!0,extensions:[`rdf`,`owl`]},"application/reginfo+xml":{source:`iana`,compressible:!0,extensions:[`rif`]},"application/relax-ng-compact-syntax":{source:`iana`,extensions:[`rnc`]},"application/remote-printing":{source:`apache`},"application/reputon+json":{source:`iana`,compressible:!0},"application/resolve-response+jwt":{source:`iana`},"application/resource-lists+xml":{source:`iana`,compressible:!0,extensions:[`rl`]},"application/resource-lists-diff+xml":{source:`iana`,compressible:!0,extensions:[`rld`]},"application/rfc+xml":{source:`iana`,compressible:!0},"application/riscos":{source:`iana`},"application/rlmi+xml":{source:`iana`,compressible:!0},"application/rls-services+xml":{source:`iana`,compressible:!0,extensions:[`rs`]},"application/route-apd+xml":{source:`iana`,compressible:!0,extensions:[`rapd`]},"application/route-s-tsid+xml":{source:`iana`,compressible:!0,extensions:[`sls`]},"application/route-usd+xml":{source:`iana`,compressible:!0,extensions:[`rusd`]},"application/rpki-checklist":{source:`iana`},"application/rpki-ghostbusters":{source:`iana`,extensions:[`gbr`]},"application/rpki-manifest":{source:`iana`,extensions:[`mft`]},"application/rpki-publication":{source:`iana`},"application/rpki-roa":{source:`iana`,extensions:[`roa`]},"application/rpki-signed-tal":{source:`iana`},"application/rpki-updown":{source:`iana`},"application/rsd+xml":{source:`apache`,compressible:!0,extensions:[`rsd`]},"application/rss+xml":{source:`apache`,compressible:!0,extensions:[`rss`]},"application/rtf":{source:`iana`,compressible:!0,extensions:[`rtf`]},"application/rtploopback":{source:`iana`},"application/rtx":{source:`iana`},"application/samlassertion+xml":{source:`iana`,compressible:!0},"application/samlmetadata+xml":{source:`iana`,compressible:!0},"application/sarif+json":{source:`iana`,compressible:!0},"application/sarif-external-properties+json":{source:`iana`,compressible:!0},"application/sbe":{source:`iana`},"application/sbml+xml":{source:`iana`,compressible:!0,extensions:[`sbml`]},"application/scaip+xml":{source:`iana`,compressible:!0},"application/scim+json":{source:`iana`,compressible:!0},"application/scvp-cv-request":{source:`iana`,extensions:[`scq`]},"application/scvp-cv-response":{source:`iana`,extensions:[`scs`]},"application/scvp-vp-request":{source:`iana`,extensions:[`spq`]},"application/scvp-vp-response":{source:`iana`,extensions:[`spp`]},"application/sdp":{source:`iana`,extensions:[`sdp`]},"application/secevent+jwt":{source:`iana`},"application/senml+cbor":{source:`iana`},"application/senml+json":{source:`iana`,compressible:!0},"application/senml+xml":{source:`iana`,compressible:!0,extensions:[`senmlx`]},"application/senml-etch+cbor":{source:`iana`},"application/senml-etch+json":{source:`iana`,compressible:!0},"application/senml-exi":{source:`iana`},"application/sensml+cbor":{source:`iana`},"application/sensml+json":{source:`iana`,compressible:!0},"application/sensml+xml":{source:`iana`,compressible:!0,extensions:[`sensmlx`]},"application/sensml-exi":{source:`iana`},"application/sep+xml":{source:`iana`,compressible:!0},"application/sep-exi":{source:`iana`},"application/session-info":{source:`iana`},"application/set-payment":{source:`iana`},"application/set-payment-initiation":{source:`iana`,extensions:[`setpay`]},"application/set-registration":{source:`iana`},"application/set-registration-initiation":{source:`iana`,extensions:[`setreg`]},"application/sgml":{source:`iana`},"application/sgml-open-catalog":{source:`iana`},"application/shf+xml":{source:`iana`,compressible:!0,extensions:[`shf`]},"application/sieve":{source:`iana`,extensions:[`siv`,`sieve`]},"application/simple-filter+xml":{source:`iana`,compressible:!0},"application/simple-message-summary":{source:`iana`},"application/simplesymbolcontainer":{source:`iana`},"application/sipc":{source:`iana`},"application/slate":{source:`iana`},"application/smil":{source:`apache`},"application/smil+xml":{source:`iana`,compressible:!0,extensions:[`smi`,`smil`]},"application/smpte336m":{source:`iana`},"application/soap+fastinfoset":{source:`iana`},"application/soap+xml":{source:`iana`,compressible:!0},"application/sparql-query":{source:`iana`,extensions:[`rq`]},"application/sparql-results+xml":{source:`iana`,compressible:!0,extensions:[`srx`]},"application/spdx+json":{source:`iana`,compressible:!0},"application/spirits-event+xml":{source:`iana`,compressible:!0},"application/sql":{source:`iana`,extensions:[`sql`]},"application/srgs":{source:`iana`,extensions:[`gram`]},"application/srgs+xml":{source:`iana`,compressible:!0,extensions:[`grxml`]},"application/sru+xml":{source:`iana`,compressible:!0,extensions:[`sru`]},"application/ssdl+xml":{source:`apache`,compressible:!0,extensions:[`ssdl`]},"application/sslkeylogfile":{source:`iana`},"application/ssml+xml":{source:`iana`,compressible:!0,extensions:[`ssml`]},"application/st2110-41":{source:`iana`},"application/stix+json":{source:`iana`,compressible:!0},"application/stratum":{source:`iana`},"application/swid+cbor":{source:`iana`},"application/swid+xml":{source:`iana`,compressible:!0,extensions:[`swidtag`]},"application/tamp-apex-update":{source:`iana`},"application/tamp-apex-update-confirm":{source:`iana`},"application/tamp-community-update":{source:`iana`},"application/tamp-community-update-confirm":{source:`iana`},"application/tamp-error":{source:`iana`},"application/tamp-sequence-adjust":{source:`iana`},"application/tamp-sequence-adjust-confirm":{source:`iana`},"application/tamp-status-query":{source:`iana`},"application/tamp-status-response":{source:`iana`},"application/tamp-update":{source:`iana`},"application/tamp-update-confirm":{source:`iana`},"application/tar":{compressible:!0},"application/taxii+json":{source:`iana`,compressible:!0},"application/td+json":{source:`iana`,compressible:!0},"application/tei+xml":{source:`iana`,compressible:!0,extensions:[`tei`,`teicorpus`]},"application/tetra_isi":{source:`iana`},"application/thraud+xml":{source:`iana`,compressible:!0,extensions:[`tfi`]},"application/timestamp-query":{source:`iana`},"application/timestamp-reply":{source:`iana`},"application/timestamped-data":{source:`iana`,extensions:[`tsd`]},"application/tlsrpt+gzip":{source:`iana`},"application/tlsrpt+json":{source:`iana`,compressible:!0},"application/tm+json":{source:`iana`,compressible:!0},"application/tnauthlist":{source:`iana`},"application/toc+cbor":{source:`iana`},"application/token-introspection+jwt":{source:`iana`},"application/toml":{source:`iana`,compressible:!0,extensions:[`toml`]},"application/trickle-ice-sdpfrag":{source:`iana`},"application/trig":{source:`iana`,extensions:[`trig`]},"application/trust-chain+json":{source:`iana`,compressible:!0},"application/trust-mark+jwt":{source:`iana`},"application/trust-mark-delegation+jwt":{source:`iana`},"application/ttml+xml":{source:`iana`,compressible:!0,extensions:[`ttml`]},"application/tve-trigger":{source:`iana`},"application/tzif":{source:`iana`},"application/tzif-leap":{source:`iana`},"application/ubjson":{compressible:!1,extensions:[`ubj`]},"application/uccs+cbor":{source:`iana`},"application/ujcs+json":{source:`iana`,compressible:!0},"application/ulpfec":{source:`iana`},"application/urc-grpsheet+xml":{source:`iana`,compressible:!0},"application/urc-ressheet+xml":{source:`iana`,compressible:!0,extensions:[`rsheet`]},"application/urc-targetdesc+xml":{source:`iana`,compressible:!0,extensions:[`td`]},"application/urc-uisocketdesc+xml":{source:`iana`,compressible:!0},"application/vc":{source:`iana`},"application/vc+cose":{source:`iana`},"application/vc+jwt":{source:`iana`},"application/vcard+json":{source:`iana`,compressible:!0},"application/vcard+xml":{source:`iana`,compressible:!0},"application/vemmi":{source:`iana`},"application/vividence.scriptfile":{source:`apache`},"application/vnd.1000minds.decision-model+xml":{source:`iana`,compressible:!0,extensions:[`1km`]},"application/vnd.1ob":{source:`iana`},"application/vnd.3gpp-prose+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp-prose-pc3a+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp-prose-pc3ach+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp-prose-pc3ch+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp-prose-pc8+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp-v2x-local-service-information":{source:`iana`},"application/vnd.3gpp.5gnas":{source:`iana`},"application/vnd.3gpp.5gsa2x":{source:`iana`},"application/vnd.3gpp.5gsa2x-local-service-information":{source:`iana`},"application/vnd.3gpp.5gsv2x":{source:`iana`},"application/vnd.3gpp.5gsv2x-local-service-information":{source:`iana`},"application/vnd.3gpp.access-transfer-events+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.bsf+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.crs+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.current-location-discovery+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.gmop+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.gtpc":{source:`iana`},"application/vnd.3gpp.interworking-data":{source:`iana`},"application/vnd.3gpp.lpp":{source:`iana`},"application/vnd.3gpp.mc-signalling-ear":{source:`iana`},"application/vnd.3gpp.mcdata-affiliation-command+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcdata-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcdata-msgstore-ctrl-request+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcdata-payload":{source:`iana`},"application/vnd.3gpp.mcdata-regroup+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcdata-service-config+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcdata-signalling":{source:`iana`},"application/vnd.3gpp.mcdata-ue-config+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcdata-user-profile+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-affiliation-command+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-floor-request+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-location-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-mbms-usage-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-regroup+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-service-config+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-signed+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-ue-config+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-ue-init-config+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcptt-user-profile+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-affiliation-command+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-location-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-mbms-usage-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-regroup+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-service-config+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-transmission-request+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-ue-config+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mcvideo-user-profile+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.mid-call+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.ngap":{source:`iana`},"application/vnd.3gpp.pfcp":{source:`iana`},"application/vnd.3gpp.pic-bw-large":{source:`iana`,extensions:[`plb`]},"application/vnd.3gpp.pic-bw-small":{source:`iana`,extensions:[`psb`]},"application/vnd.3gpp.pic-bw-var":{source:`iana`,extensions:[`pvb`]},"application/vnd.3gpp.pinapp-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.s1ap":{source:`iana`},"application/vnd.3gpp.seal-group-doc+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.seal-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.seal-location-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.seal-mbms-usage-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.seal-network-qos-management-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.seal-ue-config-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.seal-unicast-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.seal-user-profile-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.sms":{source:`iana`},"application/vnd.3gpp.sms+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.srvcc-ext+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.srvcc-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.state-and-event-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.ussd+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp.v2x":{source:`iana`},"application/vnd.3gpp.vae-info+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp2.bcmcsinfo+xml":{source:`iana`,compressible:!0},"application/vnd.3gpp2.sms":{source:`iana`},"application/vnd.3gpp2.tcap":{source:`iana`,extensions:[`tcap`]},"application/vnd.3lightssoftware.imagescal":{source:`iana`},"application/vnd.3m.post-it-notes":{source:`iana`,extensions:[`pwn`]},"application/vnd.accpac.simply.aso":{source:`iana`,extensions:[`aso`]},"application/vnd.accpac.simply.imp":{source:`iana`,extensions:[`imp`]},"application/vnd.acm.addressxfer+json":{source:`iana`,compressible:!0},"application/vnd.acm.chatbot+json":{source:`iana`,compressible:!0},"application/vnd.acucobol":{source:`iana`,extensions:[`acu`]},"application/vnd.acucorp":{source:`iana`,extensions:[`atc`,`acutc`]},"application/vnd.adobe.air-application-installer-package+zip":{source:`apache`,compressible:!1,extensions:[`air`]},"application/vnd.adobe.flash.movie":{source:`iana`},"application/vnd.adobe.formscentral.fcdt":{source:`iana`,extensions:[`fcdt`]},"application/vnd.adobe.fxp":{source:`iana`,extensions:[`fxp`,`fxpl`]},"application/vnd.adobe.partial-upload":{source:`iana`},"application/vnd.adobe.xdp+xml":{source:`iana`,compressible:!0,extensions:[`xdp`]},"application/vnd.adobe.xfdf":{source:`apache`,extensions:[`xfdf`]},"application/vnd.aether.imp":{source:`iana`},"application/vnd.afpc.afplinedata":{source:`iana`},"application/vnd.afpc.afplinedata-pagedef":{source:`iana`},"application/vnd.afpc.cmoca-cmresource":{source:`iana`},"application/vnd.afpc.foca-charset":{source:`iana`},"application/vnd.afpc.foca-codedfont":{source:`iana`},"application/vnd.afpc.foca-codepage":{source:`iana`},"application/vnd.afpc.modca":{source:`iana`},"application/vnd.afpc.modca-cmtable":{source:`iana`},"application/vnd.afpc.modca-formdef":{source:`iana`},"application/vnd.afpc.modca-mediummap":{source:`iana`},"application/vnd.afpc.modca-objectcontainer":{source:`iana`},"application/vnd.afpc.modca-overlay":{source:`iana`},"application/vnd.afpc.modca-pagesegment":{source:`iana`},"application/vnd.age":{source:`iana`,extensions:[`age`]},"application/vnd.ah-barcode":{source:`apache`},"application/vnd.ahead.space":{source:`iana`,extensions:[`ahead`]},"application/vnd.airzip.filesecure.azf":{source:`iana`,extensions:[`azf`]},"application/vnd.airzip.filesecure.azs":{source:`iana`,extensions:[`azs`]},"application/vnd.amadeus+json":{source:`iana`,compressible:!0},"application/vnd.amazon.ebook":{source:`apache`,extensions:[`azw`]},"application/vnd.amazon.mobi8-ebook":{source:`iana`},"application/vnd.americandynamics.acc":{source:`iana`,extensions:[`acc`]},"application/vnd.amiga.ami":{source:`iana`,extensions:[`ami`]},"application/vnd.amundsen.maze+xml":{source:`iana`,compressible:!0},"application/vnd.android.ota":{source:`iana`},"application/vnd.android.package-archive":{source:`apache`,compressible:!1,extensions:[`apk`]},"application/vnd.anki":{source:`iana`},"application/vnd.anser-web-certificate-issue-initiation":{source:`iana`,extensions:[`cii`]},"application/vnd.anser-web-funds-transfer-initiation":{source:`apache`,extensions:[`fti`]},"application/vnd.antix.game-component":{source:`iana`,extensions:[`atx`]},"application/vnd.apache.arrow.file":{source:`iana`},"application/vnd.apache.arrow.stream":{source:`iana`},"application/vnd.apache.parquet":{source:`iana`},"application/vnd.apache.thrift.binary":{source:`iana`},"application/vnd.apache.thrift.compact":{source:`iana`},"application/vnd.apache.thrift.json":{source:`iana`},"application/vnd.apexlang":{source:`iana`},"application/vnd.api+json":{source:`iana`,compressible:!0},"application/vnd.aplextor.warrp+json":{source:`iana`,compressible:!0},"application/vnd.apothekende.reservation+json":{source:`iana`,compressible:!0},"application/vnd.apple.installer+xml":{source:`iana`,compressible:!0,extensions:[`mpkg`]},"application/vnd.apple.keynote":{source:`iana`,extensions:[`key`]},"application/vnd.apple.mpegurl":{source:`iana`,extensions:[`m3u8`]},"application/vnd.apple.numbers":{source:`iana`,extensions:[`numbers`]},"application/vnd.apple.pages":{source:`iana`,extensions:[`pages`]},"application/vnd.apple.pkpass":{compressible:!1,extensions:[`pkpass`]},"application/vnd.arastra.swi":{source:`apache`},"application/vnd.aristanetworks.swi":{source:`iana`,extensions:[`swi`]},"application/vnd.artisan+json":{source:`iana`,compressible:!0},"application/vnd.artsquare":{source:`iana`},"application/vnd.astraea-software.iota":{source:`iana`,extensions:[`iota`]},"application/vnd.audiograph":{source:`iana`,extensions:[`aep`]},"application/vnd.autodesk.fbx":{extensions:[`fbx`]},"application/vnd.autopackage":{source:`iana`},"application/vnd.avalon+json":{source:`iana`,compressible:!0},"application/vnd.avistar+xml":{source:`iana`,compressible:!0},"application/vnd.balsamiq.bmml+xml":{source:`iana`,compressible:!0,extensions:[`bmml`]},"application/vnd.balsamiq.bmpr":{source:`iana`},"application/vnd.banana-accounting":{source:`iana`},"application/vnd.bbf.usp.error":{source:`iana`},"application/vnd.bbf.usp.msg":{source:`iana`},"application/vnd.bbf.usp.msg+json":{source:`iana`,compressible:!0},"application/vnd.bekitzur-stech+json":{source:`iana`,compressible:!0},"application/vnd.belightsoft.lhzd+zip":{source:`iana`,compressible:!1},"application/vnd.belightsoft.lhzl+zip":{source:`iana`,compressible:!1},"application/vnd.bint.med-content":{source:`iana`},"application/vnd.biopax.rdf+xml":{source:`iana`,compressible:!0},"application/vnd.blink-idb-value-wrapper":{source:`iana`},"application/vnd.blueice.multipass":{source:`iana`,extensions:[`mpm`]},"application/vnd.bluetooth.ep.oob":{source:`iana`},"application/vnd.bluetooth.le.oob":{source:`iana`},"application/vnd.bmi":{source:`iana`,extensions:[`bmi`]},"application/vnd.bpf":{source:`iana`},"application/vnd.bpf3":{source:`iana`},"application/vnd.businessobjects":{source:`iana`,extensions:[`rep`]},"application/vnd.byu.uapi+json":{source:`iana`,compressible:!0},"application/vnd.bzip3":{source:`iana`},"application/vnd.c3voc.schedule+xml":{source:`iana`,compressible:!0},"application/vnd.cab-jscript":{source:`iana`},"application/vnd.canon-cpdl":{source:`iana`},"application/vnd.canon-lips":{source:`iana`},"application/vnd.capasystems-pg+json":{source:`iana`,compressible:!0},"application/vnd.cendio.thinlinc.clientconf":{source:`iana`},"application/vnd.century-systems.tcp_stream":{source:`iana`},"application/vnd.chemdraw+xml":{source:`iana`,compressible:!0,extensions:[`cdxml`]},"application/vnd.chess-pgn":{source:`iana`},"application/vnd.chipnuts.karaoke-mmd":{source:`iana`,extensions:[`mmd`]},"application/vnd.ciedi":{source:`iana`},"application/vnd.cinderella":{source:`iana`,extensions:[`cdy`]},"application/vnd.cirpack.isdn-ext":{source:`iana`},"application/vnd.citationstyles.style+xml":{source:`iana`,compressible:!0,extensions:[`csl`]},"application/vnd.claymore":{source:`iana`,extensions:[`cla`]},"application/vnd.cloanto.rp9":{source:`iana`,extensions:[`rp9`]},"application/vnd.clonk.c4group":{source:`iana`,extensions:[`c4g`,`c4d`,`c4f`,`c4p`,`c4u`]},"application/vnd.cluetrust.cartomobile-config":{source:`iana`,extensions:[`c11amc`]},"application/vnd.cluetrust.cartomobile-config-pkg":{source:`iana`,extensions:[`c11amz`]},"application/vnd.cncf.helm.chart.content.v1.tar+gzip":{source:`iana`},"application/vnd.cncf.helm.chart.provenance.v1.prov":{source:`iana`},"application/vnd.cncf.helm.config.v1+json":{source:`iana`,compressible:!0},"application/vnd.coffeescript":{source:`iana`},"application/vnd.collabio.xodocuments.document":{source:`iana`},"application/vnd.collabio.xodocuments.document-template":{source:`iana`},"application/vnd.collabio.xodocuments.presentation":{source:`iana`},"application/vnd.collabio.xodocuments.presentation-template":{source:`iana`},"application/vnd.collabio.xodocuments.spreadsheet":{source:`iana`},"application/vnd.collabio.xodocuments.spreadsheet-template":{source:`iana`},"application/vnd.collection+json":{source:`iana`,compressible:!0},"application/vnd.collection.doc+json":{source:`iana`,compressible:!0},"application/vnd.collection.next+json":{source:`iana`,compressible:!0},"application/vnd.comicbook+zip":{source:`iana`,compressible:!1},"application/vnd.comicbook-rar":{source:`iana`},"application/vnd.commerce-battelle":{source:`iana`},"application/vnd.commonspace":{source:`iana`,extensions:[`csp`]},"application/vnd.contact.cmsg":{source:`iana`,extensions:[`cdbcmsg`]},"application/vnd.coreos.ignition+json":{source:`iana`,compressible:!0},"application/vnd.cosmocaller":{source:`iana`,extensions:[`cmc`]},"application/vnd.crick.clicker":{source:`iana`,extensions:[`clkx`]},"application/vnd.crick.clicker.keyboard":{source:`iana`,extensions:[`clkk`]},"application/vnd.crick.clicker.palette":{source:`iana`,extensions:[`clkp`]},"application/vnd.crick.clicker.template":{source:`iana`,extensions:[`clkt`]},"application/vnd.crick.clicker.wordbank":{source:`iana`,extensions:[`clkw`]},"application/vnd.criticaltools.wbs+xml":{source:`iana`,compressible:!0,extensions:[`wbs`]},"application/vnd.cryptii.pipe+json":{source:`iana`,compressible:!0},"application/vnd.crypto-shade-file":{source:`iana`},"application/vnd.cryptomator.encrypted":{source:`iana`},"application/vnd.cryptomator.vault":{source:`iana`},"application/vnd.ctc-posml":{source:`iana`,extensions:[`pml`]},"application/vnd.ctct.ws+xml":{source:`iana`,compressible:!0},"application/vnd.cups-pdf":{source:`iana`},"application/vnd.cups-postscript":{source:`iana`},"application/vnd.cups-ppd":{source:`iana`,extensions:[`ppd`]},"application/vnd.cups-raster":{source:`iana`},"application/vnd.cups-raw":{source:`iana`},"application/vnd.curl":{source:`iana`},"application/vnd.curl.car":{source:`apache`,extensions:[`car`]},"application/vnd.curl.pcurl":{source:`apache`,extensions:[`pcurl`]},"application/vnd.cyan.dean.root+xml":{source:`iana`,compressible:!0},"application/vnd.cybank":{source:`iana`},"application/vnd.cyclonedx+json":{source:`iana`,compressible:!0},"application/vnd.cyclonedx+xml":{source:`iana`,compressible:!0},"application/vnd.d2l.coursepackage1p0+zip":{source:`iana`,compressible:!1},"application/vnd.d3m-dataset":{source:`iana`},"application/vnd.d3m-problem":{source:`iana`},"application/vnd.dart":{source:`iana`,compressible:!0,extensions:[`dart`]},"application/vnd.data-vision.rdz":{source:`iana`,extensions:[`rdz`]},"application/vnd.datalog":{source:`iana`},"application/vnd.datapackage+json":{source:`iana`,compressible:!0},"application/vnd.dataresource+json":{source:`iana`,compressible:!0},"application/vnd.dbf":{source:`iana`,extensions:[`dbf`]},"application/vnd.dcmp+xml":{source:`iana`,compressible:!0,extensions:[`dcmp`]},"application/vnd.debian.binary-package":{source:`iana`},"application/vnd.dece.data":{source:`iana`,extensions:[`uvf`,`uvvf`,`uvd`,`uvvd`]},"application/vnd.dece.ttml+xml":{source:`iana`,compressible:!0,extensions:[`uvt`,`uvvt`]},"application/vnd.dece.unspecified":{source:`iana`,extensions:[`uvx`,`uvvx`]},"application/vnd.dece.zip":{source:`iana`,extensions:[`uvz`,`uvvz`]},"application/vnd.denovo.fcselayout-link":{source:`iana`,extensions:[`fe_launch`]},"application/vnd.desmume.movie":{source:`iana`},"application/vnd.dir-bi.plate-dl-nosuffix":{source:`iana`},"application/vnd.dm.delegation+xml":{source:`iana`,compressible:!0},"application/vnd.dna":{source:`iana`,extensions:[`dna`]},"application/vnd.document+json":{source:`iana`,compressible:!0},"application/vnd.dolby.mlp":{source:`apache`,extensions:[`mlp`]},"application/vnd.dolby.mobile.1":{source:`iana`},"application/vnd.dolby.mobile.2":{source:`iana`},"application/vnd.doremir.scorecloud-binary-document":{source:`iana`},"application/vnd.dpgraph":{source:`iana`,extensions:[`dpg`]},"application/vnd.dreamfactory":{source:`iana`,extensions:[`dfac`]},"application/vnd.drive+json":{source:`iana`,compressible:!0},"application/vnd.ds-keypoint":{source:`apache`,extensions:[`kpxx`]},"application/vnd.dtg.local":{source:`iana`},"application/vnd.dtg.local.flash":{source:`iana`},"application/vnd.dtg.local.html":{source:`iana`},"application/vnd.dvb.ait":{source:`iana`,extensions:[`ait`]},"application/vnd.dvb.dvbisl+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.dvbj":{source:`iana`},"application/vnd.dvb.esgcontainer":{source:`iana`},"application/vnd.dvb.ipdcdftnotifaccess":{source:`iana`},"application/vnd.dvb.ipdcesgaccess":{source:`iana`},"application/vnd.dvb.ipdcesgaccess2":{source:`iana`},"application/vnd.dvb.ipdcesgpdd":{source:`iana`},"application/vnd.dvb.ipdcroaming":{source:`iana`},"application/vnd.dvb.iptv.alfec-base":{source:`iana`},"application/vnd.dvb.iptv.alfec-enhancement":{source:`iana`},"application/vnd.dvb.notif-aggregate-root+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.notif-container+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.notif-generic+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.notif-ia-msglist+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.notif-ia-registration-request+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.notif-ia-registration-response+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.notif-init+xml":{source:`iana`,compressible:!0},"application/vnd.dvb.pfr":{source:`iana`},"application/vnd.dvb.service":{source:`iana`,extensions:[`svc`]},"application/vnd.dxr":{source:`iana`},"application/vnd.dynageo":{source:`iana`,extensions:[`geo`]},"application/vnd.dzr":{source:`iana`},"application/vnd.easykaraoke.cdgdownload":{source:`iana`},"application/vnd.ecdis-update":{source:`iana`},"application/vnd.ecip.rlp":{source:`iana`},"application/vnd.eclipse.ditto+json":{source:`iana`,compressible:!0},"application/vnd.ecowin.chart":{source:`iana`,extensions:[`mag`]},"application/vnd.ecowin.filerequest":{source:`iana`},"application/vnd.ecowin.fileupdate":{source:`iana`},"application/vnd.ecowin.series":{source:`iana`},"application/vnd.ecowin.seriesrequest":{source:`iana`},"application/vnd.ecowin.seriesupdate":{source:`iana`},"application/vnd.efi.img":{source:`iana`},"application/vnd.efi.iso":{source:`iana`},"application/vnd.eln+zip":{source:`iana`,compressible:!1},"application/vnd.emclient.accessrequest+xml":{source:`iana`,compressible:!0},"application/vnd.enliven":{source:`iana`,extensions:[`nml`]},"application/vnd.enphase.envoy":{source:`iana`},"application/vnd.eprints.data+xml":{source:`iana`,compressible:!0},"application/vnd.epson.esf":{source:`iana`,extensions:[`esf`]},"application/vnd.epson.msf":{source:`iana`,extensions:[`msf`]},"application/vnd.epson.quickanime":{source:`iana`,extensions:[`qam`]},"application/vnd.epson.salt":{source:`iana`,extensions:[`slt`]},"application/vnd.epson.ssf":{source:`iana`,extensions:[`ssf`]},"application/vnd.ericsson.quickcall":{source:`iana`},"application/vnd.erofs":{source:`iana`},"application/vnd.espass-espass+zip":{source:`iana`,compressible:!1},"application/vnd.eszigno3+xml":{source:`iana`,compressible:!0,extensions:[`es3`,`et3`]},"application/vnd.etsi.aoc+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.asic-e+zip":{source:`iana`,compressible:!1},"application/vnd.etsi.asic-s+zip":{source:`iana`,compressible:!1},"application/vnd.etsi.cug+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvcommand+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvdiscovery+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvprofile+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvsad-bc+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvsad-cod+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvsad-npvr+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvservice+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvsync+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.iptvueprofile+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.mcid+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.mheg5":{source:`iana`},"application/vnd.etsi.overload-control-policy-dataset+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.pstn+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.sci+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.simservs+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.timestamp-token":{source:`iana`},"application/vnd.etsi.tsl+xml":{source:`iana`,compressible:!0},"application/vnd.etsi.tsl.der":{source:`iana`},"application/vnd.eu.kasparian.car+json":{source:`iana`,compressible:!0},"application/vnd.eudora.data":{source:`iana`},"application/vnd.evolv.ecig.profile":{source:`iana`},"application/vnd.evolv.ecig.settings":{source:`iana`},"application/vnd.evolv.ecig.theme":{source:`iana`},"application/vnd.exstream-empower+zip":{source:`iana`,compressible:!1},"application/vnd.exstream-package":{source:`iana`},"application/vnd.ezpix-album":{source:`iana`,extensions:[`ez2`]},"application/vnd.ezpix-package":{source:`iana`,extensions:[`ez3`]},"application/vnd.f-secure.mobile":{source:`iana`},"application/vnd.familysearch.gedcom+zip":{source:`iana`,compressible:!1},"application/vnd.fastcopy-disk-image":{source:`iana`},"application/vnd.fdf":{source:`apache`,extensions:[`fdf`]},"application/vnd.fdsn.mseed":{source:`iana`,extensions:[`mseed`]},"application/vnd.fdsn.seed":{source:`iana`,extensions:[`seed`,`dataless`]},"application/vnd.fdsn.stationxml+xml":{source:`iana`,charset:`XML-BASED`,compressible:!0},"application/vnd.ffsns":{source:`iana`},"application/vnd.ficlab.flb+zip":{source:`iana`,compressible:!1},"application/vnd.filmit.zfc":{source:`iana`},"application/vnd.fints":{source:`iana`},"application/vnd.firemonkeys.cloudcell":{source:`iana`},"application/vnd.flographit":{source:`iana`,extensions:[`gph`]},"application/vnd.fluxtime.clip":{source:`iana`,extensions:[`ftc`]},"application/vnd.font-fontforge-sfd":{source:`iana`},"application/vnd.framemaker":{source:`iana`,extensions:[`fm`,`frame`,`maker`,`book`]},"application/vnd.freelog.comic":{source:`iana`},"application/vnd.frogans.fnc":{source:`apache`,extensions:[`fnc`]},"application/vnd.frogans.ltf":{source:`apache`,extensions:[`ltf`]},"application/vnd.fsc.weblaunch":{source:`iana`,extensions:[`fsc`]},"application/vnd.fujifilm.fb.docuworks":{source:`iana`},"application/vnd.fujifilm.fb.docuworks.binder":{source:`iana`},"application/vnd.fujifilm.fb.docuworks.container":{source:`iana`},"application/vnd.fujifilm.fb.jfi+xml":{source:`iana`,compressible:!0},"application/vnd.fujitsu.oasys":{source:`iana`,extensions:[`oas`]},"application/vnd.fujitsu.oasys2":{source:`iana`,extensions:[`oa2`]},"application/vnd.fujitsu.oasys3":{source:`iana`,extensions:[`oa3`]},"application/vnd.fujitsu.oasysgp":{source:`iana`,extensions:[`fg5`]},"application/vnd.fujitsu.oasysprs":{source:`iana`,extensions:[`bh2`]},"application/vnd.fujixerox.art-ex":{source:`iana`},"application/vnd.fujixerox.art4":{source:`iana`},"application/vnd.fujixerox.ddd":{source:`iana`,extensions:[`ddd`]},"application/vnd.fujixerox.docuworks":{source:`iana`,extensions:[`xdw`]},"application/vnd.fujixerox.docuworks.binder":{source:`iana`,extensions:[`xbd`]},"application/vnd.fujixerox.docuworks.container":{source:`iana`},"application/vnd.fujixerox.hbpl":{source:`iana`},"application/vnd.fut-misnet":{source:`iana`},"application/vnd.futoin+cbor":{source:`iana`},"application/vnd.futoin+json":{source:`iana`,compressible:!0},"application/vnd.fuzzysheet":{source:`iana`,extensions:[`fzs`]},"application/vnd.ga4gh.passport+jwt":{source:`iana`},"application/vnd.genomatix.tuxedo":{source:`iana`,extensions:[`txd`]},"application/vnd.genozip":{source:`iana`},"application/vnd.gentics.grd+json":{source:`iana`,compressible:!0},"application/vnd.gentoo.catmetadata+xml":{source:`iana`,compressible:!0},"application/vnd.gentoo.ebuild":{source:`iana`},"application/vnd.gentoo.eclass":{source:`iana`},"application/vnd.gentoo.gpkg":{source:`iana`},"application/vnd.gentoo.manifest":{source:`iana`},"application/vnd.gentoo.pkgmetadata+xml":{source:`iana`,compressible:!0},"application/vnd.gentoo.xpak":{source:`iana`},"application/vnd.geo+json":{source:`apache`,compressible:!0},"application/vnd.geocube+xml":{source:`apache`,compressible:!0},"application/vnd.geogebra.file":{source:`iana`,extensions:[`ggb`]},"application/vnd.geogebra.pinboard":{source:`iana`},"application/vnd.geogebra.slides":{source:`iana`,extensions:[`ggs`]},"application/vnd.geogebra.tool":{source:`iana`,extensions:[`ggt`]},"application/vnd.geometry-explorer":{source:`iana`,extensions:[`gex`,`gre`]},"application/vnd.geonext":{source:`iana`,extensions:[`gxt`]},"application/vnd.geoplan":{source:`iana`,extensions:[`g2w`]},"application/vnd.geospace":{source:`iana`,extensions:[`g3w`]},"application/vnd.gerber":{source:`iana`},"application/vnd.globalplatform.card-content-mgt":{source:`iana`},"application/vnd.globalplatform.card-content-mgt-response":{source:`iana`},"application/vnd.gmx":{source:`iana`,extensions:[`gmx`]},"application/vnd.gnu.taler.exchange+json":{source:`iana`,compressible:!0},"application/vnd.gnu.taler.merchant+json":{source:`iana`,compressible:!0},"application/vnd.google-apps.audio":{},"application/vnd.google-apps.document":{compressible:!1,extensions:[`gdoc`]},"application/vnd.google-apps.drawing":{compressible:!1,extensions:[`gdraw`]},"application/vnd.google-apps.drive-sdk":{compressible:!1},"application/vnd.google-apps.file":{},"application/vnd.google-apps.folder":{compressible:!1},"application/vnd.google-apps.form":{compressible:!1,extensions:[`gform`]},"application/vnd.google-apps.fusiontable":{},"application/vnd.google-apps.jam":{compressible:!1,extensions:[`gjam`]},"application/vnd.google-apps.mail-layout":{},"application/vnd.google-apps.map":{compressible:!1,extensions:[`gmap`]},"application/vnd.google-apps.photo":{},"application/vnd.google-apps.presentation":{compressible:!1,extensions:[`gslides`]},"application/vnd.google-apps.script":{compressible:!1,extensions:[`gscript`]},"application/vnd.google-apps.shortcut":{},"application/vnd.google-apps.site":{compressible:!1,extensions:[`gsite`]},"application/vnd.google-apps.spreadsheet":{compressible:!1,extensions:[`gsheet`]},"application/vnd.google-apps.unknown":{},"application/vnd.google-apps.video":{},"application/vnd.google-earth.kml+xml":{source:`iana`,compressible:!0,extensions:[`kml`]},"application/vnd.google-earth.kmz":{source:`iana`,compressible:!1,extensions:[`kmz`]},"application/vnd.gov.sk.e-form+xml":{source:`apache`,compressible:!0},"application/vnd.gov.sk.e-form+zip":{source:`iana`,compressible:!1},"application/vnd.gov.sk.xmldatacontainer+xml":{source:`iana`,compressible:!0,extensions:[`xdcf`]},"application/vnd.gpxsee.map+xml":{source:`iana`,compressible:!0},"application/vnd.grafeq":{source:`iana`,extensions:[`gqf`,`gqs`]},"application/vnd.gridmp":{source:`iana`},"application/vnd.groove-account":{source:`iana`,extensions:[`gac`]},"application/vnd.groove-help":{source:`iana`,extensions:[`ghf`]},"application/vnd.groove-identity-message":{source:`iana`,extensions:[`gim`]},"application/vnd.groove-injector":{source:`iana`,extensions:[`grv`]},"application/vnd.groove-tool-message":{source:`iana`,extensions:[`gtm`]},"application/vnd.groove-tool-template":{source:`iana`,extensions:[`tpl`]},"application/vnd.groove-vcard":{source:`iana`,extensions:[`vcg`]},"application/vnd.hal+json":{source:`iana`,compressible:!0},"application/vnd.hal+xml":{source:`iana`,compressible:!0,extensions:[`hal`]},"application/vnd.handheld-entertainment+xml":{source:`iana`,compressible:!0,extensions:[`zmm`]},"application/vnd.hbci":{source:`iana`,extensions:[`hbci`]},"application/vnd.hc+json":{source:`iana`,compressible:!0},"application/vnd.hcl-bireports":{source:`iana`},"application/vnd.hdt":{source:`iana`},"application/vnd.heroku+json":{source:`iana`,compressible:!0},"application/vnd.hhe.lesson-player":{source:`iana`,extensions:[`les`]},"application/vnd.hp-hpgl":{source:`iana`,extensions:[`hpgl`]},"application/vnd.hp-hpid":{source:`iana`,extensions:[`hpid`]},"application/vnd.hp-hps":{source:`iana`,extensions:[`hps`]},"application/vnd.hp-jlyt":{source:`iana`,extensions:[`jlt`]},"application/vnd.hp-pcl":{source:`iana`,extensions:[`pcl`]},"application/vnd.hp-pclxl":{source:`iana`,extensions:[`pclxl`]},"application/vnd.hsl":{source:`iana`},"application/vnd.httphone":{source:`iana`},"application/vnd.hydrostatix.sof-data":{source:`iana`,extensions:[`sfd-hdstx`]},"application/vnd.hyper+json":{source:`iana`,compressible:!0},"application/vnd.hyper-item+json":{source:`iana`,compressible:!0},"application/vnd.hyperdrive+json":{source:`iana`,compressible:!0},"application/vnd.hzn-3d-crossword":{source:`iana`},"application/vnd.ibm.afplinedata":{source:`apache`},"application/vnd.ibm.electronic-media":{source:`iana`},"application/vnd.ibm.minipay":{source:`iana`,extensions:[`mpy`]},"application/vnd.ibm.modcap":{source:`apache`,extensions:[`afp`,`listafp`,`list3820`]},"application/vnd.ibm.rights-management":{source:`iana`,extensions:[`irm`]},"application/vnd.ibm.secure-container":{source:`iana`,extensions:[`sc`]},"application/vnd.iccprofile":{source:`iana`,extensions:[`icc`,`icm`]},"application/vnd.ieee.1905":{source:`iana`},"application/vnd.igloader":{source:`iana`,extensions:[`igl`]},"application/vnd.imagemeter.folder+zip":{source:`iana`,compressible:!1},"application/vnd.imagemeter.image+zip":{source:`iana`,compressible:!1},"application/vnd.immervision-ivp":{source:`iana`,extensions:[`ivp`]},"application/vnd.immervision-ivu":{source:`iana`,extensions:[`ivu`]},"application/vnd.ims.imsccv1p1":{source:`iana`},"application/vnd.ims.imsccv1p2":{source:`iana`},"application/vnd.ims.imsccv1p3":{source:`iana`},"application/vnd.ims.lis.v2.result+json":{source:`iana`,compressible:!0},"application/vnd.ims.lti.v2.toolconsumerprofile+json":{source:`iana`,compressible:!0},"application/vnd.ims.lti.v2.toolproxy+json":{source:`iana`,compressible:!0},"application/vnd.ims.lti.v2.toolproxy.id+json":{source:`iana`,compressible:!0},"application/vnd.ims.lti.v2.toolsettings+json":{source:`iana`,compressible:!0},"application/vnd.ims.lti.v2.toolsettings.simple+json":{source:`iana`,compressible:!0},"application/vnd.informedcontrol.rms+xml":{source:`iana`,compressible:!0},"application/vnd.informix-visionary":{source:`apache`},"application/vnd.infotech.project":{source:`iana`},"application/vnd.infotech.project+xml":{source:`iana`,compressible:!0},"application/vnd.innopath.wamp.notification":{source:`iana`},"application/vnd.insors.igm":{source:`iana`,extensions:[`igm`]},"application/vnd.intercon.formnet":{source:`iana`,extensions:[`xpw`,`xpx`]},"application/vnd.intergeo":{source:`iana`,extensions:[`i2g`]},"application/vnd.intertrust.digibox":{source:`iana`},"application/vnd.intertrust.nncp":{source:`iana`},"application/vnd.intu.qbo":{source:`iana`,extensions:[`qbo`]},"application/vnd.intu.qfx":{source:`iana`,extensions:[`qfx`]},"application/vnd.ipfs.ipns-record":{source:`iana`},"application/vnd.ipld.car":{source:`iana`},"application/vnd.ipld.dag-cbor":{source:`iana`},"application/vnd.ipld.dag-json":{source:`iana`},"application/vnd.ipld.raw":{source:`iana`},"application/vnd.iptc.g2.catalogitem+xml":{source:`iana`,compressible:!0},"application/vnd.iptc.g2.conceptitem+xml":{source:`iana`,compressible:!0},"application/vnd.iptc.g2.knowledgeitem+xml":{source:`iana`,compressible:!0},"application/vnd.iptc.g2.newsitem+xml":{source:`iana`,compressible:!0},"application/vnd.iptc.g2.newsmessage+xml":{source:`iana`,compressible:!0},"application/vnd.iptc.g2.packageitem+xml":{source:`iana`,compressible:!0},"application/vnd.iptc.g2.planningitem+xml":{source:`iana`,compressible:!0},"application/vnd.ipunplugged.rcprofile":{source:`iana`,extensions:[`rcprofile`]},"application/vnd.irepository.package+xml":{source:`iana`,compressible:!0,extensions:[`irp`]},"application/vnd.is-xpr":{source:`iana`,extensions:[`xpr`]},"application/vnd.isac.fcs":{source:`iana`,extensions:[`fcs`]},"application/vnd.iso11783-10+zip":{source:`iana`,compressible:!1},"application/vnd.jam":{source:`iana`,extensions:[`jam`]},"application/vnd.japannet-directory-service":{source:`iana`},"application/vnd.japannet-jpnstore-wakeup":{source:`iana`},"application/vnd.japannet-payment-wakeup":{source:`iana`},"application/vnd.japannet-registration":{source:`iana`},"application/vnd.japannet-registration-wakeup":{source:`iana`},"application/vnd.japannet-setstore-wakeup":{source:`iana`},"application/vnd.japannet-verification":{source:`iana`},"application/vnd.japannet-verification-wakeup":{source:`iana`},"application/vnd.jcp.javame.midlet-rms":{source:`iana`,extensions:[`rms`]},"application/vnd.jisp":{source:`iana`,extensions:[`jisp`]},"application/vnd.joost.joda-archive":{source:`iana`,extensions:[`joda`]},"application/vnd.jsk.isdn-ngn":{source:`iana`},"application/vnd.kahootz":{source:`iana`,extensions:[`ktz`,`ktr`]},"application/vnd.kde.karbon":{source:`iana`,extensions:[`karbon`]},"application/vnd.kde.kchart":{source:`iana`,extensions:[`chrt`]},"application/vnd.kde.kformula":{source:`iana`,extensions:[`kfo`]},"application/vnd.kde.kivio":{source:`iana`,extensions:[`flw`]},"application/vnd.kde.kontour":{source:`iana`,extensions:[`kon`]},"application/vnd.kde.kpresenter":{source:`iana`,extensions:[`kpr`,`kpt`]},"application/vnd.kde.kspread":{source:`iana`,extensions:[`ksp`]},"application/vnd.kde.kword":{source:`iana`,extensions:[`kwd`,`kwt`]},"application/vnd.kdl":{source:`iana`},"application/vnd.kenameaapp":{source:`iana`,extensions:[`htke`]},"application/vnd.keyman.kmp+zip":{source:`iana`,compressible:!1},"application/vnd.keyman.kmx":{source:`iana`},"application/vnd.kidspiration":{source:`iana`,extensions:[`kia`]},"application/vnd.kinar":{source:`iana`,extensions:[`kne`,`knp`]},"application/vnd.koan":{source:`iana`,extensions:[`skp`,`skd`,`skt`,`skm`]},"application/vnd.kodak-descriptor":{source:`iana`,extensions:[`sse`]},"application/vnd.las":{source:`iana`},"application/vnd.las.las+json":{source:`iana`,compressible:!0},"application/vnd.las.las+xml":{source:`iana`,compressible:!0,extensions:[`lasxml`]},"application/vnd.laszip":{source:`iana`},"application/vnd.ldev.productlicensing":{source:`iana`},"application/vnd.leap+json":{source:`iana`,compressible:!0},"application/vnd.liberty-request+xml":{source:`iana`,compressible:!0},"application/vnd.llamagraphics.life-balance.desktop":{source:`iana`,extensions:[`lbd`]},"application/vnd.llamagraphics.life-balance.exchange+xml":{source:`iana`,compressible:!0,extensions:[`lbe`]},"application/vnd.logipipe.circuit+zip":{source:`iana`,compressible:!1},"application/vnd.loom":{source:`iana`},"application/vnd.lotus-1-2-3":{source:`iana`,extensions:[`123`]},"application/vnd.lotus-approach":{source:`iana`,extensions:[`apr`]},"application/vnd.lotus-freelance":{source:`iana`,extensions:[`pre`]},"application/vnd.lotus-notes":{source:`iana`,extensions:[`nsf`]},"application/vnd.lotus-organizer":{source:`iana`,extensions:[`org`]},"application/vnd.lotus-screencam":{source:`iana`,extensions:[`scm`]},"application/vnd.lotus-wordpro":{source:`iana`,extensions:[`lwp`]},"application/vnd.macports.portpkg":{source:`iana`,extensions:[`portpkg`]},"application/vnd.mapbox-vector-tile":{source:`iana`,extensions:[`mvt`]},"application/vnd.marlin.drm.actiontoken+xml":{source:`iana`,compressible:!0},"application/vnd.marlin.drm.conftoken+xml":{source:`iana`,compressible:!0},"application/vnd.marlin.drm.license+xml":{source:`iana`,compressible:!0},"application/vnd.marlin.drm.mdcf":{source:`iana`},"application/vnd.mason+json":{source:`iana`,compressible:!0},"application/vnd.maxar.archive.3tz+zip":{source:`iana`,compressible:!1},"application/vnd.maxmind.maxmind-db":{source:`iana`},"application/vnd.mcd":{source:`iana`,extensions:[`mcd`]},"application/vnd.mdl":{source:`iana`},"application/vnd.mdl-mbsdf":{source:`iana`},"application/vnd.medcalcdata":{source:`iana`,extensions:[`mc1`]},"application/vnd.mediastation.cdkey":{source:`iana`,extensions:[`cdkey`]},"application/vnd.medicalholodeck.recordxr":{source:`iana`},"application/vnd.meridian-slingshot":{source:`iana`},"application/vnd.mermaid":{source:`iana`},"application/vnd.mfer":{source:`iana`,extensions:[`mwf`]},"application/vnd.mfmp":{source:`iana`,extensions:[`mfm`]},"application/vnd.micro+json":{source:`iana`,compressible:!0},"application/vnd.micrografx.flo":{source:`iana`,extensions:[`flo`]},"application/vnd.micrografx.igx":{source:`iana`,extensions:[`igx`]},"application/vnd.microsoft.portable-executable":{source:`iana`},"application/vnd.microsoft.windows.thumbnail-cache":{source:`iana`},"application/vnd.miele+json":{source:`iana`,compressible:!0},"application/vnd.mif":{source:`iana`,extensions:[`mif`]},"application/vnd.minisoft-hp3000-save":{source:`iana`},"application/vnd.mitsubishi.misty-guard.trustweb":{source:`iana`},"application/vnd.mobius.daf":{source:`iana`,extensions:[`daf`]},"application/vnd.mobius.dis":{source:`iana`,extensions:[`dis`]},"application/vnd.mobius.mbk":{source:`iana`,extensions:[`mbk`]},"application/vnd.mobius.mqy":{source:`iana`,extensions:[`mqy`]},"application/vnd.mobius.msl":{source:`iana`,extensions:[`msl`]},"application/vnd.mobius.plc":{source:`iana`,extensions:[`plc`]},"application/vnd.mobius.txf":{source:`iana`,extensions:[`txf`]},"application/vnd.modl":{source:`iana`},"application/vnd.mophun.application":{source:`iana`,extensions:[`mpn`]},"application/vnd.mophun.certificate":{source:`iana`,extensions:[`mpc`]},"application/vnd.motorola.flexsuite":{source:`iana`},"application/vnd.motorola.flexsuite.adsi":{source:`iana`},"application/vnd.motorola.flexsuite.fis":{source:`iana`},"application/vnd.motorola.flexsuite.gotap":{source:`iana`},"application/vnd.motorola.flexsuite.kmr":{source:`iana`},"application/vnd.motorola.flexsuite.ttc":{source:`iana`},"application/vnd.motorola.flexsuite.wem":{source:`iana`},"application/vnd.motorola.iprm":{source:`iana`},"application/vnd.mozilla.xul+xml":{source:`iana`,compressible:!0,extensions:[`xul`]},"application/vnd.ms-3mfdocument":{source:`iana`},"application/vnd.ms-artgalry":{source:`iana`,extensions:[`cil`]},"application/vnd.ms-asf":{source:`iana`},"application/vnd.ms-cab-compressed":{source:`iana`,extensions:[`cab`]},"application/vnd.ms-color.iccprofile":{source:`apache`},"application/vnd.ms-excel":{source:`iana`,compressible:!1,extensions:[`xls`,`xlm`,`xla`,`xlc`,`xlt`,`xlw`]},"application/vnd.ms-excel.addin.macroenabled.12":{source:`iana`,extensions:[`xlam`]},"application/vnd.ms-excel.sheet.binary.macroenabled.12":{source:`iana`,extensions:[`xlsb`]},"application/vnd.ms-excel.sheet.macroenabled.12":{source:`iana`,extensions:[`xlsm`]},"application/vnd.ms-excel.template.macroenabled.12":{source:`iana`,extensions:[`xltm`]},"application/vnd.ms-fontobject":{source:`iana`,compressible:!0,extensions:[`eot`]},"application/vnd.ms-htmlhelp":{source:`iana`,extensions:[`chm`]},"application/vnd.ms-ims":{source:`iana`,extensions:[`ims`]},"application/vnd.ms-lrm":{source:`iana`,extensions:[`lrm`]},"application/vnd.ms-office.activex+xml":{source:`iana`,compressible:!0},"application/vnd.ms-officetheme":{source:`iana`,extensions:[`thmx`]},"application/vnd.ms-opentype":{source:`apache`,compressible:!0},"application/vnd.ms-outlook":{compressible:!1,extensions:[`msg`]},"application/vnd.ms-package.obfuscated-opentype":{source:`apache`},"application/vnd.ms-pki.seccat":{source:`apache`,extensions:[`cat`]},"application/vnd.ms-pki.stl":{source:`apache`,extensions:[`stl`]},"application/vnd.ms-playready.initiator+xml":{source:`iana`,compressible:!0},"application/vnd.ms-powerpoint":{source:`iana`,compressible:!1,extensions:[`ppt`,`pps`,`pot`]},"application/vnd.ms-powerpoint.addin.macroenabled.12":{source:`iana`,extensions:[`ppam`]},"application/vnd.ms-powerpoint.presentation.macroenabled.12":{source:`iana`,extensions:[`pptm`]},"application/vnd.ms-powerpoint.slide.macroenabled.12":{source:`iana`,extensions:[`sldm`]},"application/vnd.ms-powerpoint.slideshow.macroenabled.12":{source:`iana`,extensions:[`ppsm`]},"application/vnd.ms-powerpoint.template.macroenabled.12":{source:`iana`,extensions:[`potm`]},"application/vnd.ms-printdevicecapabilities+xml":{source:`iana`,compressible:!0},"application/vnd.ms-printing.printticket+xml":{source:`apache`,compressible:!0},"application/vnd.ms-printschematicket+xml":{source:`iana`,compressible:!0},"application/vnd.ms-project":{source:`iana`,extensions:[`mpp`,`mpt`]},"application/vnd.ms-tnef":{source:`iana`},"application/vnd.ms-visio.viewer":{extensions:[`vdx`]},"application/vnd.ms-windows.devicepairing":{source:`iana`},"application/vnd.ms-windows.nwprinting.oob":{source:`iana`},"application/vnd.ms-windows.printerpairing":{source:`iana`},"application/vnd.ms-windows.wsd.oob":{source:`iana`},"application/vnd.ms-wmdrm.lic-chlg-req":{source:`iana`},"application/vnd.ms-wmdrm.lic-resp":{source:`iana`},"application/vnd.ms-wmdrm.meter-chlg-req":{source:`iana`},"application/vnd.ms-wmdrm.meter-resp":{source:`iana`},"application/vnd.ms-word.document.macroenabled.12":{source:`iana`,extensions:[`docm`]},"application/vnd.ms-word.template.macroenabled.12":{source:`iana`,extensions:[`dotm`]},"application/vnd.ms-works":{source:`iana`,extensions:[`wps`,`wks`,`wcm`,`wdb`]},"application/vnd.ms-wpl":{source:`iana`,extensions:[`wpl`]},"application/vnd.ms-xpsdocument":{source:`iana`,compressible:!1,extensions:[`xps`]},"application/vnd.msa-disk-image":{source:`iana`},"application/vnd.mseq":{source:`iana`,extensions:[`mseq`]},"application/vnd.msgpack":{source:`iana`},"application/vnd.msign":{source:`iana`},"application/vnd.multiad.creator":{source:`iana`},"application/vnd.multiad.creator.cif":{source:`iana`},"application/vnd.music-niff":{source:`iana`},"application/vnd.musician":{source:`iana`,extensions:[`mus`]},"application/vnd.muvee.style":{source:`iana`,extensions:[`msty`]},"application/vnd.mynfc":{source:`iana`,extensions:[`taglet`]},"application/vnd.nacamar.ybrid+json":{source:`iana`,compressible:!0},"application/vnd.nato.bindingdataobject+cbor":{source:`iana`},"application/vnd.nato.bindingdataobject+json":{source:`iana`,compressible:!0},"application/vnd.nato.bindingdataobject+xml":{source:`iana`,compressible:!0,extensions:[`bdo`]},"application/vnd.nato.openxmlformats-package.iepd+zip":{source:`iana`,compressible:!1},"application/vnd.ncd.control":{source:`iana`},"application/vnd.ncd.reference":{source:`iana`},"application/vnd.nearst.inv+json":{source:`iana`,compressible:!0},"application/vnd.nebumind.line":{source:`iana`},"application/vnd.nervana":{source:`iana`},"application/vnd.netfpx":{source:`iana`},"application/vnd.neurolanguage.nlu":{source:`iana`,extensions:[`nlu`]},"application/vnd.nimn":{source:`iana`},"application/vnd.nintendo.nitro.rom":{source:`iana`},"application/vnd.nintendo.snes.rom":{source:`iana`},"application/vnd.nitf":{source:`iana`,extensions:[`ntf`,`nitf`]},"application/vnd.noblenet-directory":{source:`iana`,extensions:[`nnd`]},"application/vnd.noblenet-sealer":{source:`iana`,extensions:[`nns`]},"application/vnd.noblenet-web":{source:`iana`,extensions:[`nnw`]},"application/vnd.nokia.catalogs":{source:`iana`},"application/vnd.nokia.conml+wbxml":{source:`iana`},"application/vnd.nokia.conml+xml":{source:`iana`,compressible:!0},"application/vnd.nokia.iptv.config+xml":{source:`iana`,compressible:!0},"application/vnd.nokia.isds-radio-presets":{source:`iana`},"application/vnd.nokia.landmark+wbxml":{source:`iana`},"application/vnd.nokia.landmark+xml":{source:`iana`,compressible:!0},"application/vnd.nokia.landmarkcollection+xml":{source:`iana`,compressible:!0},"application/vnd.nokia.n-gage.ac+xml":{source:`iana`,compressible:!0,extensions:[`ac`]},"application/vnd.nokia.n-gage.data":{source:`iana`,extensions:[`ngdat`]},"application/vnd.nokia.n-gage.symbian.install":{source:`apache`,extensions:[`n-gage`]},"application/vnd.nokia.ncd":{source:`iana`},"application/vnd.nokia.pcd+wbxml":{source:`iana`},"application/vnd.nokia.pcd+xml":{source:`iana`,compressible:!0},"application/vnd.nokia.radio-preset":{source:`iana`,extensions:[`rpst`]},"application/vnd.nokia.radio-presets":{source:`iana`,extensions:[`rpss`]},"application/vnd.novadigm.edm":{source:`iana`,extensions:[`edm`]},"application/vnd.novadigm.edx":{source:`iana`,extensions:[`edx`]},"application/vnd.novadigm.ext":{source:`iana`,extensions:[`ext`]},"application/vnd.ntt-local.content-share":{source:`iana`},"application/vnd.ntt-local.file-transfer":{source:`iana`},"application/vnd.ntt-local.ogw_remote-access":{source:`iana`},"application/vnd.ntt-local.sip-ta_remote":{source:`iana`},"application/vnd.ntt-local.sip-ta_tcp_stream":{source:`iana`},"application/vnd.oai.workflows":{source:`iana`},"application/vnd.oai.workflows+json":{source:`iana`,compressible:!0},"application/vnd.oai.workflows+yaml":{source:`iana`},"application/vnd.oasis.opendocument.base":{source:`iana`},"application/vnd.oasis.opendocument.chart":{source:`iana`,extensions:[`odc`]},"application/vnd.oasis.opendocument.chart-template":{source:`iana`,extensions:[`otc`]},"application/vnd.oasis.opendocument.database":{source:`apache`,extensions:[`odb`]},"application/vnd.oasis.opendocument.formula":{source:`iana`,extensions:[`odf`]},"application/vnd.oasis.opendocument.formula-template":{source:`iana`,extensions:[`odft`]},"application/vnd.oasis.opendocument.graphics":{source:`iana`,compressible:!1,extensions:[`odg`]},"application/vnd.oasis.opendocument.graphics-template":{source:`iana`,extensions:[`otg`]},"application/vnd.oasis.opendocument.image":{source:`iana`,extensions:[`odi`]},"application/vnd.oasis.opendocument.image-template":{source:`iana`,extensions:[`oti`]},"application/vnd.oasis.opendocument.presentation":{source:`iana`,compressible:!1,extensions:[`odp`]},"application/vnd.oasis.opendocument.presentation-template":{source:`iana`,extensions:[`otp`]},"application/vnd.oasis.opendocument.spreadsheet":{source:`iana`,compressible:!1,extensions:[`ods`]},"application/vnd.oasis.opendocument.spreadsheet-template":{source:`iana`,extensions:[`ots`]},"application/vnd.oasis.opendocument.text":{source:`iana`,compressible:!1,extensions:[`odt`]},"application/vnd.oasis.opendocument.text-master":{source:`iana`,extensions:[`odm`]},"application/vnd.oasis.opendocument.text-master-template":{source:`iana`},"application/vnd.oasis.opendocument.text-template":{source:`iana`,extensions:[`ott`]},"application/vnd.oasis.opendocument.text-web":{source:`iana`,extensions:[`oth`]},"application/vnd.obn":{source:`iana`},"application/vnd.ocf+cbor":{source:`iana`},"application/vnd.oci.image.manifest.v1+json":{source:`iana`,compressible:!0},"application/vnd.oftn.l10n+json":{source:`iana`,compressible:!0},"application/vnd.oipf.contentaccessdownload+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.contentaccessstreaming+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.cspg-hexbinary":{source:`iana`},"application/vnd.oipf.dae.svg+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.dae.xhtml+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.mippvcontrolmessage+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.pae.gem":{source:`iana`},"application/vnd.oipf.spdiscovery+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.spdlist+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.ueprofile+xml":{source:`iana`,compressible:!0},"application/vnd.oipf.userprofile+xml":{source:`iana`,compressible:!0},"application/vnd.olpc-sugar":{source:`iana`,extensions:[`xo`]},"application/vnd.oma-scws-config":{source:`iana`},"application/vnd.oma-scws-http-request":{source:`iana`},"application/vnd.oma-scws-http-response":{source:`iana`},"application/vnd.oma.bcast.associated-procedure-parameter+xml":{source:`iana`,compressible:!0},"application/vnd.oma.bcast.drm-trigger+xml":{source:`apache`,compressible:!0},"application/vnd.oma.bcast.imd+xml":{source:`iana`,compressible:!0},"application/vnd.oma.bcast.ltkm":{source:`iana`},"application/vnd.oma.bcast.notification+xml":{source:`iana`,compressible:!0},"application/vnd.oma.bcast.provisioningtrigger":{source:`iana`},"application/vnd.oma.bcast.sgboot":{source:`iana`},"application/vnd.oma.bcast.sgdd+xml":{source:`iana`,compressible:!0},"application/vnd.oma.bcast.sgdu":{source:`iana`},"application/vnd.oma.bcast.simple-symbol-container":{source:`iana`},"application/vnd.oma.bcast.smartcard-trigger+xml":{source:`apache`,compressible:!0},"application/vnd.oma.bcast.sprov+xml":{source:`iana`,compressible:!0},"application/vnd.oma.bcast.stkm":{source:`iana`},"application/vnd.oma.cab-address-book+xml":{source:`iana`,compressible:!0},"application/vnd.oma.cab-feature-handler+xml":{source:`iana`,compressible:!0},"application/vnd.oma.cab-pcc+xml":{source:`iana`,compressible:!0},"application/vnd.oma.cab-subs-invite+xml":{source:`iana`,compressible:!0},"application/vnd.oma.cab-user-prefs+xml":{source:`iana`,compressible:!0},"application/vnd.oma.dcd":{source:`iana`},"application/vnd.oma.dcdc":{source:`iana`},"application/vnd.oma.dd2+xml":{source:`iana`,compressible:!0,extensions:[`dd2`]},"application/vnd.oma.drm.risd+xml":{source:`iana`,compressible:!0},"application/vnd.oma.group-usage-list+xml":{source:`iana`,compressible:!0},"application/vnd.oma.lwm2m+cbor":{source:`iana`},"application/vnd.oma.lwm2m+json":{source:`iana`,compressible:!0},"application/vnd.oma.lwm2m+tlv":{source:`iana`},"application/vnd.oma.pal+xml":{source:`iana`,compressible:!0},"application/vnd.oma.poc.detailed-progress-report+xml":{source:`iana`,compressible:!0},"application/vnd.oma.poc.final-report+xml":{source:`iana`,compressible:!0},"application/vnd.oma.poc.groups+xml":{source:`iana`,compressible:!0},"application/vnd.oma.poc.invocation-descriptor+xml":{source:`iana`,compressible:!0},"application/vnd.oma.poc.optimized-progress-report+xml":{source:`iana`,compressible:!0},"application/vnd.oma.push":{source:`iana`},"application/vnd.oma.scidm.messages+xml":{source:`iana`,compressible:!0},"application/vnd.oma.xcap-directory+xml":{source:`iana`,compressible:!0},"application/vnd.omads-email+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/vnd.omads-file+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/vnd.omads-folder+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/vnd.omaloc-supl-init":{source:`iana`},"application/vnd.onepager":{source:`iana`},"application/vnd.onepagertamp":{source:`iana`},"application/vnd.onepagertamx":{source:`iana`},"application/vnd.onepagertat":{source:`iana`},"application/vnd.onepagertatp":{source:`iana`},"application/vnd.onepagertatx":{source:`iana`},"application/vnd.onvif.metadata":{source:`iana`},"application/vnd.openblox.game+xml":{source:`iana`,compressible:!0,extensions:[`obgx`]},"application/vnd.openblox.game-binary":{source:`iana`},"application/vnd.openeye.oeb":{source:`iana`},"application/vnd.openofficeorg.extension":{source:`apache`,extensions:[`oxt`]},"application/vnd.openstreetmap.data+xml":{source:`iana`,compressible:!0,extensions:[`osm`]},"application/vnd.opentimestamps.ots":{source:`iana`},"application/vnd.openvpi.dspx+json":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.custom-properties+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.customxmlproperties+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.drawing+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.drawingml.chart+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.extended-properties+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.comments+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.presentation":{source:`iana`,compressible:!1,extensions:[`pptx`]},"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.presprops+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.slide":{source:`iana`,extensions:[`sldx`]},"application/vnd.openxmlformats-officedocument.presentationml.slide+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.slideshow":{source:`iana`,extensions:[`ppsx`]},"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.tags+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.template":{source:`iana`,extensions:[`potx`]},"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{source:`iana`,compressible:!1,extensions:[`xlsx`]},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.template":{source:`iana`,extensions:[`xltx`]},"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.theme+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.themeoverride+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.vmldrawing":{source:`iana`},"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.document":{source:`iana`,compressible:!1,extensions:[`docx`]},"application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.template":{source:`iana`,extensions:[`dotx`]},"application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-package.core-properties+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml":{source:`iana`,compressible:!0},"application/vnd.openxmlformats-package.relationships+xml":{source:`iana`,compressible:!0},"application/vnd.oracle.resource+json":{source:`iana`,compressible:!0},"application/vnd.orange.indata":{source:`iana`},"application/vnd.osa.netdeploy":{source:`iana`},"application/vnd.osgeo.mapguide.package":{source:`iana`,extensions:[`mgp`]},"application/vnd.osgi.bundle":{source:`iana`},"application/vnd.osgi.dp":{source:`iana`,extensions:[`dp`]},"application/vnd.osgi.subsystem":{source:`iana`,extensions:[`esa`]},"application/vnd.otps.ct-kip+xml":{source:`iana`,compressible:!0},"application/vnd.oxli.countgraph":{source:`iana`},"application/vnd.pagerduty+json":{source:`iana`,compressible:!0},"application/vnd.palm":{source:`iana`,extensions:[`pdb`,`pqa`,`oprc`]},"application/vnd.panoply":{source:`iana`},"application/vnd.paos.xml":{source:`iana`},"application/vnd.patentdive":{source:`iana`},"application/vnd.patientecommsdoc":{source:`iana`},"application/vnd.pawaafile":{source:`iana`,extensions:[`paw`]},"application/vnd.pcos":{source:`iana`},"application/vnd.pg.format":{source:`iana`,extensions:[`str`]},"application/vnd.pg.osasli":{source:`iana`,extensions:[`ei6`]},"application/vnd.piaccess.application-licence":{source:`iana`},"application/vnd.picsel":{source:`iana`,extensions:[`efif`]},"application/vnd.pmi.widget":{source:`iana`,extensions:[`wg`]},"application/vnd.poc.group-advertisement+xml":{source:`iana`,compressible:!0},"application/vnd.pocketlearn":{source:`iana`,extensions:[`plf`]},"application/vnd.powerbuilder6":{source:`iana`,extensions:[`pbd`]},"application/vnd.powerbuilder6-s":{source:`iana`},"application/vnd.powerbuilder7":{source:`iana`},"application/vnd.powerbuilder7-s":{source:`iana`},"application/vnd.powerbuilder75":{source:`iana`},"application/vnd.powerbuilder75-s":{source:`iana`},"application/vnd.preminet":{source:`iana`},"application/vnd.previewsystems.box":{source:`iana`,extensions:[`box`]},"application/vnd.procrate.brushset":{extensions:[`brushset`]},"application/vnd.procreate.brush":{extensions:[`brush`]},"application/vnd.procreate.dream":{extensions:[`drm`]},"application/vnd.proteus.magazine":{source:`iana`,extensions:[`mgz`]},"application/vnd.psfs":{source:`iana`},"application/vnd.pt.mundusmundi":{source:`iana`},"application/vnd.publishare-delta-tree":{source:`iana`,extensions:[`qps`]},"application/vnd.pvi.ptid1":{source:`iana`,extensions:[`ptid`]},"application/vnd.pwg-multiplexed":{source:`iana`},"application/vnd.pwg-xhtml-print+xml":{source:`iana`,compressible:!0,extensions:[`xhtm`]},"application/vnd.qualcomm.brew-app-res":{source:`iana`},"application/vnd.quarantainenet":{source:`iana`},"application/vnd.quark.quarkxpress":{source:`iana`,extensions:[`qxd`,`qxt`,`qwd`,`qwt`,`qxl`,`qxb`]},"application/vnd.quobject-quoxdocument":{source:`iana`},"application/vnd.radisys.moml+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-audit+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-audit-conf+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-audit-conn+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-audit-dialog+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-audit-stream+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-conf+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-dialog+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-dialog-base+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-dialog-fax-detect+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-dialog-fax-sendrecv+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-dialog-group+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-dialog-speech+xml":{source:`iana`,compressible:!0},"application/vnd.radisys.msml-dialog-transform+xml":{source:`iana`,compressible:!0},"application/vnd.rainstor.data":{source:`iana`},"application/vnd.rapid":{source:`iana`},"application/vnd.rar":{source:`iana`,extensions:[`rar`]},"application/vnd.realvnc.bed":{source:`iana`,extensions:[`bed`]},"application/vnd.recordare.musicxml":{source:`iana`,extensions:[`mxl`]},"application/vnd.recordare.musicxml+xml":{source:`iana`,compressible:!0,extensions:[`musicxml`]},"application/vnd.relpipe":{source:`iana`},"application/vnd.renlearn.rlprint":{source:`iana`},"application/vnd.resilient.logic":{source:`iana`},"application/vnd.restful+json":{source:`iana`,compressible:!0},"application/vnd.rig.cryptonote":{source:`iana`,extensions:[`cryptonote`]},"application/vnd.rim.cod":{source:`apache`,extensions:[`cod`]},"application/vnd.rn-realmedia":{source:`apache`,extensions:[`rm`]},"application/vnd.rn-realmedia-vbr":{source:`apache`,extensions:[`rmvb`]},"application/vnd.route66.link66+xml":{source:`iana`,compressible:!0,extensions:[`link66`]},"application/vnd.rs-274x":{source:`iana`},"application/vnd.ruckus.download":{source:`iana`},"application/vnd.s3sms":{source:`iana`},"application/vnd.sailingtracker.track":{source:`iana`,extensions:[`st`]},"application/vnd.sar":{source:`iana`},"application/vnd.sbm.cid":{source:`iana`},"application/vnd.sbm.mid2":{source:`iana`},"application/vnd.scribus":{source:`iana`},"application/vnd.sealed.3df":{source:`iana`},"application/vnd.sealed.csf":{source:`iana`},"application/vnd.sealed.doc":{source:`iana`},"application/vnd.sealed.eml":{source:`iana`},"application/vnd.sealed.mht":{source:`iana`},"application/vnd.sealed.net":{source:`iana`},"application/vnd.sealed.ppt":{source:`iana`},"application/vnd.sealed.tiff":{source:`iana`},"application/vnd.sealed.xls":{source:`iana`},"application/vnd.sealedmedia.softseal.html":{source:`iana`},"application/vnd.sealedmedia.softseal.pdf":{source:`iana`},"application/vnd.seemail":{source:`iana`,extensions:[`see`]},"application/vnd.seis+json":{source:`iana`,compressible:!0},"application/vnd.sema":{source:`iana`,extensions:[`sema`]},"application/vnd.semd":{source:`iana`,extensions:[`semd`]},"application/vnd.semf":{source:`iana`,extensions:[`semf`]},"application/vnd.shade-save-file":{source:`iana`},"application/vnd.shana.informed.formdata":{source:`iana`,extensions:[`ifm`]},"application/vnd.shana.informed.formtemplate":{source:`iana`,extensions:[`itp`]},"application/vnd.shana.informed.interchange":{source:`iana`,extensions:[`iif`]},"application/vnd.shana.informed.package":{source:`iana`,extensions:[`ipk`]},"application/vnd.shootproof+json":{source:`iana`,compressible:!0},"application/vnd.shopkick+json":{source:`iana`,compressible:!0},"application/vnd.shp":{source:`iana`},"application/vnd.shx":{source:`iana`},"application/vnd.sigrok.session":{source:`iana`},"application/vnd.simtech-mindmapper":{source:`iana`,extensions:[`twd`,`twds`]},"application/vnd.siren+json":{source:`iana`,compressible:!0},"application/vnd.sketchometry":{source:`iana`},"application/vnd.smaf":{source:`iana`,extensions:[`mmf`]},"application/vnd.smart.notebook":{source:`iana`},"application/vnd.smart.teacher":{source:`iana`,extensions:[`teacher`]},"application/vnd.smintio.portals.archive":{source:`iana`},"application/vnd.snesdev-page-table":{source:`iana`},"application/vnd.software602.filler.form+xml":{source:`iana`,compressible:!0,extensions:[`fo`]},"application/vnd.software602.filler.form-xml-zip":{source:`iana`},"application/vnd.solent.sdkm+xml":{source:`iana`,compressible:!0,extensions:[`sdkm`,`sdkd`]},"application/vnd.spotfire.dxp":{source:`iana`,extensions:[`dxp`]},"application/vnd.spotfire.sfs":{source:`iana`,extensions:[`sfs`]},"application/vnd.sqlite3":{source:`iana`},"application/vnd.sss-cod":{source:`iana`},"application/vnd.sss-dtf":{source:`iana`},"application/vnd.sss-ntf":{source:`iana`},"application/vnd.stardivision.calc":{source:`apache`,extensions:[`sdc`]},"application/vnd.stardivision.draw":{source:`apache`,extensions:[`sda`]},"application/vnd.stardivision.impress":{source:`apache`,extensions:[`sdd`]},"application/vnd.stardivision.math":{source:`apache`,extensions:[`smf`]},"application/vnd.stardivision.writer":{source:`apache`,extensions:[`sdw`,`vor`]},"application/vnd.stardivision.writer-global":{source:`apache`,extensions:[`sgl`]},"application/vnd.stepmania.package":{source:`iana`,extensions:[`smzip`]},"application/vnd.stepmania.stepchart":{source:`iana`,extensions:[`sm`]},"application/vnd.street-stream":{source:`iana`},"application/vnd.sun.wadl+xml":{source:`iana`,compressible:!0,extensions:[`wadl`]},"application/vnd.sun.xml.calc":{source:`apache`,extensions:[`sxc`]},"application/vnd.sun.xml.calc.template":{source:`apache`,extensions:[`stc`]},"application/vnd.sun.xml.draw":{source:`apache`,extensions:[`sxd`]},"application/vnd.sun.xml.draw.template":{source:`apache`,extensions:[`std`]},"application/vnd.sun.xml.impress":{source:`apache`,extensions:[`sxi`]},"application/vnd.sun.xml.impress.template":{source:`apache`,extensions:[`sti`]},"application/vnd.sun.xml.math":{source:`apache`,extensions:[`sxm`]},"application/vnd.sun.xml.writer":{source:`apache`,extensions:[`sxw`]},"application/vnd.sun.xml.writer.global":{source:`apache`,extensions:[`sxg`]},"application/vnd.sun.xml.writer.template":{source:`apache`,extensions:[`stw`]},"application/vnd.sus-calendar":{source:`iana`,extensions:[`sus`,`susp`]},"application/vnd.svd":{source:`iana`,extensions:[`svd`]},"application/vnd.swiftview-ics":{source:`iana`},"application/vnd.sybyl.mol2":{source:`iana`},"application/vnd.sycle+xml":{source:`iana`,compressible:!0},"application/vnd.syft+json":{source:`iana`,compressible:!0},"application/vnd.symbian.install":{source:`apache`,extensions:[`sis`,`sisx`]},"application/vnd.syncml+xml":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`xsm`]},"application/vnd.syncml.dm+wbxml":{source:`iana`,charset:`UTF-8`,extensions:[`bdm`]},"application/vnd.syncml.dm+xml":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`xdm`]},"application/vnd.syncml.dm.notification":{source:`iana`},"application/vnd.syncml.dmddf+wbxml":{source:`iana`},"application/vnd.syncml.dmddf+xml":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`ddf`]},"application/vnd.syncml.dmtnds+wbxml":{source:`iana`},"application/vnd.syncml.dmtnds+xml":{source:`iana`,charset:`UTF-8`,compressible:!0},"application/vnd.syncml.ds.notification":{source:`iana`},"application/vnd.tableschema+json":{source:`iana`,compressible:!0},"application/vnd.tao.intent-module-archive":{source:`iana`,extensions:[`tao`]},"application/vnd.tcpdump.pcap":{source:`iana`,extensions:[`pcap`,`cap`,`dmp`]},"application/vnd.think-cell.ppttc+json":{source:`iana`,compressible:!0},"application/vnd.tmd.mediaflex.api+xml":{source:`iana`,compressible:!0},"application/vnd.tml":{source:`iana`},"application/vnd.tmobile-livetv":{source:`iana`,extensions:[`tmo`]},"application/vnd.tri.onesource":{source:`iana`},"application/vnd.trid.tpt":{source:`iana`,extensions:[`tpt`]},"application/vnd.triscape.mxs":{source:`iana`,extensions:[`mxs`]},"application/vnd.trueapp":{source:`iana`,extensions:[`tra`]},"application/vnd.truedoc":{source:`iana`},"application/vnd.ubisoft.webplayer":{source:`iana`},"application/vnd.ufdl":{source:`iana`,extensions:[`ufd`,`ufdl`]},"application/vnd.uic.osdm+json":{source:`iana`,compressible:!0},"application/vnd.uiq.theme":{source:`iana`,extensions:[`utz`]},"application/vnd.umajin":{source:`iana`,extensions:[`umj`]},"application/vnd.unity":{source:`iana`,extensions:[`unityweb`]},"application/vnd.uoml+xml":{source:`iana`,compressible:!0,extensions:[`uoml`,`uo`]},"application/vnd.uplanet.alert":{source:`iana`},"application/vnd.uplanet.alert-wbxml":{source:`iana`},"application/vnd.uplanet.bearer-choice":{source:`iana`},"application/vnd.uplanet.bearer-choice-wbxml":{source:`iana`},"application/vnd.uplanet.cacheop":{source:`iana`},"application/vnd.uplanet.cacheop-wbxml":{source:`iana`},"application/vnd.uplanet.channel":{source:`iana`},"application/vnd.uplanet.channel-wbxml":{source:`iana`},"application/vnd.uplanet.list":{source:`iana`},"application/vnd.uplanet.list-wbxml":{source:`iana`},"application/vnd.uplanet.listcmd":{source:`iana`},"application/vnd.uplanet.listcmd-wbxml":{source:`iana`},"application/vnd.uplanet.signal":{source:`iana`},"application/vnd.uri-map":{source:`iana`},"application/vnd.valve.source.material":{source:`iana`},"application/vnd.vcx":{source:`iana`,extensions:[`vcx`]},"application/vnd.vd-study":{source:`iana`},"application/vnd.vectorworks":{source:`iana`},"application/vnd.vel+json":{source:`iana`,compressible:!0},"application/vnd.veraison.tsm-report+cbor":{source:`iana`},"application/vnd.veraison.tsm-report+json":{source:`iana`,compressible:!0},"application/vnd.verimatrix.vcas":{source:`iana`},"application/vnd.veritone.aion+json":{source:`iana`,compressible:!0},"application/vnd.veryant.thin":{source:`iana`},"application/vnd.ves.encrypted":{source:`iana`},"application/vnd.vidsoft.vidconference":{source:`iana`},"application/vnd.visio":{source:`iana`,extensions:[`vsd`,`vst`,`vss`,`vsw`,`vsdx`,`vtx`]},"application/vnd.visionary":{source:`iana`,extensions:[`vis`]},"application/vnd.vividence.scriptfile":{source:`iana`},"application/vnd.vocalshaper.vsp4":{source:`iana`},"application/vnd.vsf":{source:`iana`,extensions:[`vsf`]},"application/vnd.wap.sic":{source:`iana`},"application/vnd.wap.slc":{source:`iana`},"application/vnd.wap.wbxml":{source:`iana`,charset:`UTF-8`,extensions:[`wbxml`]},"application/vnd.wap.wmlc":{source:`iana`,extensions:[`wmlc`]},"application/vnd.wap.wmlscriptc":{source:`iana`,extensions:[`wmlsc`]},"application/vnd.wasmflow.wafl":{source:`iana`},"application/vnd.webturbo":{source:`iana`,extensions:[`wtb`]},"application/vnd.wfa.dpp":{source:`iana`},"application/vnd.wfa.p2p":{source:`iana`},"application/vnd.wfa.wsc":{source:`iana`},"application/vnd.windows.devicepairing":{source:`iana`},"application/vnd.wmc":{source:`iana`},"application/vnd.wmf.bootstrap":{source:`iana`},"application/vnd.wolfram.mathematica":{source:`iana`},"application/vnd.wolfram.mathematica.package":{source:`iana`},"application/vnd.wolfram.player":{source:`iana`,extensions:[`nbp`]},"application/vnd.wordlift":{source:`iana`},"application/vnd.wordperfect":{source:`iana`,extensions:[`wpd`]},"application/vnd.wqd":{source:`iana`,extensions:[`wqd`]},"application/vnd.wrq-hp3000-labelled":{source:`iana`},"application/vnd.wt.stf":{source:`iana`,extensions:[`stf`]},"application/vnd.wv.csp+wbxml":{source:`iana`},"application/vnd.wv.csp+xml":{source:`iana`,compressible:!0},"application/vnd.wv.ssp+xml":{source:`iana`,compressible:!0},"application/vnd.xacml+json":{source:`iana`,compressible:!0},"application/vnd.xara":{source:`iana`,extensions:[`xar`]},"application/vnd.xarin.cpj":{source:`iana`},"application/vnd.xecrets-encrypted":{source:`iana`},"application/vnd.xfdl":{source:`iana`,extensions:[`xfdl`]},"application/vnd.xfdl.webform":{source:`iana`},"application/vnd.xmi+xml":{source:`iana`,compressible:!0},"application/vnd.xmpie.cpkg":{source:`iana`},"application/vnd.xmpie.dpkg":{source:`iana`},"application/vnd.xmpie.plan":{source:`iana`},"application/vnd.xmpie.ppkg":{source:`iana`},"application/vnd.xmpie.xlim":{source:`iana`},"application/vnd.yamaha.hv-dic":{source:`iana`,extensions:[`hvd`]},"application/vnd.yamaha.hv-script":{source:`iana`,extensions:[`hvs`]},"application/vnd.yamaha.hv-voice":{source:`iana`,extensions:[`hvp`]},"application/vnd.yamaha.openscoreformat":{source:`iana`,extensions:[`osf`]},"application/vnd.yamaha.openscoreformat.osfpvg+xml":{source:`iana`,compressible:!0,extensions:[`osfpvg`]},"application/vnd.yamaha.remote-setup":{source:`iana`},"application/vnd.yamaha.smaf-audio":{source:`iana`,extensions:[`saf`]},"application/vnd.yamaha.smaf-phrase":{source:`iana`,extensions:[`spf`]},"application/vnd.yamaha.through-ngn":{source:`iana`},"application/vnd.yamaha.tunnel-udpencap":{source:`iana`},"application/vnd.yaoweme":{source:`iana`},"application/vnd.yellowriver-custom-menu":{source:`iana`,extensions:[`cmp`]},"application/vnd.zul":{source:`iana`,extensions:[`zir`,`zirz`]},"application/vnd.zzazz.deck+xml":{source:`iana`,compressible:!0,extensions:[`zaz`]},"application/voicexml+xml":{source:`iana`,compressible:!0,extensions:[`vxml`]},"application/voucher-cms+json":{source:`iana`,compressible:!0},"application/voucher-jws+json":{source:`iana`,compressible:!0},"application/vp":{source:`iana`},"application/vp+cose":{source:`iana`},"application/vp+jwt":{source:`iana`},"application/vq-rtcpxr":{source:`iana`},"application/wasm":{source:`iana`,compressible:!0,extensions:[`wasm`]},"application/watcherinfo+xml":{source:`iana`,compressible:!0,extensions:[`wif`]},"application/webpush-options+json":{source:`iana`,compressible:!0},"application/whoispp-query":{source:`iana`},"application/whoispp-response":{source:`iana`},"application/widget":{source:`iana`,extensions:[`wgt`]},"application/winhlp":{source:`apache`,extensions:[`hlp`]},"application/wita":{source:`iana`},"application/wordperfect5.1":{source:`iana`},"application/wsdl+xml":{source:`iana`,compressible:!0,extensions:[`wsdl`]},"application/wspolicy+xml":{source:`iana`,compressible:!0,extensions:[`wspolicy`]},"application/x-7z-compressed":{source:`apache`,compressible:!1,extensions:[`7z`]},"application/x-abiword":{source:`apache`,extensions:[`abw`]},"application/x-ace-compressed":{source:`apache`,extensions:[`ace`]},"application/x-amf":{source:`apache`},"application/x-apple-diskimage":{source:`apache`,extensions:[`dmg`]},"application/x-arj":{compressible:!1,extensions:[`arj`]},"application/x-authorware-bin":{source:`apache`,extensions:[`aab`,`x32`,`u32`,`vox`]},"application/x-authorware-map":{source:`apache`,extensions:[`aam`]},"application/x-authorware-seg":{source:`apache`,extensions:[`aas`]},"application/x-bcpio":{source:`apache`,extensions:[`bcpio`]},"application/x-bdoc":{compressible:!1,extensions:[`bdoc`]},"application/x-bittorrent":{source:`apache`,extensions:[`torrent`]},"application/x-blender":{extensions:[`blend`]},"application/x-blorb":{source:`apache`,extensions:[`blb`,`blorb`]},"application/x-bzip":{source:`apache`,compressible:!1,extensions:[`bz`]},"application/x-bzip2":{source:`apache`,compressible:!1,extensions:[`bz2`,`boz`]},"application/x-cbr":{source:`apache`,extensions:[`cbr`,`cba`,`cbt`,`cbz`,`cb7`]},"application/x-cdlink":{source:`apache`,extensions:[`vcd`]},"application/x-cfs-compressed":{source:`apache`,extensions:[`cfs`]},"application/x-chat":{source:`apache`,extensions:[`chat`]},"application/x-chess-pgn":{source:`apache`,extensions:[`pgn`]},"application/x-chrome-extension":{extensions:[`crx`]},"application/x-cocoa":{source:`nginx`,extensions:[`cco`]},"application/x-compress":{source:`apache`},"application/x-compressed":{extensions:[`rar`]},"application/x-conference":{source:`apache`,extensions:[`nsc`]},"application/x-cpio":{source:`apache`,extensions:[`cpio`]},"application/x-csh":{source:`apache`,extensions:[`csh`]},"application/x-deb":{compressible:!1},"application/x-debian-package":{source:`apache`,extensions:[`deb`,`udeb`]},"application/x-dgc-compressed":{source:`apache`,extensions:[`dgc`]},"application/x-director":{source:`apache`,extensions:[`dir`,`dcr`,`dxr`,`cst`,`cct`,`cxt`,`w3d`,`fgd`,`swa`]},"application/x-doom":{source:`apache`,extensions:[`wad`]},"application/x-dtbncx+xml":{source:`apache`,compressible:!0,extensions:[`ncx`]},"application/x-dtbook+xml":{source:`apache`,compressible:!0,extensions:[`dtb`]},"application/x-dtbresource+xml":{source:`apache`,compressible:!0,extensions:[`res`]},"application/x-dvi":{source:`apache`,compressible:!1,extensions:[`dvi`]},"application/x-envoy":{source:`apache`,extensions:[`evy`]},"application/x-eva":{source:`apache`,extensions:[`eva`]},"application/x-font-bdf":{source:`apache`,extensions:[`bdf`]},"application/x-font-dos":{source:`apache`},"application/x-font-framemaker":{source:`apache`},"application/x-font-ghostscript":{source:`apache`,extensions:[`gsf`]},"application/x-font-libgrx":{source:`apache`},"application/x-font-linux-psf":{source:`apache`,extensions:[`psf`]},"application/x-font-pcf":{source:`apache`,extensions:[`pcf`]},"application/x-font-snf":{source:`apache`,extensions:[`snf`]},"application/x-font-speedo":{source:`apache`},"application/x-font-sunos-news":{source:`apache`},"application/x-font-type1":{source:`apache`,extensions:[`pfa`,`pfb`,`pfm`,`afm`]},"application/x-font-vfont":{source:`apache`},"application/x-freearc":{source:`apache`,extensions:[`arc`]},"application/x-futuresplash":{source:`apache`,extensions:[`spl`]},"application/x-gca-compressed":{source:`apache`,extensions:[`gca`]},"application/x-glulx":{source:`apache`,extensions:[`ulx`]},"application/x-gnumeric":{source:`apache`,extensions:[`gnumeric`]},"application/x-gramps-xml":{source:`apache`,extensions:[`gramps`]},"application/x-gtar":{source:`apache`,extensions:[`gtar`]},"application/x-gzip":{source:`apache`},"application/x-hdf":{source:`apache`,extensions:[`hdf`]},"application/x-httpd-php":{compressible:!0,extensions:[`php`]},"application/x-install-instructions":{source:`apache`,extensions:[`install`]},"application/x-ipynb+json":{compressible:!0,extensions:[`ipynb`]},"application/x-iso9660-image":{source:`apache`,extensions:[`iso`]},"application/x-iwork-keynote-sffkey":{extensions:[`key`]},"application/x-iwork-numbers-sffnumbers":{extensions:[`numbers`]},"application/x-iwork-pages-sffpages":{extensions:[`pages`]},"application/x-java-archive-diff":{source:`nginx`,extensions:[`jardiff`]},"application/x-java-jnlp-file":{source:`apache`,compressible:!1,extensions:[`jnlp`]},"application/x-javascript":{compressible:!0},"application/x-keepass2":{extensions:[`kdbx`]},"application/x-latex":{source:`apache`,compressible:!1,extensions:[`latex`]},"application/x-lua-bytecode":{extensions:[`luac`]},"application/x-lzh-compressed":{source:`apache`,extensions:[`lzh`,`lha`]},"application/x-makeself":{source:`nginx`,extensions:[`run`]},"application/x-mie":{source:`apache`,extensions:[`mie`]},"application/x-mobipocket-ebook":{source:`apache`,extensions:[`prc`,`mobi`]},"application/x-mpegurl":{compressible:!1},"application/x-ms-application":{source:`apache`,extensions:[`application`]},"application/x-ms-shortcut":{source:`apache`,extensions:[`lnk`]},"application/x-ms-wmd":{source:`apache`,extensions:[`wmd`]},"application/x-ms-wmz":{source:`apache`,extensions:[`wmz`]},"application/x-ms-xbap":{source:`apache`,extensions:[`xbap`]},"application/x-msaccess":{source:`apache`,extensions:[`mdb`]},"application/x-msbinder":{source:`apache`,extensions:[`obd`]},"application/x-mscardfile":{source:`apache`,extensions:[`crd`]},"application/x-msclip":{source:`apache`,extensions:[`clp`]},"application/x-msdos-program":{extensions:[`exe`]},"application/x-msdownload":{source:`apache`,extensions:[`exe`,`dll`,`com`,`bat`,`msi`]},"application/x-msmediaview":{source:`apache`,extensions:[`mvb`,`m13`,`m14`]},"application/x-msmetafile":{source:`apache`,extensions:[`wmf`,`wmz`,`emf`,`emz`]},"application/x-msmoney":{source:`apache`,extensions:[`mny`]},"application/x-mspublisher":{source:`apache`,extensions:[`pub`]},"application/x-msschedule":{source:`apache`,extensions:[`scd`]},"application/x-msterminal":{source:`apache`,extensions:[`trm`]},"application/x-mswrite":{source:`apache`,extensions:[`wri`]},"application/x-netcdf":{source:`apache`,extensions:[`nc`,`cdf`]},"application/x-ns-proxy-autoconfig":{compressible:!0,extensions:[`pac`]},"application/x-nzb":{source:`apache`,extensions:[`nzb`]},"application/x-perl":{source:`nginx`,extensions:[`pl`,`pm`]},"application/x-pilot":{source:`nginx`,extensions:[`prc`,`pdb`]},"application/x-pkcs12":{source:`apache`,compressible:!1,extensions:[`p12`,`pfx`]},"application/x-pkcs7-certificates":{source:`apache`,extensions:[`p7b`,`spc`]},"application/x-pkcs7-certreqresp":{source:`apache`,extensions:[`p7r`]},"application/x-pki-message":{source:`iana`},"application/x-rar-compressed":{source:`apache`,compressible:!1,extensions:[`rar`]},"application/x-redhat-package-manager":{source:`nginx`,extensions:[`rpm`]},"application/x-research-info-systems":{source:`apache`,extensions:[`ris`]},"application/x-sea":{source:`nginx`,extensions:[`sea`]},"application/x-sh":{source:`apache`,compressible:!0,extensions:[`sh`]},"application/x-shar":{source:`apache`,extensions:[`shar`]},"application/x-shockwave-flash":{source:`apache`,compressible:!1,extensions:[`swf`]},"application/x-silverlight-app":{source:`apache`,extensions:[`xap`]},"application/x-sql":{source:`apache`,extensions:[`sql`]},"application/x-stuffit":{source:`apache`,compressible:!1,extensions:[`sit`]},"application/x-stuffitx":{source:`apache`,extensions:[`sitx`]},"application/x-subrip":{source:`apache`,extensions:[`srt`]},"application/x-sv4cpio":{source:`apache`,extensions:[`sv4cpio`]},"application/x-sv4crc":{source:`apache`,extensions:[`sv4crc`]},"application/x-t3vm-image":{source:`apache`,extensions:[`t3`]},"application/x-tads":{source:`apache`,extensions:[`gam`]},"application/x-tar":{source:`apache`,compressible:!0,extensions:[`tar`]},"application/x-tcl":{source:`apache`,extensions:[`tcl`,`tk`]},"application/x-tex":{source:`apache`,extensions:[`tex`]},"application/x-tex-tfm":{source:`apache`,extensions:[`tfm`]},"application/x-texinfo":{source:`apache`,extensions:[`texinfo`,`texi`]},"application/x-tgif":{source:`apache`,extensions:[`obj`]},"application/x-ustar":{source:`apache`,extensions:[`ustar`]},"application/x-virtualbox-hdd":{compressible:!0,extensions:[`hdd`]},"application/x-virtualbox-ova":{compressible:!0,extensions:[`ova`]},"application/x-virtualbox-ovf":{compressible:!0,extensions:[`ovf`]},"application/x-virtualbox-vbox":{compressible:!0,extensions:[`vbox`]},"application/x-virtualbox-vbox-extpack":{compressible:!1,extensions:[`vbox-extpack`]},"application/x-virtualbox-vdi":{compressible:!0,extensions:[`vdi`]},"application/x-virtualbox-vhd":{compressible:!0,extensions:[`vhd`]},"application/x-virtualbox-vmdk":{compressible:!0,extensions:[`vmdk`]},"application/x-wais-source":{source:`apache`,extensions:[`src`]},"application/x-web-app-manifest+json":{compressible:!0,extensions:[`webapp`]},"application/x-www-form-urlencoded":{source:`iana`,compressible:!0},"application/x-x509-ca-cert":{source:`iana`,extensions:[`der`,`crt`,`pem`]},"application/x-x509-ca-ra-cert":{source:`iana`},"application/x-x509-next-ca-cert":{source:`iana`},"application/x-xfig":{source:`apache`,extensions:[`fig`]},"application/x-xliff+xml":{source:`apache`,compressible:!0,extensions:[`xlf`]},"application/x-xpinstall":{source:`apache`,compressible:!1,extensions:[`xpi`]},"application/x-xz":{source:`apache`,extensions:[`xz`]},"application/x-zip-compressed":{extensions:[`zip`]},"application/x-zmachine":{source:`apache`,extensions:[`z1`,`z2`,`z3`,`z4`,`z5`,`z6`,`z7`,`z8`]},"application/x400-bp":{source:`iana`},"application/xacml+xml":{source:`iana`,compressible:!0},"application/xaml+xml":{source:`apache`,compressible:!0,extensions:[`xaml`]},"application/xcap-att+xml":{source:`iana`,compressible:!0,extensions:[`xav`]},"application/xcap-caps+xml":{source:`iana`,compressible:!0,extensions:[`xca`]},"application/xcap-diff+xml":{source:`iana`,compressible:!0,extensions:[`xdf`]},"application/xcap-el+xml":{source:`iana`,compressible:!0,extensions:[`xel`]},"application/xcap-error+xml":{source:`iana`,compressible:!0},"application/xcap-ns+xml":{source:`iana`,compressible:!0,extensions:[`xns`]},"application/xcon-conference-info+xml":{source:`iana`,compressible:!0},"application/xcon-conference-info-diff+xml":{source:`iana`,compressible:!0},"application/xenc+xml":{source:`iana`,compressible:!0,extensions:[`xenc`]},"application/xfdf":{source:`iana`,extensions:[`xfdf`]},"application/xhtml+xml":{source:`iana`,compressible:!0,extensions:[`xhtml`,`xht`]},"application/xhtml-voice+xml":{source:`apache`,compressible:!0},"application/xliff+xml":{source:`iana`,compressible:!0,extensions:[`xlf`]},"application/xml":{source:`iana`,compressible:!0,extensions:[`xml`,`xsl`,`xsd`,`rng`]},"application/xml-dtd":{source:`iana`,compressible:!0,extensions:[`dtd`]},"application/xml-external-parsed-entity":{source:`iana`},"application/xml-patch+xml":{source:`iana`,compressible:!0},"application/xmpp+xml":{source:`iana`,compressible:!0},"application/xop+xml":{source:`iana`,compressible:!0,extensions:[`xop`]},"application/xproc+xml":{source:`apache`,compressible:!0,extensions:[`xpl`]},"application/xslt+xml":{source:`iana`,compressible:!0,extensions:[`xsl`,`xslt`]},"application/xspf+xml":{source:`apache`,compressible:!0,extensions:[`xspf`]},"application/xv+xml":{source:`iana`,compressible:!0,extensions:[`mxml`,`xhvml`,`xvml`,`xvm`]},"application/yaml":{source:`iana`},"application/yang":{source:`iana`,extensions:[`yang`]},"application/yang-data+cbor":{source:`iana`},"application/yang-data+json":{source:`iana`,compressible:!0},"application/yang-data+xml":{source:`iana`,compressible:!0},"application/yang-patch+json":{source:`iana`,compressible:!0},"application/yang-patch+xml":{source:`iana`,compressible:!0},"application/yang-sid+json":{source:`iana`,compressible:!0},"application/yin+xml":{source:`iana`,compressible:!0,extensions:[`yin`]},"application/zip":{source:`iana`,compressible:!1,extensions:[`zip`]},"application/zip+dotlottie":{extensions:[`lottie`]},"application/zlib":{source:`iana`},"application/zstd":{source:`iana`},"audio/1d-interleaved-parityfec":{source:`iana`},"audio/32kadpcm":{source:`iana`},"audio/3gpp":{source:`iana`,compressible:!1,extensions:[`3gpp`]},"audio/3gpp2":{source:`iana`},"audio/aac":{source:`iana`,extensions:[`adts`,`aac`]},"audio/ac3":{source:`iana`},"audio/adpcm":{source:`apache`,extensions:[`adp`]},"audio/amr":{source:`iana`,extensions:[`amr`]},"audio/amr-wb":{source:`iana`},"audio/amr-wb+":{source:`iana`},"audio/aptx":{source:`iana`},"audio/asc":{source:`iana`},"audio/atrac-advanced-lossless":{source:`iana`},"audio/atrac-x":{source:`iana`},"audio/atrac3":{source:`iana`},"audio/basic":{source:`iana`,compressible:!1,extensions:[`au`,`snd`]},"audio/bv16":{source:`iana`},"audio/bv32":{source:`iana`},"audio/clearmode":{source:`iana`},"audio/cn":{source:`iana`},"audio/dat12":{source:`iana`},"audio/dls":{source:`iana`},"audio/dsr-es201108":{source:`iana`},"audio/dsr-es202050":{source:`iana`},"audio/dsr-es202211":{source:`iana`},"audio/dsr-es202212":{source:`iana`},"audio/dv":{source:`iana`},"audio/dvi4":{source:`iana`},"audio/eac3":{source:`iana`},"audio/encaprtp":{source:`iana`},"audio/evrc":{source:`iana`},"audio/evrc-qcp":{source:`iana`},"audio/evrc0":{source:`iana`},"audio/evrc1":{source:`iana`},"audio/evrcb":{source:`iana`},"audio/evrcb0":{source:`iana`},"audio/evrcb1":{source:`iana`},"audio/evrcnw":{source:`iana`},"audio/evrcnw0":{source:`iana`},"audio/evrcnw1":{source:`iana`},"audio/evrcwb":{source:`iana`},"audio/evrcwb0":{source:`iana`},"audio/evrcwb1":{source:`iana`},"audio/evs":{source:`iana`},"audio/flac":{source:`iana`},"audio/flexfec":{source:`iana`},"audio/fwdred":{source:`iana`},"audio/g711-0":{source:`iana`},"audio/g719":{source:`iana`},"audio/g722":{source:`iana`},"audio/g7221":{source:`iana`},"audio/g723":{source:`iana`},"audio/g726-16":{source:`iana`},"audio/g726-24":{source:`iana`},"audio/g726-32":{source:`iana`},"audio/g726-40":{source:`iana`},"audio/g728":{source:`iana`},"audio/g729":{source:`iana`},"audio/g7291":{source:`iana`},"audio/g729d":{source:`iana`},"audio/g729e":{source:`iana`},"audio/gsm":{source:`iana`},"audio/gsm-efr":{source:`iana`},"audio/gsm-hr-08":{source:`iana`},"audio/ilbc":{source:`iana`},"audio/ip-mr_v2.5":{source:`iana`},"audio/isac":{source:`apache`},"audio/l16":{source:`iana`},"audio/l20":{source:`iana`},"audio/l24":{source:`iana`,compressible:!1},"audio/l8":{source:`iana`},"audio/lpc":{source:`iana`},"audio/matroska":{source:`iana`},"audio/melp":{source:`iana`},"audio/melp1200":{source:`iana`},"audio/melp2400":{source:`iana`},"audio/melp600":{source:`iana`},"audio/mhas":{source:`iana`},"audio/midi":{source:`apache`,extensions:[`mid`,`midi`,`kar`,`rmi`]},"audio/midi-clip":{source:`iana`},"audio/mobile-xmf":{source:`iana`,extensions:[`mxmf`]},"audio/mp3":{compressible:!1,extensions:[`mp3`]},"audio/mp4":{source:`iana`,compressible:!1,extensions:[`m4a`,`mp4a`,`m4b`]},"audio/mp4a-latm":{source:`iana`},"audio/mpa":{source:`iana`},"audio/mpa-robust":{source:`iana`},"audio/mpeg":{source:`iana`,compressible:!1,extensions:[`mpga`,`mp2`,`mp2a`,`mp3`,`m2a`,`m3a`]},"audio/mpeg4-generic":{source:`iana`},"audio/musepack":{source:`apache`},"audio/ogg":{source:`iana`,compressible:!1,extensions:[`oga`,`ogg`,`spx`,`opus`]},"audio/opus":{source:`iana`},"audio/parityfec":{source:`iana`},"audio/pcma":{source:`iana`},"audio/pcma-wb":{source:`iana`},"audio/pcmu":{source:`iana`},"audio/pcmu-wb":{source:`iana`},"audio/prs.sid":{source:`iana`},"audio/qcelp":{source:`iana`},"audio/raptorfec":{source:`iana`},"audio/red":{source:`iana`},"audio/rtp-enc-aescm128":{source:`iana`},"audio/rtp-midi":{source:`iana`},"audio/rtploopback":{source:`iana`},"audio/rtx":{source:`iana`},"audio/s3m":{source:`apache`,extensions:[`s3m`]},"audio/scip":{source:`iana`},"audio/silk":{source:`apache`,extensions:[`sil`]},"audio/smv":{source:`iana`},"audio/smv-qcp":{source:`iana`},"audio/smv0":{source:`iana`},"audio/sofa":{source:`iana`},"audio/sp-midi":{source:`iana`},"audio/speex":{source:`iana`},"audio/t140c":{source:`iana`},"audio/t38":{source:`iana`},"audio/telephone-event":{source:`iana`},"audio/tetra_acelp":{source:`iana`},"audio/tetra_acelp_bb":{source:`iana`},"audio/tone":{source:`iana`},"audio/tsvcis":{source:`iana`},"audio/uemclip":{source:`iana`},"audio/ulpfec":{source:`iana`},"audio/usac":{source:`iana`},"audio/vdvi":{source:`iana`},"audio/vmr-wb":{source:`iana`},"audio/vnd.3gpp.iufp":{source:`iana`},"audio/vnd.4sb":{source:`iana`},"audio/vnd.audiokoz":{source:`iana`},"audio/vnd.celp":{source:`iana`},"audio/vnd.cisco.nse":{source:`iana`},"audio/vnd.cmles.radio-events":{source:`iana`},"audio/vnd.cns.anp1":{source:`iana`},"audio/vnd.cns.inf1":{source:`iana`},"audio/vnd.dece.audio":{source:`iana`,extensions:[`uva`,`uvva`]},"audio/vnd.digital-winds":{source:`iana`,extensions:[`eol`]},"audio/vnd.dlna.adts":{source:`iana`},"audio/vnd.dolby.heaac.1":{source:`iana`},"audio/vnd.dolby.heaac.2":{source:`iana`},"audio/vnd.dolby.mlp":{source:`iana`},"audio/vnd.dolby.mps":{source:`iana`},"audio/vnd.dolby.pl2":{source:`iana`},"audio/vnd.dolby.pl2x":{source:`iana`},"audio/vnd.dolby.pl2z":{source:`iana`},"audio/vnd.dolby.pulse.1":{source:`iana`},"audio/vnd.dra":{source:`iana`,extensions:[`dra`]},"audio/vnd.dts":{source:`iana`,extensions:[`dts`]},"audio/vnd.dts.hd":{source:`iana`,extensions:[`dtshd`]},"audio/vnd.dts.uhd":{source:`iana`},"audio/vnd.dvb.file":{source:`iana`},"audio/vnd.everad.plj":{source:`iana`},"audio/vnd.hns.audio":{source:`iana`},"audio/vnd.lucent.voice":{source:`iana`,extensions:[`lvp`]},"audio/vnd.ms-playready.media.pya":{source:`iana`,extensions:[`pya`]},"audio/vnd.nokia.mobile-xmf":{source:`iana`},"audio/vnd.nortel.vbk":{source:`iana`},"audio/vnd.nuera.ecelp4800":{source:`iana`,extensions:[`ecelp4800`]},"audio/vnd.nuera.ecelp7470":{source:`iana`,extensions:[`ecelp7470`]},"audio/vnd.nuera.ecelp9600":{source:`iana`,extensions:[`ecelp9600`]},"audio/vnd.octel.sbc":{source:`iana`},"audio/vnd.presonus.multitrack":{source:`iana`},"audio/vnd.qcelp":{source:`apache`},"audio/vnd.rhetorex.32kadpcm":{source:`iana`},"audio/vnd.rip":{source:`iana`,extensions:[`rip`]},"audio/vnd.rn-realaudio":{compressible:!1},"audio/vnd.sealedmedia.softseal.mpeg":{source:`iana`},"audio/vnd.vmx.cvsd":{source:`iana`},"audio/vnd.wave":{compressible:!1},"audio/vorbis":{source:`iana`,compressible:!1},"audio/vorbis-config":{source:`iana`},"audio/wav":{compressible:!1,extensions:[`wav`]},"audio/wave":{compressible:!1,extensions:[`wav`]},"audio/webm":{source:`apache`,compressible:!1,extensions:[`weba`]},"audio/x-aac":{source:`apache`,compressible:!1,extensions:[`aac`]},"audio/x-aiff":{source:`apache`,extensions:[`aif`,`aiff`,`aifc`]},"audio/x-caf":{source:`apache`,compressible:!1,extensions:[`caf`]},"audio/x-flac":{source:`apache`,extensions:[`flac`]},"audio/x-m4a":{source:`nginx`,extensions:[`m4a`]},"audio/x-matroska":{source:`apache`,extensions:[`mka`]},"audio/x-mpegurl":{source:`apache`,extensions:[`m3u`]},"audio/x-ms-wax":{source:`apache`,extensions:[`wax`]},"audio/x-ms-wma":{source:`apache`,extensions:[`wma`]},"audio/x-pn-realaudio":{source:`apache`,extensions:[`ram`,`ra`]},"audio/x-pn-realaudio-plugin":{source:`apache`,extensions:[`rmp`]},"audio/x-realaudio":{source:`nginx`,extensions:[`ra`]},"audio/x-tta":{source:`apache`},"audio/x-wav":{source:`apache`,extensions:[`wav`]},"audio/xm":{source:`apache`,extensions:[`xm`]},"chemical/x-cdx":{source:`apache`,extensions:[`cdx`]},"chemical/x-cif":{source:`apache`,extensions:[`cif`]},"chemical/x-cmdf":{source:`apache`,extensions:[`cmdf`]},"chemical/x-cml":{source:`apache`,extensions:[`cml`]},"chemical/x-csml":{source:`apache`,extensions:[`csml`]},"chemical/x-pdb":{source:`apache`},"chemical/x-xyz":{source:`apache`,extensions:[`xyz`]},"font/collection":{source:`iana`,extensions:[`ttc`]},"font/otf":{source:`iana`,compressible:!0,extensions:[`otf`]},"font/sfnt":{source:`iana`},"font/ttf":{source:`iana`,compressible:!0,extensions:[`ttf`]},"font/woff":{source:`iana`,extensions:[`woff`]},"font/woff2":{source:`iana`,extensions:[`woff2`]},"image/aces":{source:`iana`,extensions:[`exr`]},"image/apng":{source:`iana`,compressible:!1,extensions:[`apng`]},"image/avci":{source:`iana`,extensions:[`avci`]},"image/avcs":{source:`iana`,extensions:[`avcs`]},"image/avif":{source:`iana`,compressible:!1,extensions:[`avif`]},"image/bmp":{source:`iana`,compressible:!0,extensions:[`bmp`,`dib`]},"image/cgm":{source:`iana`,extensions:[`cgm`]},"image/dicom-rle":{source:`iana`,extensions:[`drle`]},"image/dpx":{source:`iana`,extensions:[`dpx`]},"image/emf":{source:`iana`,extensions:[`emf`]},"image/fits":{source:`iana`,extensions:[`fits`]},"image/g3fax":{source:`iana`,extensions:[`g3`]},"image/gif":{source:`iana`,compressible:!1,extensions:[`gif`]},"image/heic":{source:`iana`,extensions:[`heic`]},"image/heic-sequence":{source:`iana`,extensions:[`heics`]},"image/heif":{source:`iana`,extensions:[`heif`]},"image/heif-sequence":{source:`iana`,extensions:[`heifs`]},"image/hej2k":{source:`iana`,extensions:[`hej2`]},"image/ief":{source:`iana`,extensions:[`ief`]},"image/j2c":{source:`iana`},"image/jaii":{source:`iana`,extensions:[`jaii`]},"image/jais":{source:`iana`,extensions:[`jais`]},"image/jls":{source:`iana`,extensions:[`jls`]},"image/jp2":{source:`iana`,compressible:!1,extensions:[`jp2`,`jpg2`]},"image/jpeg":{source:`iana`,compressible:!1,extensions:[`jpg`,`jpeg`,`jpe`]},"image/jph":{source:`iana`,extensions:[`jph`]},"image/jphc":{source:`iana`,extensions:[`jhc`]},"image/jpm":{source:`iana`,compressible:!1,extensions:[`jpm`,`jpgm`]},"image/jpx":{source:`iana`,compressible:!1,extensions:[`jpx`,`jpf`]},"image/jxl":{source:`iana`,extensions:[`jxl`]},"image/jxr":{source:`iana`,extensions:[`jxr`]},"image/jxra":{source:`iana`,extensions:[`jxra`]},"image/jxrs":{source:`iana`,extensions:[`jxrs`]},"image/jxs":{source:`iana`,extensions:[`jxs`]},"image/jxsc":{source:`iana`,extensions:[`jxsc`]},"image/jxsi":{source:`iana`,extensions:[`jxsi`]},"image/jxss":{source:`iana`,extensions:[`jxss`]},"image/ktx":{source:`iana`,extensions:[`ktx`]},"image/ktx2":{source:`iana`,extensions:[`ktx2`]},"image/naplps":{source:`iana`},"image/pjpeg":{compressible:!1,extensions:[`jfif`]},"image/png":{source:`iana`,compressible:!1,extensions:[`png`]},"image/prs.btif":{source:`iana`,extensions:[`btif`,`btf`]},"image/prs.pti":{source:`iana`,extensions:[`pti`]},"image/pwg-raster":{source:`iana`},"image/sgi":{source:`apache`,extensions:[`sgi`]},"image/svg+xml":{source:`iana`,compressible:!0,extensions:[`svg`,`svgz`]},"image/t38":{source:`iana`,extensions:[`t38`]},"image/tiff":{source:`iana`,compressible:!1,extensions:[`tif`,`tiff`]},"image/tiff-fx":{source:`iana`,extensions:[`tfx`]},"image/vnd.adobe.photoshop":{source:`iana`,compressible:!0,extensions:[`psd`]},"image/vnd.airzip.accelerator.azv":{source:`iana`,extensions:[`azv`]},"image/vnd.clip":{source:`iana`},"image/vnd.cns.inf2":{source:`iana`},"image/vnd.dece.graphic":{source:`iana`,extensions:[`uvi`,`uvvi`,`uvg`,`uvvg`]},"image/vnd.djvu":{source:`iana`,extensions:[`djvu`,`djv`]},"image/vnd.dvb.subtitle":{source:`iana`,extensions:[`sub`]},"image/vnd.dwg":{source:`iana`,extensions:[`dwg`]},"image/vnd.dxf":{source:`iana`,extensions:[`dxf`]},"image/vnd.fastbidsheet":{source:`iana`,extensions:[`fbs`]},"image/vnd.fpx":{source:`iana`,extensions:[`fpx`]},"image/vnd.fst":{source:`iana`,extensions:[`fst`]},"image/vnd.fujixerox.edmics-mmr":{source:`iana`,extensions:[`mmr`]},"image/vnd.fujixerox.edmics-rlc":{source:`iana`,extensions:[`rlc`]},"image/vnd.globalgraphics.pgb":{source:`iana`},"image/vnd.microsoft.icon":{source:`iana`,compressible:!0,extensions:[`ico`]},"image/vnd.mix":{source:`iana`},"image/vnd.mozilla.apng":{source:`iana`},"image/vnd.ms-dds":{compressible:!0,extensions:[`dds`]},"image/vnd.ms-modi":{source:`iana`,extensions:[`mdi`]},"image/vnd.ms-photo":{source:`apache`,extensions:[`wdp`]},"image/vnd.net-fpx":{source:`iana`,extensions:[`npx`]},"image/vnd.pco.b16":{source:`iana`,extensions:[`b16`]},"image/vnd.radiance":{source:`iana`},"image/vnd.sealed.png":{source:`iana`},"image/vnd.sealedmedia.softseal.gif":{source:`iana`},"image/vnd.sealedmedia.softseal.jpg":{source:`iana`},"image/vnd.svf":{source:`iana`},"image/vnd.tencent.tap":{source:`iana`,extensions:[`tap`]},"image/vnd.valve.source.texture":{source:`iana`,extensions:[`vtf`]},"image/vnd.wap.wbmp":{source:`iana`,extensions:[`wbmp`]},"image/vnd.xiff":{source:`iana`,extensions:[`xif`]},"image/vnd.zbrush.pcx":{source:`iana`,extensions:[`pcx`]},"image/webp":{source:`iana`,extensions:[`webp`]},"image/wmf":{source:`iana`,extensions:[`wmf`]},"image/x-3ds":{source:`apache`,extensions:[`3ds`]},"image/x-adobe-dng":{extensions:[`dng`]},"image/x-cmu-raster":{source:`apache`,extensions:[`ras`]},"image/x-cmx":{source:`apache`,extensions:[`cmx`]},"image/x-emf":{source:`iana`},"image/x-freehand":{source:`apache`,extensions:[`fh`,`fhc`,`fh4`,`fh5`,`fh7`]},"image/x-icon":{source:`apache`,compressible:!0,extensions:[`ico`]},"image/x-jng":{source:`nginx`,extensions:[`jng`]},"image/x-mrsid-image":{source:`apache`,extensions:[`sid`]},"image/x-ms-bmp":{source:`nginx`,compressible:!0,extensions:[`bmp`]},"image/x-pcx":{source:`apache`,extensions:[`pcx`]},"image/x-pict":{source:`apache`,extensions:[`pic`,`pct`]},"image/x-portable-anymap":{source:`apache`,extensions:[`pnm`]},"image/x-portable-bitmap":{source:`apache`,extensions:[`pbm`]},"image/x-portable-graymap":{source:`apache`,extensions:[`pgm`]},"image/x-portable-pixmap":{source:`apache`,extensions:[`ppm`]},"image/x-rgb":{source:`apache`,extensions:[`rgb`]},"image/x-tga":{source:`apache`,extensions:[`tga`]},"image/x-wmf":{source:`iana`},"image/x-xbitmap":{source:`apache`,extensions:[`xbm`]},"image/x-xcf":{compressible:!1},"image/x-xpixmap":{source:`apache`,extensions:[`xpm`]},"image/x-xwindowdump":{source:`apache`,extensions:[`xwd`]},"message/bhttp":{source:`iana`},"message/cpim":{source:`iana`},"message/delivery-status":{source:`iana`},"message/disposition-notification":{source:`iana`,extensions:[`disposition-notification`]},"message/external-body":{source:`iana`},"message/feedback-report":{source:`iana`},"message/global":{source:`iana`,extensions:[`u8msg`]},"message/global-delivery-status":{source:`iana`,extensions:[`u8dsn`]},"message/global-disposition-notification":{source:`iana`,extensions:[`u8mdn`]},"message/global-headers":{source:`iana`,extensions:[`u8hdr`]},"message/http":{source:`iana`,compressible:!1},"message/imdn+xml":{source:`iana`,compressible:!0},"message/mls":{source:`iana`},"message/news":{source:`apache`},"message/ohttp-req":{source:`iana`},"message/ohttp-res":{source:`iana`},"message/partial":{source:`iana`,compressible:!1},"message/rfc822":{source:`iana`,compressible:!0,extensions:[`eml`,`mime`,`mht`,`mhtml`]},"message/s-http":{source:`apache`},"message/sip":{source:`iana`},"message/sipfrag":{source:`iana`},"message/tracking-status":{source:`iana`},"message/vnd.si.simp":{source:`apache`},"message/vnd.wfa.wsc":{source:`iana`,extensions:[`wsc`]},"model/3mf":{source:`iana`,extensions:[`3mf`]},"model/e57":{source:`iana`},"model/gltf+json":{source:`iana`,compressible:!0,extensions:[`gltf`]},"model/gltf-binary":{source:`iana`,compressible:!0,extensions:[`glb`]},"model/iges":{source:`iana`,compressible:!1,extensions:[`igs`,`iges`]},"model/jt":{source:`iana`,extensions:[`jt`]},"model/mesh":{source:`iana`,compressible:!1,extensions:[`msh`,`mesh`,`silo`]},"model/mtl":{source:`iana`,extensions:[`mtl`]},"model/obj":{source:`iana`,extensions:[`obj`]},"model/prc":{source:`iana`,extensions:[`prc`]},"model/step":{source:`iana`,extensions:[`step`,`stp`,`stpnc`,`p21`,`210`]},"model/step+xml":{source:`iana`,compressible:!0,extensions:[`stpx`]},"model/step+zip":{source:`iana`,compressible:!1,extensions:[`stpz`]},"model/step-xml+zip":{source:`iana`,compressible:!1,extensions:[`stpxz`]},"model/stl":{source:`iana`,extensions:[`stl`]},"model/u3d":{source:`iana`,extensions:[`u3d`]},"model/vnd.bary":{source:`iana`,extensions:[`bary`]},"model/vnd.cld":{source:`iana`,extensions:[`cld`]},"model/vnd.collada+xml":{source:`iana`,compressible:!0,extensions:[`dae`]},"model/vnd.dwf":{source:`iana`,extensions:[`dwf`]},"model/vnd.flatland.3dml":{source:`iana`},"model/vnd.gdl":{source:`iana`,extensions:[`gdl`]},"model/vnd.gs-gdl":{source:`apache`},"model/vnd.gs.gdl":{source:`iana`},"model/vnd.gtw":{source:`iana`,extensions:[`gtw`]},"model/vnd.moml+xml":{source:`iana`,compressible:!0},"model/vnd.mts":{source:`iana`,extensions:[`mts`]},"model/vnd.opengex":{source:`iana`,extensions:[`ogex`]},"model/vnd.parasolid.transmit.binary":{source:`iana`,extensions:[`x_b`]},"model/vnd.parasolid.transmit.text":{source:`iana`,extensions:[`x_t`]},"model/vnd.pytha.pyox":{source:`iana`,extensions:[`pyo`,`pyox`]},"model/vnd.rosette.annotated-data-model":{source:`iana`},"model/vnd.sap.vds":{source:`iana`,extensions:[`vds`]},"model/vnd.usda":{source:`iana`,extensions:[`usda`]},"model/vnd.usdz+zip":{source:`iana`,compressible:!1,extensions:[`usdz`]},"model/vnd.valve.source.compiled-map":{source:`iana`,extensions:[`bsp`]},"model/vnd.vtu":{source:`iana`,extensions:[`vtu`]},"model/vrml":{source:`iana`,compressible:!1,extensions:[`wrl`,`vrml`]},"model/x3d+binary":{source:`apache`,compressible:!1,extensions:[`x3db`,`x3dbz`]},"model/x3d+fastinfoset":{source:`iana`,extensions:[`x3db`]},"model/x3d+vrml":{source:`apache`,compressible:!1,extensions:[`x3dv`,`x3dvz`]},"model/x3d+xml":{source:`iana`,compressible:!0,extensions:[`x3d`,`x3dz`]},"model/x3d-vrml":{source:`iana`,extensions:[`x3dv`]},"multipart/alternative":{source:`iana`,compressible:!1},"multipart/appledouble":{source:`iana`},"multipart/byteranges":{source:`iana`},"multipart/digest":{source:`iana`},"multipart/encrypted":{source:`iana`,compressible:!1},"multipart/form-data":{source:`iana`,compressible:!1},"multipart/header-set":{source:`iana`},"multipart/mixed":{source:`iana`},"multipart/multilingual":{source:`iana`},"multipart/parallel":{source:`iana`},"multipart/related":{source:`iana`,compressible:!1},"multipart/report":{source:`iana`},"multipart/signed":{source:`iana`,compressible:!1},"multipart/vnd.bint.med-plus":{source:`iana`},"multipart/voice-message":{source:`iana`},"multipart/x-mixed-replace":{source:`iana`},"text/1d-interleaved-parityfec":{source:`iana`},"text/cache-manifest":{source:`iana`,compressible:!0,extensions:[`appcache`,`manifest`]},"text/calendar":{source:`iana`,extensions:[`ics`,`ifb`]},"text/calender":{compressible:!0},"text/cmd":{compressible:!0},"text/coffeescript":{extensions:[`coffee`,`litcoffee`]},"text/cql":{source:`iana`},"text/cql-expression":{source:`iana`},"text/cql-identifier":{source:`iana`},"text/css":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`css`]},"text/csv":{source:`iana`,compressible:!0,extensions:[`csv`]},"text/csv-schema":{source:`iana`},"text/directory":{source:`iana`},"text/dns":{source:`iana`},"text/ecmascript":{source:`apache`},"text/encaprtp":{source:`iana`},"text/enriched":{source:`iana`},"text/fhirpath":{source:`iana`},"text/flexfec":{source:`iana`},"text/fwdred":{source:`iana`},"text/gff3":{source:`iana`},"text/grammar-ref-list":{source:`iana`},"text/hl7v2":{source:`iana`},"text/html":{source:`iana`,compressible:!0,extensions:[`html`,`htm`,`shtml`]},"text/jade":{extensions:[`jade`]},"text/javascript":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`js`,`mjs`]},"text/jcr-cnd":{source:`iana`},"text/jsx":{compressible:!0,extensions:[`jsx`]},"text/less":{compressible:!0,extensions:[`less`]},"text/markdown":{source:`iana`,compressible:!0,extensions:[`md`,`markdown`]},"text/mathml":{source:`nginx`,extensions:[`mml`]},"text/mdx":{compressible:!0,extensions:[`mdx`]},"text/mizar":{source:`iana`},"text/n3":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`n3`]},"text/parameters":{source:`iana`,charset:`UTF-8`},"text/parityfec":{source:`iana`},"text/plain":{source:`iana`,compressible:!0,extensions:[`txt`,`text`,`conf`,`def`,`list`,`log`,`in`,`ini`]},"text/provenance-notation":{source:`iana`,charset:`UTF-8`},"text/prs.fallenstein.rst":{source:`iana`},"text/prs.lines.tag":{source:`iana`,extensions:[`dsc`]},"text/prs.prop.logic":{source:`iana`},"text/prs.texi":{source:`iana`},"text/raptorfec":{source:`iana`},"text/red":{source:`iana`},"text/rfc822-headers":{source:`iana`},"text/richtext":{source:`iana`,compressible:!0,extensions:[`rtx`]},"text/rtf":{source:`iana`,compressible:!0,extensions:[`rtf`]},"text/rtp-enc-aescm128":{source:`iana`},"text/rtploopback":{source:`iana`},"text/rtx":{source:`iana`},"text/sgml":{source:`iana`,extensions:[`sgml`,`sgm`]},"text/shaclc":{source:`iana`},"text/shex":{source:`iana`,extensions:[`shex`]},"text/slim":{extensions:[`slim`,`slm`]},"text/spdx":{source:`iana`,extensions:[`spdx`]},"text/strings":{source:`iana`},"text/stylus":{extensions:[`stylus`,`styl`]},"text/t140":{source:`iana`},"text/tab-separated-values":{source:`iana`,compressible:!0,extensions:[`tsv`]},"text/troff":{source:`iana`,extensions:[`t`,`tr`,`roff`,`man`,`me`,`ms`]},"text/turtle":{source:`iana`,charset:`UTF-8`,extensions:[`ttl`]},"text/ulpfec":{source:`iana`},"text/uri-list":{source:`iana`,compressible:!0,extensions:[`uri`,`uris`,`urls`]},"text/vcard":{source:`iana`,compressible:!0,extensions:[`vcard`]},"text/vnd.a":{source:`iana`},"text/vnd.abc":{source:`iana`},"text/vnd.ascii-art":{source:`iana`},"text/vnd.curl":{source:`iana`,extensions:[`curl`]},"text/vnd.curl.dcurl":{source:`apache`,extensions:[`dcurl`]},"text/vnd.curl.mcurl":{source:`apache`,extensions:[`mcurl`]},"text/vnd.curl.scurl":{source:`apache`,extensions:[`scurl`]},"text/vnd.debian.copyright":{source:`iana`,charset:`UTF-8`},"text/vnd.dmclientscript":{source:`iana`},"text/vnd.dvb.subtitle":{source:`iana`,extensions:[`sub`]},"text/vnd.esmertec.theme-descriptor":{source:`iana`,charset:`UTF-8`},"text/vnd.exchangeable":{source:`iana`},"text/vnd.familysearch.gedcom":{source:`iana`,extensions:[`ged`]},"text/vnd.ficlab.flt":{source:`iana`},"text/vnd.fly":{source:`iana`,extensions:[`fly`]},"text/vnd.fmi.flexstor":{source:`iana`,extensions:[`flx`]},"text/vnd.gml":{source:`iana`},"text/vnd.graphviz":{source:`iana`,extensions:[`gv`]},"text/vnd.hans":{source:`iana`},"text/vnd.hgl":{source:`iana`},"text/vnd.in3d.3dml":{source:`iana`,extensions:[`3dml`]},"text/vnd.in3d.spot":{source:`iana`,extensions:[`spot`]},"text/vnd.iptc.newsml":{source:`iana`},"text/vnd.iptc.nitf":{source:`iana`},"text/vnd.latex-z":{source:`iana`},"text/vnd.motorola.reflex":{source:`iana`},"text/vnd.ms-mediapackage":{source:`iana`},"text/vnd.net2phone.commcenter.command":{source:`iana`},"text/vnd.radisys.msml-basic-layout":{source:`iana`},"text/vnd.senx.warpscript":{source:`iana`},"text/vnd.si.uricatalogue":{source:`apache`},"text/vnd.sosi":{source:`iana`},"text/vnd.sun.j2me.app-descriptor":{source:`iana`,charset:`UTF-8`,extensions:[`jad`]},"text/vnd.trolltech.linguist":{source:`iana`,charset:`UTF-8`},"text/vnd.vcf":{source:`iana`},"text/vnd.wap.si":{source:`iana`},"text/vnd.wap.sl":{source:`iana`},"text/vnd.wap.wml":{source:`iana`,extensions:[`wml`]},"text/vnd.wap.wmlscript":{source:`iana`,extensions:[`wmls`]},"text/vnd.zoo.kcl":{source:`iana`},"text/vtt":{source:`iana`,charset:`UTF-8`,compressible:!0,extensions:[`vtt`]},"text/wgsl":{source:`iana`,extensions:[`wgsl`]},"text/x-asm":{source:`apache`,extensions:[`s`,`asm`]},"text/x-c":{source:`apache`,extensions:[`c`,`cc`,`cxx`,`cpp`,`h`,`hh`,`dic`]},"text/x-component":{source:`nginx`,extensions:[`htc`]},"text/x-fortran":{source:`apache`,extensions:[`f`,`for`,`f77`,`f90`]},"text/x-gwt-rpc":{compressible:!0},"text/x-handlebars-template":{extensions:[`hbs`]},"text/x-java-source":{source:`apache`,extensions:[`java`]},"text/x-jquery-tmpl":{compressible:!0},"text/x-lua":{extensions:[`lua`]},"text/x-markdown":{compressible:!0,extensions:[`mkd`]},"text/x-nfo":{source:`apache`,extensions:[`nfo`]},"text/x-opml":{source:`apache`,extensions:[`opml`]},"text/x-org":{compressible:!0,extensions:[`org`]},"text/x-pascal":{source:`apache`,extensions:[`p`,`pas`]},"text/x-processing":{compressible:!0,extensions:[`pde`]},"text/x-sass":{extensions:[`sass`]},"text/x-scss":{extensions:[`scss`]},"text/x-setext":{source:`apache`,extensions:[`etx`]},"text/x-sfv":{source:`apache`,extensions:[`sfv`]},"text/x-suse-ymp":{compressible:!0,extensions:[`ymp`]},"text/x-uuencode":{source:`apache`,extensions:[`uu`]},"text/x-vcalendar":{source:`apache`,extensions:[`vcs`]},"text/x-vcard":{source:`apache`,extensions:[`vcf`]},"text/xml":{source:`iana`,compressible:!0,extensions:[`xml`]},"text/xml-external-parsed-entity":{source:`iana`},"text/yaml":{compressible:!0,extensions:[`yaml`,`yml`]},"video/1d-interleaved-parityfec":{source:`iana`},"video/3gpp":{source:`iana`,extensions:[`3gp`,`3gpp`]},"video/3gpp-tt":{source:`iana`},"video/3gpp2":{source:`iana`,extensions:[`3g2`]},"video/av1":{source:`iana`},"video/bmpeg":{source:`iana`},"video/bt656":{source:`iana`},"video/celb":{source:`iana`},"video/dv":{source:`iana`},"video/encaprtp":{source:`iana`},"video/evc":{source:`iana`},"video/ffv1":{source:`iana`},"video/flexfec":{source:`iana`},"video/h261":{source:`iana`,extensions:[`h261`]},"video/h263":{source:`iana`,extensions:[`h263`]},"video/h263-1998":{source:`iana`},"video/h263-2000":{source:`iana`},"video/h264":{source:`iana`,extensions:[`h264`]},"video/h264-rcdo":{source:`iana`},"video/h264-svc":{source:`iana`},"video/h265":{source:`iana`},"video/h266":{source:`iana`},"video/iso.segment":{source:`iana`,extensions:[`m4s`]},"video/jpeg":{source:`iana`,extensions:[`jpgv`]},"video/jpeg2000":{source:`iana`},"video/jpm":{source:`apache`,extensions:[`jpm`,`jpgm`]},"video/jxsv":{source:`iana`},"video/lottie+json":{source:`iana`,compressible:!0},"video/matroska":{source:`iana`},"video/matroska-3d":{source:`iana`},"video/mj2":{source:`iana`,extensions:[`mj2`,`mjp2`]},"video/mp1s":{source:`iana`},"video/mp2p":{source:`iana`},"video/mp2t":{source:`iana`,extensions:[`ts`,`m2t`,`m2ts`,`mts`]},"video/mp4":{source:`iana`,compressible:!1,extensions:[`mp4`,`mp4v`,`mpg4`]},"video/mp4v-es":{source:`iana`},"video/mpeg":{source:`iana`,compressible:!1,extensions:[`mpeg`,`mpg`,`mpe`,`m1v`,`m2v`]},"video/mpeg4-generic":{source:`iana`},"video/mpv":{source:`iana`},"video/nv":{source:`iana`},"video/ogg":{source:`iana`,compressible:!1,extensions:[`ogv`]},"video/parityfec":{source:`iana`},"video/pointer":{source:`iana`},"video/quicktime":{source:`iana`,compressible:!1,extensions:[`qt`,`mov`]},"video/raptorfec":{source:`iana`},"video/raw":{source:`iana`},"video/rtp-enc-aescm128":{source:`iana`},"video/rtploopback":{source:`iana`},"video/rtx":{source:`iana`},"video/scip":{source:`iana`},"video/smpte291":{source:`iana`},"video/smpte292m":{source:`iana`},"video/ulpfec":{source:`iana`},"video/vc1":{source:`iana`},"video/vc2":{source:`iana`},"video/vnd.cctv":{source:`iana`},"video/vnd.dece.hd":{source:`iana`,extensions:[`uvh`,`uvvh`]},"video/vnd.dece.mobile":{source:`iana`,extensions:[`uvm`,`uvvm`]},"video/vnd.dece.mp4":{source:`iana`},"video/vnd.dece.pd":{source:`iana`,extensions:[`uvp`,`uvvp`]},"video/vnd.dece.sd":{source:`iana`,extensions:[`uvs`,`uvvs`]},"video/vnd.dece.video":{source:`iana`,extensions:[`uvv`,`uvvv`]},"video/vnd.directv.mpeg":{source:`iana`},"video/vnd.directv.mpeg-tts":{source:`iana`},"video/vnd.dlna.mpeg-tts":{source:`iana`},"video/vnd.dvb.file":{source:`iana`,extensions:[`dvb`]},"video/vnd.fvt":{source:`iana`,extensions:[`fvt`]},"video/vnd.hns.video":{source:`iana`},"video/vnd.iptvforum.1dparityfec-1010":{source:`iana`},"video/vnd.iptvforum.1dparityfec-2005":{source:`iana`},"video/vnd.iptvforum.2dparityfec-1010":{source:`iana`},"video/vnd.iptvforum.2dparityfec-2005":{source:`iana`},"video/vnd.iptvforum.ttsavc":{source:`iana`},"video/vnd.iptvforum.ttsmpeg2":{source:`iana`},"video/vnd.motorola.video":{source:`iana`},"video/vnd.motorola.videop":{source:`iana`},"video/vnd.mpegurl":{source:`iana`,extensions:[`mxu`,`m4u`]},"video/vnd.ms-playready.media.pyv":{source:`iana`,extensions:[`pyv`]},"video/vnd.nokia.interleaved-multimedia":{source:`iana`},"video/vnd.nokia.mp4vr":{source:`iana`},"video/vnd.nokia.videovoip":{source:`iana`},"video/vnd.objectvideo":{source:`iana`},"video/vnd.planar":{source:`iana`},"video/vnd.radgamettools.bink":{source:`iana`},"video/vnd.radgamettools.smacker":{source:`apache`},"video/vnd.sealed.mpeg1":{source:`iana`},"video/vnd.sealed.mpeg4":{source:`iana`},"video/vnd.sealed.swf":{source:`iana`},"video/vnd.sealedmedia.softseal.mov":{source:`iana`},"video/vnd.uvvu.mp4":{source:`iana`,extensions:[`uvu`,`uvvu`]},"video/vnd.vivo":{source:`iana`,extensions:[`viv`]},"video/vnd.youtube.yt":{source:`iana`},"video/vp8":{source:`iana`},"video/vp9":{source:`iana`},"video/webm":{source:`apache`,compressible:!1,extensions:[`webm`]},"video/x-f4v":{source:`apache`,extensions:[`f4v`]},"video/x-fli":{source:`apache`,extensions:[`fli`]},"video/x-flv":{source:`apache`,compressible:!1,extensions:[`flv`]},"video/x-m4v":{source:`apache`,extensions:[`m4v`]},"video/x-matroska":{source:`apache`,compressible:!1,extensions:[`mkv`,`mk3d`,`mks`]},"video/x-mng":{source:`apache`,extensions:[`mng`]},"video/x-ms-asf":{source:`apache`,extensions:[`asf`,`asx`]},"video/x-ms-vob":{source:`apache`,extensions:[`vob`]},"video/x-ms-wm":{source:`apache`,extensions:[`wm`]},"video/x-ms-wmv":{source:`apache`,compressible:!1,extensions:[`wmv`]},"video/x-ms-wmx":{source:`apache`,extensions:[`wmx`]},"video/x-ms-wvx":{source:`apache`,extensions:[`wvx`]},"video/x-msvideo":{source:`apache`,extensions:[`avi`]},"video/x-sgi-movie":{source:`apache`,extensions:[`movie`]},"video/x-smv":{source:`apache`,extensions:[`smv`]},"x-conference/x-cooltalk":{source:`apache`,extensions:[`ice`]},"x-shader/x-fragment":{compressible:!0},"x-shader/x-vertex":{compressible:!0}}})),gf=e.er(((t,n)=>{n.exports=(hf(),e.rr(pf).default)})),_f=e.er(((e,t)=>{var n=gf();t.exports=function(){var e={};return Object.keys(n).forEach(function(t){var r=n[t];r.extensions&&r.extensions.length>0&&r.extensions.forEach(function(n){e[n]=t})}),e}})),vf=e.er(((e,t)=>{var n=Object.prototype.toString;t.exports=function(e){var t;return n.call(e)===`[object Object]`&&(t=Object.getPrototypeOf(e),t===null||t===Object.getPrototypeOf({}))}})),yf=e.er(((e,t)=>{var n=vf();t.exports=function(e,t){if(!n(e))throw TypeError(`Expected a plain object`);t||={},typeof t==`function`&&(t={compare:t});var r=t.deep,i=[],a=[],o=function(e){var s=i.indexOf(e);if(s!==-1)return a[s];var c={},l=Object.keys(e).sort(t.compare);i.push(e),a.push(c);for(var u=0;u{var n=yf();t.exports.desc=function(e){return n(e,function(e,t){return t.length-e.length})},t.exports.asc=function(e){return n(e,function(e,t){return e.length-t.length})}})),xf=e.er(((e,t)=>{var n=_f(),r=bf();t.exports=e=>{let t=r.desc(n()),i=Object.keys(t).filter(t=>e.endsWith(t));return i.length===0?[]:i.map(e=>({ext:e,mime:t[e]}))},t.exports.mime=e=>{let t=r.desc(n()),i=Object.keys(t).filter(n=>t[n]===e);return i.length===0?[]:i.map(e=>({ext:e,mime:t[e]}))}})),Sf=e.ir(xf(),1),Cf=class extends Error{},wf=(e,t)=>{let n=Sf.default.mime(t);return n.length===1?`${e}.${n[0].ext}`:e};function Tf(e,n,i=()=>{}){let a=new Set,o=0,c=0,l=0,u=()=>a.size,d=()=>o/l;n={showBadge:!0,showProgressBar:!0,...n};let f=(p,m,h)=>{a.add(m),l+=m.getTotalBytes();let g=t.BrowserWindow.fromWebContents(h);if(!g)throw Error(`Failed to get window from web contents.`);if(n.directory&&!r.default.isAbsolute(n.directory))throw Error("The `directory` option must be an absolute path");let _=n.directory??t.app.getPath(`downloads`),v;if(n.filename)v=r.default.join(_,n.filename);else{let e=m.getFilename(),t=r.default.extname(e)?e:wf(e,m.getMimeType());v=n.overwrite?r.default.join(_,t):sf(r.default.join(_,t))}let y=n.errorMessage??`The download of {filename} was interrupted`;n.saveAs?m.setSaveDialogOptions({defaultPath:v,...n.dialogOptions}):m.setSavePath(v),m.on(`updated`,()=>{o=c;for(let e of a)o+=e.getReceivedBytes();if(n.showBadge&&[`darwin`,`linux`].includes(s.default.platform)&&(t.app.badgeCount=u()),!g.isDestroyed()&&n.showProgressBar&&g.setProgressBar(d()),typeof n.onProgress==`function`){let e=m.getReceivedBytes(),t=m.getTotalBytes();n.onProgress({percent:t?e/t:0,transferredBytes:e,totalBytes:t})}typeof n.onTotalProgress==`function`&&n.onTotalProgress({percent:d(),transferredBytes:o,totalBytes:l})}),m.on(`done`,(d,p)=>{if(c+=m.getTotalBytes(),a.delete(m),n.showBadge&&[`darwin`,`linux`].includes(s.default.platform)&&(t.app.badgeCount=u()),!g.isDestroyed()&&!u()&&(g.setProgressBar(-1),o=0,c=0,l=0),n.unregisterWhenDone&&e.removeListener(`will-download`,f),p===`cancelled`)typeof n.onCancel==`function`&&n.onCancel(m),i(new Cf);else if(p===`interrupted`){let e=ff(y,{filename:r.default.basename(v)});i(Error(e))}else if(p===`completed`){let e=m.getSavePath();s.default.platform===`darwin`&&t.app.dock.downloadFinished(e),n.openFolderWhenDone&&t.shell.showItemInFolder(e),typeof n.onCompleted==`function`&&n.onCompleted({fileName:m.getFilename(),filename:m.getFilename(),path:e,fileSize:m.getReceivedBytes(),mimeType:m.getMimeType(),url:m.getURL()}),i(null,m)}}),typeof n.onStarted==`function`&&n.onStarted(m)};e.on(`will-download`,f)}async function Ef(e,t,n){return new Promise((r,i)=>{n={...n,unregisterWhenDone:!0},Tf(e.webContents.session,n,(e,t)=>{e?i(e):r(t)}),e.webContents.downloadURL(t)})}if(typeof t.default==`string`)throw TypeError(`Not running in an Electron environment!`);var{env:Df}=process,Of=`ELECTRON_IS_DEV`in Df,kf=Number.parseInt(Df.ELECTRON_IS_DEV,10)===1,Af=Of?kf:!t.default.app.isPackaged,Q=e=>e.webContents??(e.id&&e),$=e=>(t={})=>(t.transform&&!t.click&&(e.transform=t.transform),e),jf=e=>{let t;return e.filter(e=>e!==void 0&&e!==!1&&e.visible!==!1&&e.visible!==``).filter((e,n,r)=>{let i=e.type===`separator`&&(!t||n===r.length-1||r[n+1].type===`separator`);return t=i?t:e,!i})},Mf=(e,n)=>{let r=(r,i)=>{if(typeof n.shouldShowMenu==`function`&&n.shouldShowMenu(r,i)===!1)return;let{editFlags:a}=i,o=i.selectionText.length>0,c=!!i.linkURL,l=e=>a[`can${e}`]&&o,u={separator:()=>({type:`separator`}),learnSpelling:$({id:`learnSpelling`,label:`&Learn Spelling`,visible:!!(i.isEditable&&o&&i.misspelledWord),click(){Q(e).session.addWordToSpellCheckerDictionary(i.misspelledWord)}}),lookUpSelection:$({id:`lookUpSelection`,label:`Look Up “{selection}”`,visible:s.default.platform===`darwin`&&o&&!c,click(){s.default.platform===`darwin`&&Q(e).showDefinitionForSelection()}}),searchWithGoogle:$({id:`searchWithGoogle`,label:`&Search with Google`,visible:o,click(){let e=new URL(`https://www.google.com/search`);e.searchParams.set(`q`,i.selectionText),t.default.shell.openExternal(e.toString())}}),cut:$({id:`cut`,label:`Cu&t`,enabled:l(`Cut`),visible:i.isEditable,click(n){let r=Q(e);!n.transform&&r?r.cut():(i.selectionText=n.transform?n.transform(i.selectionText):i.selectionText,t.default.clipboard.writeText(i.selectionText))}}),copy:$({id:`copy`,label:`&Copy`,enabled:l(`Copy`),visible:i.isEditable||o,click(n){let r=Q(e);!n.transform&&r?r.copy():(i.selectionText=n.transform?n.transform(i.selectionText):i.selectionText,t.default.clipboard.writeText(i.selectionText))}}),paste:$({id:`paste`,label:`&Paste`,enabled:a.canPaste,visible:i.isEditable,click(n){let r=Q(e);if(n.transform){let e=t.default.clipboard.readText(i.selectionText);e=n.transform?n.transform(e):e,r.insertText(e)}else r.paste()}}),selectAll:$({id:`selectAll`,label:`Select &All`,click(){Q(e).selectAll()}}),saveImage:$({id:`saveImage`,label:`Save I&mage`,visible:i.mediaType===`image`,click(t){i.srcURL=t.transform?t.transform(i.srcURL):i.srcURL,Ef(e,i.srcURL)}}),saveImageAs:$({id:`saveImageAs`,label:`Sa&ve Image As…`,visible:i.mediaType===`image`,click(t){i.srcURL=t.transform?t.transform(i.srcURL):i.srcURL,Ef(e,i.srcURL,{saveAs:!0})}}),saveVideo:$({id:`saveVideo`,label:`Save Vide&o`,visible:i.mediaType===`video`,click(t){i.srcURL=t.transform?t.transform(i.srcURL):i.srcURL,Ef(e,i.srcURL)}}),saveVideoAs:$({id:`saveVideoAs`,label:`Save Video& As…`,visible:i.mediaType===`video`,click(t){i.srcURL=t.transform?t.transform(i.srcURL):i.srcURL,Ef(e,i.srcURL,{saveAs:!0})}}),copyLink:$({id:`copyLink`,label:`Copy Lin&k`,visible:i.linkURL.length>0&&i.mediaType===`none`,click(e){i.linkURL=e.transform?e.transform(i.linkURL):i.linkURL,t.default.clipboard.write({bookmark:i.linkText,text:i.linkURL})}}),saveLinkAs:$({id:`saveLinkAs`,label:`Save Link As…`,visible:i.linkURL.length>0&&i.mediaType===`none`,click(t){i.linkURL=t.transform?t.transform(i.linkURL):i.linkURL,Ef(e,i.linkURL,{saveAs:!0})}}),copyImage:$({id:`copyImage`,label:`Cop&y Image`,visible:i.mediaType===`image`,click(){Q(e).copyImageAt(i.x,i.y)}}),copyImageAddress:$({id:`copyImageAddress`,label:`C&opy Image Address`,visible:i.mediaType===`image`,click(e){i.srcURL=e.transform?e.transform(i.srcURL):i.srcURL,t.default.clipboard.write({bookmark:i.srcURL,text:i.srcURL})}}),copyVideoAddress:$({id:`copyVideoAddress`,label:`Copy Video Ad&dress`,visible:i.mediaType===`video`,click(e){i.srcURL=e.transform?e.transform(i.srcURL):i.srcURL,t.default.clipboard.write({bookmark:i.srcURL,text:i.srcURL})}}),inspect:()=>({id:`inspect`,label:`I&nspect Element`,click(){Q(e).inspectElement(i.x,i.y),Q(e).isDevToolsOpened()&&Q(e).devToolsWebContents.focus()}}),services:()=>({id:`services`,label:`Services`,role:`services`,visible:s.default.platform===`darwin`&&(i.isEditable||o)})},d=typeof n.showInspectElement==`boolean`?n.showInspectElement:Af,f=n.showSelectAll||n.showSelectAll!==!1&&s.default.platform!==`darwin`;function p(t){return{id:`dictionarySuggestions`,label:t,visible:!!(i.isEditable&&o&&i.misspelledWord),click(t){Q(e).replaceMisspelling(t.label)}}}let m=[];o&&i.misspelledWord&&i.dictionarySuggestions.length>0?m=i.dictionarySuggestions.map(e=>p(e)):m.push({id:`dictionarySuggestions`,label:`No Guesses Found`,visible:!!(o&&i.misspelledWord),enabled:!1});let h=[m.length>0&&u.separator(),...m,u.separator(),n.showLearnSpelling!==!1&&u.learnSpelling(),u.separator(),n.showLookUpSelection!==!1&&u.lookUpSelection(),u.separator(),n.showSearchWithGoogle!==!1&&u.searchWithGoogle(),u.separator(),u.cut(),u.copy(),u.paste(),f&&u.selectAll(),u.separator(),n.showSaveImage&&u.saveImage(),n.showSaveImageAs&&u.saveImageAs(),n.showCopyImage!==!1&&u.copyImage(),n.showCopyImageAddress&&u.copyImageAddress(),n.showSaveVideo&&u.saveVideo(),n.showSaveVideoAs&&u.saveVideoAs(),n.showCopyVideoAddress&&u.copyVideoAddress(),u.separator(),n.showCopyLink!==!1&&u.copyLink(),n.showSaveLinkAs&&u.saveLinkAs(),u.separator(),d&&u.inspect(),n.showServices&&u.services(),u.separator()];if(n.menu&&(h=n.menu(u,i,e,m,r)),n.prepend){let t=n.prepend(u,i,e,r);Array.isArray(t)&&h.unshift(...t)}if(n.append){let t=n.append(u,i,e,r);Array.isArray(t)&&h.push(...t)}h=jf(h);for(let e of h)if(n.labels&&n.labels[e.id]&&(e.label=n.labels[e.id]),typeof e.label==`string`&&e.label.includes(`{selection}`)){let t=typeof i.selectionText==`string`?i.selectionText.trim():``;e.label=e.label.replace(`{selection}`,tf(t,25).replaceAll(`&`,`&&`))}if(h.length>0){let r=t.default.Menu.buildFromTemplate(h);typeof n.onShow==`function`&&r.on(`menu-will-show`,n.onShow),typeof n.onClose==`function`&&r.on(`menu-will-close`,n.onClose),r.popup(e)}};return Q(e).on(`context-menu`,r),()=>{e?.isDestroyed?.()||Q(e).removeListener(`context-menu`,r)}};function Nf(e={}){if(s.default.type===`renderer`)throw Error(`Cannot use electron-context-menu in the renderer process!`);let n=!1,r=[],i=t=>{if(n)return;let i=Mf(t,e),a=()=>{i()};Q(t).once(`destroyed`,a),r.push(a)},a=()=>{for(let e of r)e();r.length=0,n=!0};if(e.window){let t=e.window;if(Q(t)===void 0){let e=()=>{i(t)};return(t.addEventListener??t.addListener)(`dom-ready`,e,{once:!0}),r.push(()=>{t.removeEventListener(`dom-ready`,e,{once:!0})}),a}return i(t),a}for(let e of t.default.BrowserWindow.getAllWindows())i(e);let o=(e,t)=>{i(t)};return t.default.app.on(`browser-window-created`,o),r.push(()=>{t.default.app.removeListener(`browser-window-created`,o)}),a}function Pf(e,t){if(!El(e))return!1;try{return new URL(e).origin===t}catch{return!1}}function Ff(e,t,n){return n&&If(e,t,n)?!0:Lf(e,t)}function If(e,t,n){let r=Rf(e),i=Rf(t);return r==null||i==null||!El(e)||!El(t)||r.origin!==n||i.origin!==n?!1:r.pathname===i.pathname}function Lf(e,t){let n=Rf(e),r=Rf(t);return n==null||r==null||n.protocol!==`file:`||r.protocol!==`file:`?!1:n.pathname===r.pathname}function Rf(e){try{return new URL(e)}catch{return null}}var zf=e.sn(`window-manager`),Bf=`http://localhost:5175/`,Vf=` - - - - Codex - - -
-

Something went wrong…

-
- -`,Hf=`#00000000`,Uf=`#000000`,Wf=`#f9f9f9`,Gf=320,Kf=140,qf=400,Jf=400,Yf=36,Xf=`#1f1f1f`,Zf=`#ffffff`;function Qf(){return{color:Hf,symbolColor:t.nativeTheme.shouldUseDarkColors?Zf:Xf,height:Yf}}var $f=class{readyWebContentsIds=new Set;pendingMessages=new Map;primaryWindows=new Map;windowAppearances=new Map;windowHostIds=new Map;webContentsHostIds=new Map;contextsByHostId=new Map;debugWindow=null;isAppQuitting=!1;rendererRecoveryAttempts=new Set;constructor(e){this.options=e}markWebContentsReady(e){this.readyWebContentsIds.add(e.id),this.flushPendingMessages(e)}forgetWebContents(e){this.readyWebContentsIds.delete(e),this.pendingMessages.delete(e)}isWebContentsReady(e){return this.readyWebContentsIds.has(e)}markAppQuitting(){this.isAppQuitting=!0;for(let[e,t]of this.primaryWindows.entries())t&&!t.isDestroyed()&&this.persistPrimaryWindowBounds(t,e)}showPrimaryWindow(e){let t=this.primaryWindows.get(e)??null;return!t||t.isDestroyed()?!1:(t.isMinimized()&&t.restore(),t.show(),t.focus(),!0)}sendMessageToWindow(e,t){this.sendMessageToWebContents(e.webContents,t)}sendMessageToAllRegisteredWindows(e){for(let n of t.BrowserWindow.getAllWindows())n.isDestroyed()||this.windowHostIds.has(n.id)&&this.sendMessageToWindow(n,e)}sendMessageToAllWindows(e,n){for(let r of t.BrowserWindow.getAllWindows())r.isDestroyed()||this.windowHostIds.get(r.id)===e&&this.sendMessageToWindow(r,n)}sendMessageToWebContents(e,t){if(e.isDestroyed())return;if(this.isWebContentsReady(e.id)){e.send(I,t);return}let n=this.pendingMessages.get(e.id)??[];n.push(t),this.pendingMessages.set(e.id,n)}getPrimaryWindow(e){let t=this.primaryWindows.get(e)??null;return!t||t.isDestroyed()?null:t}refreshWindowBackdropForHost(e){let n=this.isOpaqueWindowsEnabled(e);for(let r of t.BrowserWindow.getAllWindows()){if(r.isDestroyed()||this.windowHostIds.get(r.id)!==e)continue;let i=this.windowAppearances.get(r.id);if(i==null)continue;let{backgroundColor:a,backgroundMaterial:o}=ap({platform:process.platform,appearance:i,opaqueWindowsEnabled:n,prefersDarkColors:t.nativeTheme.shouldUseDarkColors});r.setBackgroundColor(a),o!=null&&r.setBackgroundMaterial(o)}}registerContext(e,n){this.contextsByHostId.set(e,n);for(let r of t.BrowserWindow.getAllWindows())r.isDestroyed()||this.windowHostIds.get(r.id)===e&&n.registerWindow(r)}getContext(e){return this.contextsByHostId.get(e)??null}getContextForWebContents(e){let t=this.webContentsHostIds.get(e.id)??null;return t?this.contextsByHostId.get(t)??null:null}getHostIdForWebContents(e){return this.webContentsHostIds.get(e.id)??null}isTrustedIpcSender(e,t){return t!=null&&t!==e.mainFrame?!1:rp((t??e.mainFrame).url,this.options.moduleDir)}async openDebugWindow(){if(!this.options.allowDebugMenu)throw Error(`Debug window disabled for this build flavor.`);let e=this.debugWindow;if(e&&!e.isDestroyed())return e.show(),e.focus(),e;let t=await this.createWindow({appearance:`hud`,title:`Codex Debug`,width:500,height:800,initialRoute:`/debug`});return this.debugWindow=t,t.on(`closed`,()=>{this.debugWindow===t&&(this.debugWindow=null)}),t}async createWindow(n={}){let{title:i,width:o=1280,height:s=820,appearance:c=`primary`,show:l=!0,initialRoute:u,hostId:d=g,startupComplete:f=!0}=n,p=this.isOpaqueWindowsEnabled(d),m=op({appearance:c,opaqueWindowsEnabled:p,platform:process.platform}),h=c===`primary`,_=h?this.restorePrimaryWindowBounds(d):null,v=_?.width??o,y=_?.height??s,b=_?.x,x=_?.y,S=_?.isMaximized===!0,C=this.getPrimaryMinimumSize(d),w=c===`primary`?C:c===`hotkeyWindowHome`?{width:Gf,height:Kf}:c===`hotkeyWindowThread`?{width:qf,height:Jf}:{width:400,height:400},{backgroundColor:T,backgroundMaterial:E}=ap({platform:process.platform,appearance:c,opaqueWindowsEnabled:p,prefersDarkColors:t.nativeTheme.shouldUseDarkColors}),D=new t.BrowserWindow({width:v,height:y,...b===void 0||x===void 0?{}:{x:b,y:x},title:i??t.app.getName(),backgroundColor:T,show:l,...process.platform===`win32`?{autoHideMenuBar:!0}:{},...E==null?{}:{backgroundMaterial:E},...m,minWidth:w.width,minHeight:w.height,webPreferences:{preload:this.options.preloadPath,contextIsolation:!0,nodeIntegration:!1,spellcheck:!1,devTools:this.options.allowDevtools}}),O=this.installWindowsTitleBarOverlaySync(D,c);process.platform===`win32`&&D.removeMenu(),D.on(`closed`,()=>{O?.()}),(c===`primary`||c===`hud`||c===`hotkeyWindowHome`||c===`hotkeyWindowThread`)&&D.on(`page-title-updated`,e=>{e.preventDefault(),D.isDestroyed()||D.setTitle(D.getTitle())}),this.installWebContentsDiagnostics(D),this.registerWindow(D,d,h,c);let k=this.contextsByHostId.get(d);k&&k.registerWindow(D),h&&D.on(`close`,e=>{this.persistPrimaryWindowBounds(D,d);let t=Array.from(this.primaryWindows.values()).some(e=>e!==D&&!e.isDestroyed());if(process.platform===`darwin`&&!this.isAppQuitting&&!t){if(D.isFullScreen()){e.preventDefault(),D.once(`leave-full-screen`,()=>{D.isDestroyed()||D.hide()}),D.setFullScreen(!1);return}e.preventDefault(),D.hide()}});let A=()=>{this.sendMessageToWindow(D,{type:`window-fullscreen-changed`,isFullScreen:D.isFullScreen()})};D.on(`enter-full-screen`,A),D.on(`leave-full-screen`,A),A();let j=()=>{this.sendMessageToWindow(D,{type:`electron-window-focus-changed`,isFocused:D.isFocused()})};D.on(`focus`,j),D.on(`blur`,j),D.on(`show`,j),D.on(`hide`,j),j(),S&&D.once(`ready-to-show`,()=>{D.isDestroyed()||D.maximize()});let M=tp();if(D.webContents.setWindowOpenHandler(({url:e})=>M&&Pf(e,M)?{action:`allow`}:(El(e)&&t.shell.openExternal(e).catch(e=>{this.options.errorReporter.reportNonFatal(e,{kind:`open-external-window`})}),{action:`deny`})),D.webContents.on(`will-navigate`,(e,n)=>{Ff(n,D.webContents.getURL(),M)||(e.preventDefault(),El(n)&&t.shell.openExternal(n).catch(e=>{this.options.errorReporter.reportNonFatal(e,{kind:`open-external-navigation`})}))}),this.installNativeContextMenu(D),!t.app.isPackaged){let e=new URL(ep());return u&&e.searchParams.set(`initialRoute`,u),e.searchParams.set(`hostId`,d),f||e.searchParams.set(`startupComplete`,`false`),D.loadURL(e.toString()),D}let N=(0,r.join)(e.u(this.options.moduleDir),`index.html`);if((0,a.existsSync)(N))try{return await D.loadURL(e.l(u,d,f)),D}catch(e){zf().warning(`Failed to load bundle at`,{safe:{},sensitive:{indexPath:N,error:e}})}let P=`data:text/html;charset=utf-8,${encodeURIComponent(Vf)}`;return await D.loadURL(P),D}async createAuxWindow(e,t){let n=await this.createWindow({title:e,width:1024,height:720,appearance:`secondary`,show:!1,hostId:t});return n.setMenuBarVisibility(!1),n.setMinimumSize(400,400),n.center(),n}async ensureAuxWindow(e,t,n){let r=t.byOwner.get(e.id);if(r&&!r.window.isDestroyed())return r;r&&(t.byOwner.delete(e.id),t.byWindowId.delete(r.window.webContents.id));let i=this.webContentsHostIds.get(e.id)??`local`,a=await this.createAuxWindow(n,i),o=a.webContents.id,s={window:a,owner:e,ownerId:e.id,ready:this.isWebContentsReady(o)};return t.byOwner.set(e.id,s),t.byWindowId.set(o,s),a.on(`closed`,()=>{t.byWindowId.delete(o),t.byOwner.get(e.id)?.window===a&&t.byOwner.delete(e.id)}),e.once(`destroyed`,()=>{a.isDestroyed()||a.close()}),s}registerWindow(e,t,n,r){this.windowAppearances.set(e.id,r),this.windowHostIds.set(e.id,t),this.webContentsHostIds.set(e.webContents.id,t),n&&this.primaryWindows.set(t,e);let i=e.webContents.id;e.on(`closed`,()=>{this.forgetWebContents(i),this.windowAppearances.delete(e.id),this.windowHostIds.delete(e.id),this.webContentsHostIds.delete(i),n&&this.primaryWindows.get(t)===e&&this.primaryWindows.delete(t)})}restorePrimaryWindowBounds(e){let t=this.options.getGlobalStateForHost(e).get(`electron-main-window-bounds`)??null;if(!t)return null;let n=this.clampPrimaryWindowBounds(t,e);return this.isWithinDisplayWorkArea(n)?n:null}getPrimaryMinimumSize(e){return(this.options.getGlobalStateForHost(e).get(`electron-persisted-atom-state`)??{})[`review-open`]===!0?{width:1200,height:800}:{width:800,height:600}}syncPrimaryMinimumSize(e){let t=this.getPrimaryWindow(e);if(!t)return;let n=this.getPrimaryMinimumSize(e);if(t.setMinimumSize(n.width,n.height),t.isMaximized()||t.isFullScreen())return;let r=t.getNormalBounds(),i=Math.max(r.width,n.width),a=Math.max(r.height,n.height);i===r.width&&a===r.height||(t.setBounds({...r,width:i,height:a}),this.persistPrimaryWindowBounds(t,e))}persistPrimaryWindowBounds(e,t){if(e.isDestroyed())return;let n=e.getNormalBounds(),r=this.getPrimaryMinimumSize(t),i={x:n.x,y:n.y,width:Math.max(n.width,r.width),height:Math.max(n.height,r.height),isMaximized:e.isMaximized()};this.options.getGlobalStateForHost(t).set(`electron-main-window-bounds`,i)}clampPrimaryWindowBounds(e,t){let n=this.getPrimaryMinimumSize(t);return{...e,width:Math.max(e.width,n.width),height:Math.max(e.height,n.height)}}flushPendingMessages(e){let t=this.pendingMessages.get(e.id);if(!t||t.length===0){this.pendingMessages.delete(e.id);return}if(this.pendingMessages.delete(e.id),!e.isDestroyed())for(let n of t)e.send(I,n)}isWithinDisplayWorkArea(e){let n=t.screen.getAllDisplays();if(n.length===0)return!0;let r={x:e.x,y:e.y,width:e.width,height:e.height};return n.some(e=>{let t=e.workArea;return t.xr.x&&t.yr.y})}installWindowsTitleBarOverlaySync(e,n){if(process.platform!==`win32`||n!==`primary`)return;let r=()=>{e.isDestroyed()||e.setTitleBarOverlay(Qf())};return t.nativeTheme.on(`updated`,r),r(),()=>{t.nativeTheme.off(`updated`,r)}}isOpaqueWindowsEnabled(t){return this.options.getGlobalStateForHost(t).get(e.$n.OPAQUE_WINDOWS)===!0}installNativeContextMenu(e){let t=Nf({window:e,showInspectElement:this.options.allowInspectElement});e.on(`closed`,()=>{t()})}installWebContentsDiagnostics(e){let t=e.webContents,n=e.id,r=t.id,i=this.options.errorReporter;t.on(`render-process-gone`,(t,a)=>{a.reason!==`clean-exit`&&(i.reportFatal(Error(`Renderer process gone (${a.reason})`),{tags:{errorType:`renderer-process-gone`,reason:a.reason},extra:{windowId:n,webContentsId:r,exitCode:a.exitCode}}),this.maybeRecoverFromRendererCrash(e,a.reason))}),t.on(`did-finish-load`,()=>{this.rendererRecoveryAttempts.delete(r)}),t.on(`unresponsive`,()=>{i.reportFatal(Error(`Renderer unresponsive`),{tags:{errorType:`renderer-unresponsive`},extra:{windowId:n,webContentsId:r,url:wc(t.getURL())}})}),t.on(`did-fail-load`,(e,t,a,o,s,c,l)=>{s&&t!==-3&&i.reportFatal(Error(`Renderer did-fail-load`),{tags:{errorType:`renderer-did-fail-load`},extra:{windowId:n,webContentsId:r,errorCode:t,errorDescription:a,validatedURL:wc(o),frameProcessId:c,frameRoutingId:l}})})}maybeRecoverFromRendererCrash(e,t){if(this.isAppQuitting||t===`clean-exit`||e.isDestroyed())return;let n=e.webContents;n.isDestroyed()||this.rendererRecoveryAttempts.has(n.id)||(this.rendererRecoveryAttempts.add(n.id),setTimeout(()=>{if(!e.isDestroyed()&&!e.webContents.isDestroyed())try{e.webContents.reload()}catch(e){zf().warning(`Failed to reload crashed window`,{safe:{},sensitive:{error:e}})}},500))}};function ep(){return process.env.ELECTRON_RENDERER_URL||Bf}function tp(){let e=ep();return El(e)?new URL(e).origin:null}function np(e){return(0,r.join)(e,`..`,`..`,`webview`,`index.html`)}function rp(e,t){if(e===`about:blank`||ip(e))return!0;let n=tp();if(n&&Pf(e,n))return!0;let r=(0,d.pathToFileURL)(np(t)).toString();return!!e.startsWith(r)}function ip(e){try{let t=new URL(e);return t.protocol===`app:`?t.host===`-`||t.host===``:!1}catch{return!1}}function ap({platform:e,appearance:t,opaqueWindowsEnabled:n,prefersDarkColors:r}){return e===`win32`&&t!==`hotkeyWindowHome`&&t!==`hotkeyWindowThread`?n?{backgroundColor:r?Uf:Wf,backgroundMaterial:`none`}:{backgroundColor:Hf,backgroundMaterial:`mica`}:{backgroundColor:Hf,backgroundMaterial:null}}function op({appearance:e,opaqueWindowsEnabled:t,platform:n}){switch(e){case`hotkeyWindowHome`:return{frame:!1,transparent:!0,hasShadow:!0,resizable:!1,minimizable:!1,maximizable:!1,fullscreenable:!1,alwaysOnTop:!0,skipTaskbar:!0,...n===`darwin`?{type:`panel`}:{}};case`hotkeyWindowThread`:return{frame:!1,transparent:!0,hasShadow:!0,resizable:!0,minimizable:!1,maximizable:!1,fullscreenable:!1,alwaysOnTop:!0,skipTaskbar:!0,...n===`darwin`?{type:`panel`}:{}};case`primary`:return n===`darwin`?t?{titleBarStyle:`hiddenInset`,trafficLightPosition:{x:16,y:16}}:{vibrancy:`menu`,titleBarStyle:`hiddenInset`,trafficLightPosition:{x:16,y:16}}:n===`win32`?{titleBarStyle:`hidden`,titleBarOverlay:Qf()}:{titleBarStyle:`default`};case`secondary`:return n===`darwin`?t?{titleBarStyle:`default`}:{vibrancy:`menu`,titleBarStyle:`default`}:{titleBarStyle:`default`};case`hud`:return n===`darwin`?t?{titleBarStyle:`hiddenInset`,minimizable:!1,maximizable:!1,fullscreenable:!1,alwaysOnTop:!0,trafficLightPosition:{x:10,y:10}}:{vibrancy:`menu`,visualEffectState:`active`,titleBarStyle:`hiddenInset`,minimizable:!1,maximizable:!1,fullscreenable:!1,alwaysOnTop:!0,trafficLightPosition:{x:10,y:10}}:{titleBarStyle:`default`,minimizable:!1,maximizable:!1,fullscreenable:!1,alwaysOnTop:!0}}}var sp=`.codex-global-state.json`,cp=800,lp=600,up=1100,dp=760,fp=2e3,pp=e.Jn.object({x:e.Jn.number(),y:e.Jn.number(),width:e.Jn.number(),height:e.Jn.number(),isMaximized:e.Jn.boolean().optional()});async function mp({moduleDir:n}){let i=gp(),o=op({appearance:`primary`,opaqueWindowsEnabled:!1,platform:process.platform}),{backgroundColor:s,backgroundMaterial:c}=ap({platform:process.platform,appearance:`primary`,opaqueWindowsEnabled:!1,prefersDarkColors:t.nativeTheme.shouldUseDarkColors}),l=new t.BrowserWindow({title:t.app.getName(),show:!1,x:i?.x,y:i?.y,width:i?.width??up,height:i?.height??dp,backgroundColor:s,...process.platform===`win32`?{autoHideMenuBar:!0}:{},...c==null?{}:{backgroundMaterial:c},...o,minWidth:cp,minHeight:lp,webPreferences:{preload:(0,r.join)(n,`preload.js`),contextIsolation:!0,sandbox:!1}});if(i?.isMaximized===!0&&l.maximize(),!t.app.isPackaged){let t=new URL(ep());return t.searchParams.set(`hostId`,g),t.searchParams.set(`startupComplete`,`false`),e.an().info(`Loading startup shell from dev renderer URL`,{safe:{hostId:g}}),l.loadURL(t.toString()),l}let u=(0,r.join)(e.u(n),`index.html`);if((0,a.existsSync)(u))try{return e.an().info(`Loading startup shell from app protocol`,{safe:{hostId:g}}),await l.loadURL(e.l(void 0,g,!1)),l}catch(t){return e.an().warning(`Failed to load startup shell from app protocol`,{safe:{},sensitive:{error:t}}),e.an().info(`Loading startup shell from packaged index file`,{safe:{hostId:g}}),await l.loadFile(u,{query:{hostId:g,startupComplete:`false`}}),l}return await l.loadURL(`data:text/html;charset=utf-8,Startup failed`),l}async function hp(t){if(!t.webContents.isLoadingMainFrame()){e.an().info(`Startup shell finished loading before wait attached`,{safe:{webContentsId:t.webContents.id}});return}await new Promise(n=>{let r=()=>{t.webContents.off(`did-stop-loading`,i),t.off(`closed`,a),clearTimeout(o)},i=()=>{e.an().info(`Startup shell main frame stopped loading`,{safe:{webContentsId:t.webContents.id}}),r(),n()},a=()=>{e.an().warning(`Startup shell window closed before load finished`,{safe:{webContentsId:t.webContents.id}}),r(),n()},o=setTimeout(()=>{e.an().warning(`Startup shell load timed out`,{safe:{timeoutMs:fp,webContentsId:t.webContents.id}}),r(),n()},fp);o.unref(),t.webContents.on(`did-stop-loading`,i),t.on(`closed`,a)})}function gp(){let n=(0,r.join)(e.yn(process.env),sp);if(!(0,a.existsSync)(n))return null;try{let e=JSON.parse((0,a.readFileSync)(n,`utf8`)),r=pp.safeParse(e[`electron-main-window-bounds`]);if(!r.success)return null;let i={...r.data,width:Math.max(r.data.width,cp),height:Math.max(r.data.height,lp)},o=t.screen.getAllDisplays();return o.length===0||o.some(e=>{let t=e.workArea;return t.xi.x&&t.yi.y})?i:null}catch(t){return e.an().info(`Falling back to default startup shell window bounds`,{safe:{hasCodexHomeOverride:process.env.CODEX_HOME!=null},sensitive:{error:t instanceof Error?t.message:`unknown-bounds-read-error`}}),null}}var _p=class{stores={byOwner:new Map,byWindowId:new Map};constructor(e,t){this.windowManager=e,this.messageChannel=t}async open(e,t){let n=await this.windowManager.ensureAuxWindow(e,this.stores,`File`);n.latest=t,this.updateTitle(n,t.filePath),n.ready&&this.sendNavigation(n,t),n.window.isDestroyed()||(n.window.show(),n.window.focus())}handleRendererReady(e){let t=this.stores.byWindowId.get(e);t&&(t.ready=!0,t.latest&&(this.updateTitle(t,t.latest.filePath),this.sendNavigation(t,t.latest)))}sendNavigation(e,t){if(e.window.isDestroyed())return;let n={type:`navigate-to-route`,path:`/file-preview`,state:{filePath:t.filePath,contents:t.contents,line:t.line,column:t.column}};e.window.webContents.send(this.messageChannel,n)}updateTitle(e,t){if(e.window.isDestroyed())return;let n=vp(t);e.window.setTitle(n)}};function vp(e){let t=e.split(/[/\\]+/);return t[t.length-1]??e}var yp=470,bp=yp,xp=340,Sp=yp,Cp=640,wp=400,Tp=400,Ep=52,Dp=110,Op=1200,kp=class{logger=e.on(`hotkey-window-controller`);configuredWindowIds=new Set;mode=`hidden`;isDisposed=!1;lastVisibleSurface=`home`;homeWindow=null;homeWindowPromise=null;threadWindow=null;lastDetailRoute=null;threadWindowPromise=null;threadSize={width:Sp,height:Cp};transitionInFlight=null;syncingWindowPositions=!1;homePointerInteractive=!0;homeMousePassthroughEnabled=!1;constructor(e,t=g){this.windowManager=e,this.hostId=t}async toggleHotkey(){if(this.transitionInFlight)return;if(this.mode===`hidden`){await this.showLastVisibleSurface();return}let t=this.getWindowForCurrentMode();if(t!=null&&!t.isFocused()){let n=this.lastDetailRoute;this.mode===`threadVisible`&&n!=null&&e.Sn(n)!=null&&t===this.threadWindow&&this.navigateToRoute(t,n,Ap()),this.showAndFocus(t);return}this.hideAll()}prewarm(){this.prewarmWindows()}async openThread(t){this.transitionInFlight||await this.openDetailRoute(e.xn(t))}async openDetailRoute(e){if(!this.transitionInFlight){if(this.lastDetailRoute=e,this.mode===`hidden`&&await this.showHome(),this.mode===`threadVisible`){let t=await this.ensureThreadWindow();this.showAndFocus(t),this.navigateToDetailRoute(t,e),this.lastVisibleSurface=`thread`;return}this.mode===`homeVisible`&&await this.startExpandTransition(e)}}async openHome(t=null){if(!this.transitionInFlight){if(this.mode===`hidden`){await this.showHome(t);return}if(this.mode===`homeVisible`){let n=await this.ensureHomeWindow();this.applyHotkeyWindowWindowPolicy(n),this.navigateToRoute(n,e.bn,jp(t)),this.showAndFocus(n),this.lastVisibleSurface=`home`,this.applyHomeInteractivityPolicy();return}this.mode===`threadVisible`&&await this.startCollapseTransition(t)}}async collapseToHome(){if(!this.transitionInFlight){if(this.mode===`hidden`){await this.showHome();return}this.mode===`threadVisible`&&await this.startCollapseTransition()}}setHomePointerInteraction(e,t){this.isHomeOrigin(e)&&(this.transitionInFlight||this.homePointerInteractive!==t.isInteractive&&(this.homePointerInteractive=t.isInteractive,this.applyHomeInteractivityPolicy()))}handleTransitionDone(e,t){let n=this.transitionInFlight;if(n&&n.id===t.transitionId){if(n.stage===`awaiting-source-raised`){if(t.step!==`raised`||e.id!==n.sourceWebContentsId)return;this.clearTransitionTimeout(),this.swapWindowsAfterRaise(n),n.stage=`awaiting-destination-lowered`,this.startTransitionTimeout(n);let r=this.getWindowByWebContentsId(n.destinationWebContentsId);r&&this.sendTransition(r,n.id,`lower-curtain`);return}n.stage===`awaiting-destination-lowered`&&t.step===`lowered`&&e.id===n.destinationWebContentsId&&this.finishTransition(n)}}hideAll(){this.clearTransitionTimeout(),this.transitionInFlight=null,this.resetHomeInteractivity(),this.homeWindow&&!this.homeWindow.isDestroyed()&&(this.homeWindow.blur(),this.homeWindow.hide()),this.threadWindow&&!this.threadWindow.isDestroyed()&&(this.threadWindow.blur(),this.threadWindow.hide()),this.mode=`hidden`}dispose(){this.isDisposed=!0,this.hideAll(),this.clearTransitionTimeout(),this.transitionInFlight=null,this.lastDetailRoute=null,this.closeWindow(this.homeWindow),this.closeWindow(this.threadWindow),this.homeWindow=null,this.threadWindow=null,this.mode=`hidden`}async prewarmWindows(){let e=await this.ensureHomeWindow(),t=await this.ensureThreadWindow();if(this.isDisposed){this.closeWindow(e),this.closeWindow(t);return}e.isDestroyed()||e.hide(),t.isDestroyed()||t.hide()}async showHome(t=null){this.homePointerInteractive=!0;let n=this.homeWindow!=null&&!this.homeWindow.isDestroyed(),r=this.threadWindow!=null&&!this.threadWindow.isDestroyed(),i=await this.ensureHomeWindow();if(r)this.alignHomeToThread();else if(!n){let e=this.computeHomeBounds(this.getTargetDisplay());i.setBounds(e,!1)}this.applyHotkeyWindowWindowPolicy(i),this.navigateToRoute(i,e.bn,jp(t)),this.showAndFocus(i),this.mode=`homeVisible`,this.lastVisibleSurface=`home`,this.applyHomeInteractivityPolicy();let a=await this.ensureThreadWindow();this.alignThreadToHome(),a.hide()}async showThread(){let e=this.lastDetailRoute;if(!e)return!1;let t=this.threadWindow!=null&&!this.threadWindow.isDestroyed(),n=await this.ensureThreadWindow();return t||n.setBounds(this.computeThreadBounds(this.getTargetDisplay()),!1),this.homeWindow&&!this.homeWindow.isDestroyed()&&this.alignThreadToHome(),this.applyHotkeyWindowWindowPolicy(n),this.navigateToDetailRoute(n,e),this.showAndFocus(n),this.homeWindow&&!this.homeWindow.isDestroyed()&&this.homeWindow.hide(),this.resetHomeInteractivity(),this.mode=`threadVisible`,this.lastVisibleSurface=`thread`,!0}async showLastVisibleSurface(){this.lastVisibleSurface===`thread`&&await this.showThread()||await this.showHome()}async startExpandTransition(e){let t=await this.ensureHomeWindow(),n=await this.ensureThreadWindow();this.lastDetailRoute=e,this.alignThreadToHome(),this.applyHotkeyWindowWindowPolicy(t),this.applyHotkeyWindowWindowPolicy(n),this.navigateToDetailRoute(n,e),this.resetHomeInteractivity();let r=this.beginTransition({kind:`expand`,sourceWindow:t,destinationWindow:n});this.sendTransition(n,r,`raise-curtain`),this.sendTransition(t,r,`raise-curtain`)}async startCollapseTransition(t=null){let n=await this.ensureHomeWindow(),r=await this.ensureThreadWindow();this.alignHomeToThread(),this.applyHotkeyWindowWindowPolicy(n),this.applyHotkeyWindowWindowPolicy(r),this.navigateToRoute(n,e.bn,jp(t)),this.resetHomeInteractivity();let i=this.beginTransition({kind:`collapse`,sourceWindow:r,destinationWindow:n});this.sendTransition(n,i,`raise-curtain`),this.sendTransition(r,i,`raise-curtain`)}beginTransition({kind:e,sourceWindow:t,destinationWindow:n}){let r=(0,c.randomUUID)(),i=setTimeout(()=>{this.handleTransitionTimeout(r)},Op);return i.unref(),this.transitionInFlight={id:r,kind:e,stage:`awaiting-source-raised`,sourceWebContentsId:t.webContents.id,destinationWebContentsId:n.webContents.id,timeout:i},r}handleTransitionTimeout(e){let t=this.transitionInFlight;!t||t.id!==e||(this.logger.warning(`Hotkey Window transition timed out; forcing completion`,{safe:{transitionId:e,stage:t.stage,kind:t.kind},sensitive:{}}),this.swapWindowsAfterRaise(t),this.finishTransition(t))}startTransitionTimeout(e){this.clearTransitionTimeout(),e.timeout=setTimeout(()=>{this.handleTransitionTimeout(e.id)},Op),e.timeout.unref()}clearTransitionTimeout(){this.transitionInFlight&&clearTimeout(this.transitionInFlight.timeout)}swapWindowsAfterRaise(e){let t=this.getWindowByWebContentsId(e.sourceWebContentsId),n=this.getWindowByWebContentsId(e.destinationWebContentsId);n&&(this.applyHotkeyWindowWindowPolicy(n),this.showAndFocus(n),t&&!t.isDestroyed()&&t.hide())}finishTransition(e){this.clearTransitionTimeout();let t=this.getWindowByWebContentsId(e.sourceWebContentsId),n=this.getWindowByWebContentsId(e.destinationWebContentsId);t&&this.sendTransition(t,e.id,`commit`),n&&this.sendTransition(n,e.id,`commit`),this.transitionInFlight=null,this.mode=e.kind===`expand`?`threadVisible`:`homeVisible`,this.lastVisibleSurface=e.kind===`expand`?`thread`:`home`,this.applyHomeInteractivityPolicy()}async ensureHomeWindow(){if(this.homeWindow&&!this.homeWindow.isDestroyed())return this.homeWindow;if(this.homeWindowPromise!=null)return this.homeWindowPromise;let e=this.windowManager.createWindow({title:t.app.getName(),width:bp,height:xp,appearance:`hotkeyWindowHome`,show:!1,initialRoute:`/hotkey-window`,hostId:this.hostId}).then(e=>this.isDisposed?(this.closeWindow(e),e):(e.setMenuBarVisibility(!1),e.on(`move`,()=>{this.transitionInFlight||this.syncingWindowPositions||this.alignThreadToHome()}),e.on(`closed`,()=>{this.configuredWindowIds.delete(e.id),this.homeMousePassthroughEnabled=!1,this.homePointerInteractive=!0,this.homeWindow===e&&(this.homeWindow=null),this.mode!==`threadVisible`&&(this.mode=`hidden`)}),this.applyHotkeyWindowWindowPolicy(e),this.homeWindow=e,e)).finally(()=>{this.homeWindowPromise===e&&(this.homeWindowPromise=null)});return this.homeWindowPromise=e,e}async ensureThreadWindow(){if(this.threadWindow&&!this.threadWindow.isDestroyed())return this.threadWindow;if(this.threadWindowPromise!=null)return this.threadWindowPromise;let e=this.windowManager.createWindow({title:t.app.getName(),width:this.threadSize.width,height:this.threadSize.height,appearance:`hotkeyWindowThread`,show:!1,initialRoute:`/hotkey-window`,hostId:this.hostId}).then(e=>this.isDisposed?(this.closeWindow(e),e):(e.setMenuBarVisibility(!1),e.setBounds(this.computeThreadBounds(this.getTargetDisplay()),!1),e.setMinimumSize(wp,Tp),e.on(`move`,()=>{this.transitionInFlight||this.syncingWindowPositions||this.alignHomeToThread()}),e.on(`resize`,()=>{if(e.isDestroyed())return;let t=e.getBounds();this.threadSize={width:Math.max(t.width,wp),height:Math.max(t.height,Tp)},!(this.transitionInFlight||this.syncingWindowPositions)&&this.alignHomeToThread()}),e.on(`closed`,()=>{this.configuredWindowIds.delete(e.id),this.threadWindow===e&&(this.threadWindow=null),this.mode!==`homeVisible`&&(this.mode=`hidden`)}),this.applyHotkeyWindowWindowPolicy(e),this.threadWindow=e,e)).finally(()=>{this.threadWindowPromise===e&&(this.threadWindowPromise=null)});return this.threadWindowPromise=e,e}applyHotkeyWindowWindowPolicy(e){e.isDestroyed()||(this.configuredWindowIds.has(e.id)||(this.configuredWindowIds.add(e.id),process.platform===`darwin`?(e.setVisibleOnAllWorkspaces(!0,{visibleOnFullScreen:!0,skipTransformProcessType:!0}),e.setAlwaysOnTop(!0,`screen-saver`,1)):(e.setAlwaysOnTop(!0,`screen-saver`),e.setVisibleOnAllWorkspaces(!0))),e.moveTop())}showAndFocus(e){e.isDestroyed()||(e.isMinimized()&&e.restore(),this.applyHotkeyWindowWindowPolicy(e),e.show(),e.focus())}getTargetDisplay(){let e=t.screen.getCursorScreenPoint();return t.screen.getDisplayNearestPoint(e)}computeHomeBounds(e){let t=e.workArea,n=Math.min(bp,t.width),r=Math.min(xp,t.height),i=this.computeThreadBounds(e);return{x:Math.round(t.x+(t.width-n)/2),y:Mp(Math.round(i.y+i.height-r),t.y,t.y+t.height-r),width:n,height:r}}computeThreadBounds(e){let t=e.workArea,n=Math.min(Math.max(this.threadSize.width,wp),t.width),r=Math.min(this.threadSize.height,t.height),i=Math.round(t.x+(t.width-n)/2),a=Math.round(t.y+Ep);return{x:Mp(i,t.x,t.x+t.width-n),y:Mp(a,t.y,t.y+t.height-r),width:n,height:r}}alignThreadToHome(){if(!this.homeWindow||this.homeWindow.isDestroyed()||!this.threadWindow||this.threadWindow.isDestroyed())return;let e=this.homeWindow.getBounds(),n=this.threadWindow.getBounds(),r=t.screen.getDisplayMatching(e).workArea,i=Math.round(e.x+(e.width-n.width)/2),a=Math.round(e.y+e.height-n.height),o=Mp(i,r.x,r.x+r.width-n.width),s=Mp(a,r.y,r.y+r.height-n.height);this.setWindowPosition(this.threadWindow,o,s)}alignHomeToThread(){if(!this.homeWindow||this.homeWindow.isDestroyed()||!this.threadWindow||this.threadWindow.isDestroyed())return;let e=this.homeWindow.getBounds(),t=this.threadWindow.getBounds(),n=Math.max(t.width,wp),r=Math.round(t.x+(t.width-n)/2),i=Math.round(t.y+t.height-e.height);if(!(e.x===r&&e.y===i&&e.width===n)){this.syncingWindowPositions=!0;try{this.homeWindow.setBounds({...e,x:r,y:i,width:n},!1)}finally{this.syncingWindowPositions=!1}}}setWindowPosition(e,t,n){if(e.isDestroyed())return;let r=e.getBounds();if(!(r.x===t&&r.y===n)){this.syncingWindowPositions=!0;try{e.setBounds({...r,x:t,y:n},!1)}finally{this.syncingWindowPositions=!1}}}closeWindow(e){!e||e.isDestroyed()||e.close()}resetHomeInteractivity(){this.homePointerInteractive=!0,this.disableHomeMousePassthrough()}disableHomeMousePassthrough(){if(!this.homeWindow||this.homeWindow.isDestroyed()){this.homeMousePassthroughEnabled=!1;return}this.homeMousePassthroughEnabled&&(this.homeMousePassthroughEnabled=!1,this.homeWindow.setIgnoreMouseEvents(!1))}applyHomeInteractivityPolicy(){if(!this.homeWindow||this.homeWindow.isDestroyed()){this.homeMousePassthroughEnabled=!1;return}if(this.transitionInFlight||this.mode!==`homeVisible`){this.disableHomeMousePassthrough();return}let e=!this.homePointerInteractive;if(this.homeMousePassthroughEnabled!==e){if(this.homeMousePassthroughEnabled=e,e){this.homeWindow.setIgnoreMouseEvents(!0,{forward:!0});return}this.homeWindow.setIgnoreMouseEvents(!1)}}getWindowByWebContentsId(e){return this.homeWindow&&!this.homeWindow.isDestroyed()&&this.homeWindow.webContents.id===e?this.homeWindow:this.threadWindow&&!this.threadWindow.isDestroyed()&&this.threadWindow.webContents.id===e?this.threadWindow:null}navigateToRoute(e,t,n){this.windowManager.sendMessageToWindow(e,{type:`navigate-to-route`,path:t,state:n})}navigateToDetailRoute(t,n){this.navigateToRoute(t,n,e.Sn(n)==null?void 0:Ap())}sendTransition(e,t,n){this.windowManager.sendMessageToWindow(e,{type:`hotkey-window-transition`,transitionId:t,step:n,durationMs:Dp})}isHomeOrigin(e){return this.homeWindow!=null&&!this.homeWindow.isDestroyed()&&this.homeWindow.webContents.id===e.id}getWindowForCurrentMode(){let e=this.mode===`homeVisible`?this.homeWindow:this.mode===`threadVisible`?this.threadWindow:null;return e==null||e.isDestroyed()?null:e}};function Ap(){return{focusComposerNonce:Date.now()}}function jp(e){return{focusComposerNonce:Date.now(),...e==null?{}:{prefillCwd:e}}}function Mp(e,t,n){return en?n:e}var Np=new Set([`cmdorctrl`,`command`,`cmd`,`control`,`ctrl`,`alt`,`option`]),Pp=new Set([...Np,`shift`]);function Fp(e){let t=e.split(`+`).map(e=>e.trim()).filter(e=>e.length>0);if(t.length===0)return`Shortcut cannot be empty.`;let n=!1,r=null;for(let e of t){let t=e.toLowerCase();if(Pp.has(t)){Np.has(t)&&(n=!0);continue}if(r!=null)return`Shortcut must include exactly one non-modifier key.`;r=e}return r==null?`Shortcut must include a non-modifier key.`:n?null:`Shortcut must include Cmd/Ctrl or Alt.`}var Ip={"hotkey-window-enabled-changed":!0,"open-in-hotkey-window":!0,"hotkey-window-collapse-to-home":!0,"hotkey-window-dismiss":!0,"hotkey-window-transition-done":!0,"hotkey-window-home-pointer-interaction-changed":!0},Lp=class{hotkeyWindowController=null;isHotkeyWindowGateEnabled=!1;isHotkeyWindowActive=!1;devHotkeyWindowHotkeyOverrideEnabled=!1;registeredHotkeyWindowHotkey=null;configuredHotkeyWindowHotkey;hotkeyController={getState:()=>this.getHotkeyWindowHotkeyState(),setHotkey:e=>this.setHotkeyWindowHotkey(e),setDevOverrideEnabled:e=>this.setHotkeyWindowDevHotkeyOverride(e)};constructor(t){this.options=t;let n=t.globalState.get(e.$n.HOTKEY_WINDOW_HOTKEY);this.configuredHotkeyWindowHotkey=n!=null&&Fp(n)==null?n:null}getHotkeyController(){return this.hotkeyController}async handleMessage(t,n){if(!Rp(n))return!1;switch(n.type){case`hotkey-window-enabled-changed`:return this.setHotkeyWindowGateEnabled(n.enabled),!0;case`open-in-hotkey-window`:return!this.isHotkeyWindowActive||!e.Cn(n.path)?!0:n.path===`/hotkey-window`?(await this.ensureHotkeyWindowController().openHome(n.prefillCwd??null),!0):(await this.ensureHotkeyWindowController().openDetailRoute(n.path),!0);case`hotkey-window-collapse-to-home`:return this.isHotkeyWindowActive&&await this.ensureHotkeyWindowController().collapseToHome(),!0;case`hotkey-window-dismiss`:{let e=this.getActiveHotkeyWindowController();return e&&e.hideAll(),!0}case`hotkey-window-transition-done`:{let e=this.getActiveHotkeyWindowController();return e&&e.handleTransitionDone(t.sender,n),!0}case`hotkey-window-home-pointer-interaction-changed`:{let e=this.getActiveHotkeyWindowController();return e&&e.setHomePointerInteraction(t.sender,n),!0}}}dispose(){this.deactivateLifecycle()}hide(){this.getActiveHotkeyWindowController()?.hideAll()}ensureHotkeyWindowController(){return this.hotkeyWindowController??=new kp(this.options.windowManager),this.hotkeyWindowController}isHotkeyWindowEffectivelyEnabled(){return this.isHotkeyWindowGateEnabled&&this.configuredHotkeyWindowHotkey!=null&&(!this.options.isDevMode||this.devHotkeyWindowHotkeyOverrideEnabled)}getHotkeyWindowHotkeyState(){return{supported:!0,configuredHotkey:this.configuredHotkeyWindowHotkey,isGateEnabled:this.isHotkeyWindowGateEnabled,isDevMode:this.options.isDevMode,isDevOverrideEnabled:this.devHotkeyWindowHotkeyOverrideEnabled,isActive:this.isHotkeyWindowActive}}createHotkeyWindowMutationFailure(e){return{success:!1,error:e,state:this.getHotkeyWindowHotkeyState()}}handleHotkeyWindowHotkeyPressed(){this.isHotkeyWindowActive&&this.ensureHotkeyWindowController().toggleHotkey()}registerHotkeyWindowHotkeyOrThrow(e){if(this.registeredHotkeyWindowHotkey===e)return;let n=this.registeredHotkeyWindowHotkey;if(!t.globalShortcut.register(e,()=>{this.handleHotkeyWindowHotkeyPressed()}))throw Error(`Unable to register hotkey window hotkey: ${e}`);n!=null&&t.globalShortcut.unregister(n),this.registeredHotkeyWindowHotkey=e}unregisterHotkeyWindowHotkey(){this.registeredHotkeyWindowHotkey&&=(t.globalShortcut.unregister(this.registeredHotkeyWindowHotkey),null)}deactivateLifecycle(){this.unregisterHotkeyWindowHotkey(),this.hotkeyWindowController?.dispose(),this.hotkeyWindowController=null,this.isHotkeyWindowActive=!1}applyLifecycleOrThrow(){if(!this.isHotkeyWindowEffectivelyEnabled()){this.deactivateLifecycle();return}if(this.configuredHotkeyWindowHotkey==null)throw Error(`Hotkey Window hotkey is not configured.`);this.registerHotkeyWindowHotkeyOrThrow(this.configuredHotkeyWindowHotkey),this.isHotkeyWindowActive=!0,this.ensureHotkeyWindowController().prewarm()}applyLifecycleWithWarning(t){try{this.applyLifecycleOrThrow()}catch(n){e.an().warning(`Failed to apply hotkey window runtime state`,{safe:{reason:t},sensitive:{error:n}}),this.deactivateLifecycle()}}setHotkeyWindowGateEnabled(e){this.isHotkeyWindowGateEnabled!==e&&(this.isHotkeyWindowGateEnabled=e,this.applyLifecycleWithWarning(`gate-change`))}setHotkeyWindowHotkey(t){if(t!=null){let e=Fp(t);if(e!=null)return this.createHotkeyWindowMutationFailure(e)}let n=this.configuredHotkeyWindowHotkey;this.configuredHotkeyWindowHotkey=t;try{this.applyLifecycleOrThrow()}catch(e){return this.configuredHotkeyWindowHotkey=n,this.applyLifecycleWithWarning(`hotkey-rollback`),this.createHotkeyWindowMutationFailure(e instanceof Error?e.message:String(e))}return this.options.globalState.set(e.$n.HOTKEY_WINDOW_HOTKEY,this.configuredHotkeyWindowHotkey??void 0),{success:!0,state:this.getHotkeyWindowHotkeyState()}}setHotkeyWindowDevHotkeyOverride(e){if(!this.options.isDevMode)return this.createHotkeyWindowMutationFailure(`Dev override is only available in unpackaged builds.`);if(this.devHotkeyWindowHotkeyOverrideEnabled===e)return{success:!0,state:this.getHotkeyWindowHotkeyState()};let t=this.devHotkeyWindowHotkeyOverrideEnabled;this.devHotkeyWindowHotkeyOverrideEnabled=e;try{this.applyLifecycleOrThrow()}catch(e){return this.devHotkeyWindowHotkeyOverrideEnabled=t,this.applyLifecycleWithWarning(`dev-override-rollback`),this.createHotkeyWindowMutationFailure(e instanceof Error?e.message:String(e))}return{success:!0,state:this.getHotkeyWindowHotkeyState()}}getActiveHotkeyWindowController(){return!this.isHotkeyWindowActive||this.hotkeyWindowController==null?null:this.hotkeyWindowController}};function Rp(e){return e.type in Ip}var zp=500,Bp=720,Vp=class{stores={byWindowId:new Map};conversationStateByKey=new Map;constructor(e,t){this.windowManager=e,this.getGlobalStateForHost=t}async open(e,t){let n=this.getHostId(e),r=this.getThreadTitle(n,t.conversationId),i=await this.ensureOverlayWindow(e,n,t,r);i.latest=t,this.isOverlayWebContents(e.id)||(i.owner=e,i.ownerId=e.id),this.updateConversationMapping(i,n,t.conversationId),this.updateOverlayTitle(i,r),this.attachOwnerDestroyedHandler(i),this.requestSnapshot(i,t),this.sendNavigation(i,t),i.ready&&this.showOverlay(i)}handleRendererReady(e){let t=this.stores.byWindowId.get(e);t&&(t.ready=!0,t.latest&&this.sendNavigation(t,t.latest),this.showOverlay(t))}setAlwaysOnTop(e,t){let n=this.stores.byWindowId.get(e);if(!(!n||n.window.isDestroyed())){if(t){n.window.setAlwaysOnTop(!0,`floating`);return}n.window.setAlwaysOnTop(!1)}}handleThreadTitleUpdated(e,t,n){let r=this.getConversationKey(e,t),i=this.conversationStateByKey.get(r);if(i){if(i.window.isDestroyed()){this.conversationStateByKey.delete(r);return}this.updateOverlayTitle(i,n)}}getOverlayConversationId(e){return this.stores.byWindowId.get(e)?.latest?.conversationId??null}async ensureOverlayWindow(e,n,r,i){let a=this.getConversationKey(n,r.conversationId),o=this.conversationStateByKey.get(a);if(o&&!o.window.isDestroyed())return o;o&&(this.removeConversationMapping(o),this.stores.byWindowId.delete(o.window.webContents.id));let s=await this.windowManager.createWindow({title:i??t.app.getName(),width:zp,height:Bp,appearance:`hud`,show:!1,hostId:n,initialRoute:Hp(r.conversationId)});s.setAlwaysOnTop(!1),process.platform===`darwin`&&s.setWindowButtonVisibility(!0),s.once(`ready-to-show`,()=>{s.isDestroyed()||(s.show(),s.focus())}),s.setMenuBarVisibility(!1),s.setMinimumSize(400,400),this.positionOverlayWindow(s,e);let c=s.webContents.id,l={window:s,owner:e,ownerId:e.id,ready:this.windowManager.isWebContentsReady(c),hostId:n};return this.stores.byWindowId.set(c,l),this.updateConversationMapping(l,n,r.conversationId),s.on(`closed`,()=>{this.stores.byWindowId.delete(c),l.ownerDestroyedOwner&&l.ownerDestroyedHandler&&(l.ownerDestroyedOwner.isDestroyed()||l.ownerDestroyedOwner.off(`destroyed`,l.ownerDestroyedHandler),l.ownerDestroyedOwner=void 0,l.ownerDestroyedHandler=void 0),this.removeConversationMapping(l)}),l}sendNavigation(e,t){if(e.window.isDestroyed())return;let n={type:`navigate-to-route`,path:Hp(t.conversationId)};this.windowManager.sendMessageToWindow(e.window,n)}requestSnapshot(e,t){if(e.owner.isDestroyed())return;let n={type:`thread-stream-snapshot-request`,hostId:e.hostId,conversationId:t.conversationId};this.windowManager.sendMessageToWebContents(e.owner,n)}getHostId(e){return this.windowManager.getHostIdForWebContents(e)??`local`}getThreadTitle(t,n){return e.v(this.getGlobalStateForHost(t).get(e.Ln.THREAD_TITLES)).titles[n]??null}updateConversationMapping(e,t,n){this.removeConversationMapping(e),e.hostId=t;let r=this.getConversationKey(t,n);e.conversationKey=r,this.conversationStateByKey.set(r,e)}removeConversationMapping(e){let t=e.conversationKey;t&&(this.conversationStateByKey.get(t)===e&&this.conversationStateByKey.delete(t),e.conversationKey=void 0)}getConversationKey(e,t){return`${e}:${t}`}updateOverlayTitle(e,n){e.window.isDestroyed()||e.window.setTitle(n??t.app.getName())}positionOverlayWindow(e,n){if(e.isDestroyed())return;let r=t.BrowserWindow.fromWebContents(n),i=(r?t.screen.getDisplayMatching(r.getBounds()):t.screen.getPrimaryDisplay()).workArea,a=Math.min(zp,i.width),o=Math.min(Bp,i.height),s=i.width*.2,c=i.height*.1;e.setBounds({x:i.x+i.width-s-a,y:i.y+i.height-c-o,width:a,height:o},!1)}showOverlay(e){e.window.isDestroyed()||(e.window.show(),e.window.focus())}isOverlayWebContents(e){return this.stores.byWindowId.has(e)}attachOwnerDestroyedHandler(e){if(e.owner.isDestroyed()||e.ownerDestroyedOwner===e.owner)return;e.ownerDestroyedOwner&&e.ownerDestroyedHandler&&(e.ownerDestroyedOwner.isDestroyed()||e.ownerDestroyedOwner.off(`destroyed`,e.ownerDestroyedHandler));let t=()=>{let t=e.latest?.conversationId;t&&this.ensureOwnerAvailable(e,e.window.webContents,t)};e.owner.on(`destroyed`,t),e.ownerDestroyedOwner=e.owner,e.ownerDestroyedHandler=t}ensureOwnerAvailable(e,n,r){if(!e.owner.isDestroyed())return!0;let i=this.windowManager.getHostIdForWebContents(n)??e.hostId;if(!i)return!1;let a=t.BrowserWindow.getAllWindows().find(e=>{if(e.isDestroyed())return!1;let t=e.webContents;return t.id===n.id||this.stores.byWindowId.has(t.id)?!1:this.windowManager.getHostIdForWebContents(t)===i})?.webContents;return!a||a.isDestroyed()?!1:(e.owner=a,e.ownerId=a.id,this.updateConversationMapping(e,i,r),this.updateOverlayTitle(e,this.getThreadTitle(i,r)),this.attachOwnerDestroyedHandler(e),this.windowManager.sendMessageToWebContents(a,{type:`thread-stream-resume-request`,hostId:i,conversationId:r}),!0)}};function Hp(e){return`/thread-overlay/${e}`}function Up(n){if(!n.isMacOS)return()=>{};let i=[],a=null,o=async t=>{let i=await Promise.all(t.map(async t=>{try{if(!(await(0,u.stat)(t)).isDirectory())return null}catch(t){return e.an().warning(`Failed to stat workspace path`,{safe:{},sensitive:{error:t}}),null}return(0,r.resolve)(t)})),a=[];for(let e of i)e&&(a.includes(e)||a.push(e));if(a.length===0)return[];let o=n.globalState.get(e.Ln.WORKSPACE_ROOT_OPTIONS)??[];for(let e of a)o=[e,...o.filter(t=>t!==e)];let s=a[a.length-1];return n.globalState.set(e.Ln.WORKSPACE_ROOT_OPTIONS,o),n.globalState.set(e.Ln.ACTIVE_WORKSPACE_ROOTS,[s]),n.windowManager.sendMessageToAllWindows(n.hostId,{type:`workspace-root-options-updated`}),n.windowManager.sendMessageToAllWindows(n.hostId,{type:`active-workspace-roots-updated`}),n.windowManager.sendMessageToAllWindows(n.hostId,{type:`navigate-to-route`,path:`/`,state:{focusComposerNonce:Date.now()}}),a},s=async()=>{i.length!==0&&(await o(i.splice(0,i.length)),i.length>0&&await s())},c=async()=>{if(a)return a;a=s();try{await a}finally{a=null}},l=(e,t)=>{e.preventDefault(),i.push(t),c().catch(e=>{n.errorReporter.reportNonFatal(e,{kind:`workspace-root-drops`})})};return t.app.on(`open-file`,l),()=>t.app.removeListener(`open-file`,l)}var Wp=2e3,Gp=2e3;function Kp({buildFlavor:n,allowDevtools:i,allowInspectElement:o,allowDebugMenu:s,errorReporter:c,globalState:l,getGlobalStateForHost:u,desktopRoot:d,preloadPath:f,repoRoot:p,isMacOS:m,isWindows:h,isDevMode:_,disposables:v}){let y=(0,r.join)(p,`electron`,`src`,`icons`),b=()=>{switch(n){case e.c.Dev:{let e=(0,r.join)(y,t.nativeTheme.shouldUseDarkColors?`icon-dev-dark.png`:`icon-dev-light.png`);return(0,a.existsSync)(e)?e:null}case e.c.Agent:{let e=(0,r.join)(process.resourcesPath,`icon-agent.png`);if(t.app.isPackaged&&(0,a.existsSync)(e))return e;let n=(0,r.join)(y,`icon-agent.png`);return(0,a.existsSync)(n)?n:null}case e.c.Nightly:case e.c.InternalAlpha:case e.c.PublicBeta:case e.c.Prod:return null}},x=()=>{let e=b(),n=e==null?t.nativeImage.createEmpty():t.nativeImage.createFromPath(e);n.isEmpty()||t.app.dock?.setIcon(n)};m&&(x(),t.nativeTheme.on(`updated`,x),v.add(()=>{t.nativeTheme.off(`updated`,x)}));let S=new $f({desktopRoot:d,iconDirectoryName:`icons`,getGlobalStateForHost:u,moduleDir:__dirname,preloadPath:f,repoRoot:p,allowDevtools:i,allowInspectElement:o,allowDebugMenu:s,errorReporter:c});v.add(Up({errorReporter:c,globalState:l,hostId:g,isMacOS:m,windowManager:S}));let C=new _p(S,I),w=new Vp(S,u),T=new Lp({windowManager:S,globalState:l,isDevMode:_}),E=null,D=null,O=null,k=(e,t)=>{if(!h||t!==`local`)return;let n=setTimeout(()=>{e.isDestroyed()||e.isVisible()||(e.show(),e.focus())},Wp);n.unref();let r=()=>{clearTimeout(n)};e.once(`show`,r),e.once(`closed`,r)};return{windowManager:S,filePreviewManager:C,threadOverlayManager:w,hotkeyWindowLifecycleManager:T,setHostBindings:e=>{E=e.getHostConfig,D=e.getContext,O=e.getOrCreateContext},ensureHostWindow:async e=>{let n=S.getPrimaryWindow(e);if(n){if(!(D?.(e)??null)){let t=E?.(e)??null;t&&O?.(t)}return n.isMinimized()&&n.restore(),n.show(),n.focus(),n.isDestroyed()?null:n}let r=E?.(e)??null;if(!r||!O)return null;O(r);let i=r.kind===`local`?t.app.getName():r.display_name,a=r.id!==g,o=await S.createWindow({title:i,hostId:r.id,show:a});return a||k(o,r.id),o},ensureStartupWindow:async()=>{let e=S.getPrimaryWindow(g);return e?(e.isMinimized()&&e.restore(),e.show(),e.focus(),e):S.createWindow({title:t.app.getName(),hostId:g,show:!1,startupComplete:!1})},waitForWindowToFinishLoad:async e=>{e.webContents.isLoadingMainFrame()&&await new Promise(t=>{let n=()=>{e.webContents.off(`did-stop-loading`,r),e.off(`closed`,i),clearTimeout(a)},r=()=>{n(),t()},i=()=>{n(),t()},a=setTimeout(()=>{n(),t()},Gp);a.unref(),e.webContents.on(`did-stop-loading`,r),e.on(`closed`,i)})},showPrimaryWindow:e=>S.showPrimaryWindow(e),markAppQuitting:()=>{S.markAppQuitting()},isTrustedIpcSender:S.isTrustedIpcSender.bind(S),sendMessageToWindow:S.sendMessageToWindow.bind(S),sendMessageToAllRegisteredWindows:S.sendMessageToAllRegisteredWindows.bind(S),sendMessageToAllWindows:S.sendMessageToAllWindows.bind(S),getPrimaryWindow:S.getPrimaryWindow.bind(S),getContext:S.getContext.bind(S),getContextForWebContents:S.getContextForWebContents.bind(S),registerContext:S.registerContext.bind(S)}}var qp=3e4,Jp=class{totalInvocations=0;rollingWindowCounter=new e.vn({windowMs:qp});recordInvocation(e=Date.now()){this.totalInvocations+=1,this.rollingWindowCounter.record(1,e)}getSnapshot(e=Date.now()){let t=this.rollingWindowCounter.getSnapshot(e);return{totalInvocations:this.totalInvocations,invocationsLast30s:t.count}}};function Yp(e){return typeof e==`object`&&!!e}function Xp(e){return e===`worktree-cleanup-inputs`}function Zp(e){return Yp(e)?e.type===`worker-main-rpc-request`&&typeof e.workerId==`string`&&typeof e.requestId==`string`&&Xp(e.method):!1}var Qp=class{worker=null;latestAuthUser=null;listeners=new Set;mainRpcHandler=null;constructor(e){this.id=e}dispose(){let e=this.worker;e&&(this.worker=null,e.terminate())}addListener(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}setMainRpcHandler(e){this.mainRpcHandler=e}postMessage(e){this.ensureWorker().postMessage(e)}setSentryUser(e,t,n){this.latestAuthUser={authMethod:e,userId:t,email:n};let r=this.worker;r&&r.postMessage({type:`worker-sentry-user-update`,authMethod:e,userId:t,email:n})}emitAppEvent(e){let t=this.worker;t&&t.postMessage({type:`worker-app-event`,event:e})}ensureWorker(){if(this.worker)return this.worker;let n=e.on(`worker-manager`),i=(0,r.join)(__dirname,`worker.js`),a=e.C(e.c.resolve()),o=t.app.isPackaged?t.app.getAppPath():process.cwd(),s={workerId:this.id,sentryInitOptions:e.i,maxLogLevel:a,sentryRewriteFramesRoot:o,spawnInsideWsl:e.nn()},c=new f.Worker(i,{name:this.id,workerData:s});return c.on(`message`,e=>{if(Zp(e)){this.handleMainRpcRequest(c,e);return}this.listeners.forEach(t=>{t(e)})}),c.on(`error`,e=>{n.warning(`Worker error`,{safe:{},sensitive:{error:e}})}),c.on(`exit`,e=>{this.worker=null,e!==0&&n.warning(`Worker exited`,{safe:{code:e},sensitive:{}})}),c.unref(),this.worker=c,this.latestAuthUser&&c.postMessage({type:`worker-sentry-user-update`,authMethod:this.latestAuthUser.authMethod,userId:this.latestAuthUser.userId,email:this.latestAuthUser.email}),c}async handleMainRpcRequest(t,n){let r=e.on(`worker-manager`);if(n.workerId!==this.id){r.warning(`Received main RPC request for wrong worker`,{safe:{expectedWorkerId:this.id,workerId:n.workerId,method:n.method}});return}let i=this.mainRpcHandler;if(!i){let e={type:`worker-main-rpc-response`,workerId:this.id,requestId:n.requestId,method:n.method,result:{type:`error`,error:{message:`No main RPC handler configured for worker '${this.id}'`}}};t.postMessage(e);return}try{let e=await i(n),r={type:`worker-main-rpc-response`,workerId:this.id,requestId:n.requestId,method:n.method,result:{type:`ok`,value:e}};t.postMessage(r)}catch(e){let r={type:`worker-main-rpc-response`,workerId:this.id,requestId:n.requestId,method:n.method,result:{type:`error`,error:{message:e instanceof Error?e.message:`Failed to handle main RPC request: ${String(e)}`}}};t.postMessage(r)}}},$p=class{logger=e.on(`worker-bus-message-handler`);pending=new Map;pendingRequestFromHost=new Map;knownWebContents=new Set;workerManager;onWorkerRequestSent;constructor(e,t={}){this.workerId=e,this.onWorkerRequestSent=t.onWorkerRequestSent??null,this.workerManager=new Qp(this.workerId),this.workerManager.addListener(e=>{this.handleWorkerMessage(e)})}async handleMessage(e,t){if(this.trackLifecycle(e),t.workerId!==this.workerId){this.logger.warning(`Received worker message for wrong worker`,{safe:{expectedWorkerId:this.workerId,workerId:t.workerId,type:t.type}});return}switch(t.type){case`worker-request`:this.pending.set(t.request.id,e),this.onWorkerRequestSent?.();break;case`worker-request-cancel`:this.pending.delete(t.id);break}this.workerManager.postMessage(t)}dispose(){let e=Error(`Worker bus disposed for '${this.workerId}'`);for(let t of this.pendingRequestFromHost.values())t.reject(e);this.pendingRequestFromHost.clear(),this.workerManager.dispose()}requestFromHost(t){let n=e.pn(`main-${(0,c.randomUUID)()}`),r={id:n,method:t.method,params:t.params},i={type:`worker-request`,workerId:this.workerId,request:r},a=new Promise((e,r)=>{this.pendingRequestFromHost.set(n,{method:t.method,resolve:t=>{e(t)},reject:r})});try{this.onWorkerRequestSent?.(),this.workerManager.postMessage(i)}catch(e){return this.pendingRequestFromHost.delete(n),Promise.reject(e instanceof Error?e:Error(`Failed to post worker request: ${String(e)}`))}return a}setSentryUser(e,t,n){this.workerManager.setSentryUser(e,t,n)}emitAppEvent(e){this.workerManager.emitAppEvent(e)}setMainRpcHandler(e){this.workerManager.setMainRpcHandler(e)}trackLifecycle(e){this.knownWebContents.has(e)||(this.knownWebContents.add(e),e.once(`destroyed`,()=>{this.knownWebContents.delete(e),this.cleanupOrigin(e)}))}cleanupOrigin(e){for(let[t,n]of this.pending.entries()){if(n!==e)continue;this.pending.delete(t);let r={type:`worker-request-cancel`,workerId:this.workerId,id:t};this.workerManager.postMessage(r)}}handleWorkerMessage(e){if(e.workerId!==this.workerId){this.logger.warning(`Received worker message for wrong worker`,{safe:{expectedWorkerId:this.workerId,workerId:e.workerId,type:e.type}});return}if(e.type===`worker-response`){let t=this.pendingRequestFromHost.get(e.response.id);if(t){if(this.pendingRequestFromHost.delete(e.response.id),t.method!==e.response.method){t.reject(Error(`Mismatched worker response method`));return}if(e.response.result.type===`ok`){t.resolve(e.response.result.value);return}t.reject(Error(e.response.result.error.message));return}let n=this.pending.get(e.response.id);if(!n)return;this.pending.delete(e.response.id),this.send(n,e);return}this.broadcast(e)}send(e,t){e.isDestroyed()||e.send(ze(this.workerId),t)}broadcast(e){this.knownWebContents.forEach(t=>{this.send(t,e)})}};function em({disposables:n,isTrustedIpcEvent:r}){let i=new e.Ut,a=new e.z(i),o=new Jp,s=[],c=new $p(`git`,{onWorkerRequestSent:()=>{o.recordInvocation()}}),l=(e,t,n)=>{s.forEach(r=>{r(e,t,n)})},u=e=>c.requestFromHost(e),d=(e,a)=>{n.add(a),s.push((e,t,n)=>{a.setSentryUser(e,t,n)}),n.add(i.subscribe(e=>{a.emitAppEvent(e)})),t.ipcMain.handle(Re(e),async(e,t)=>{r(e)&&await a.handleMessage(e.sender,t)})};return{appEvent:i,gitManager:a,gitWorkerInvocationSampler:o,requestGitWorker:u,updateWorkerSentryUser:l,registerWorkerBusMessageHandlers:()=>{for(let t of e.fn){if(t===`git`){d(t,c);continue}d(t,new $p(t))}},setGitWorkerMainRpcHandler:e=>{c.setMainRpcHandler(e)}}}var tm=(0,i.promisify)(o.execFile),nm=`--open-project`,rm=`HKCU\\Software\\Classes\\Directory\\shell\\OpenProjectInCodex`,im=`${rm}\\command`;function am(e){return[{key:rm,value:`Open project in Codex`},{key:rm,name:`Icon`,value:`"${e}",0`},{key:im,value:`"${e}" ${nm} "%1"`}]}async function om({key:e,name:t,value:n}){await tm(`reg.exe`,[`add`,e,...t?[`/v`,t]:[`/ve`],`/d`,n,`/f`],{windowsHide:!0})}async function sm({isWindows:t,isPackaged:n,executablePath:r}){if(!(!t||!n))try{await Promise.all(am(r).map(om))}catch(t){e.an().warning(`Failed to register Windows folder context menu`,{safe:{},sensitive:{error:t}})}}function cm(e){return e.replace(/\\/g,`/`)}function lm(t){let n=e.q().replace(/\\/g,`/`),r=G(t).map(cm),i=e=>{let t=cm(e);return r.includes(t)?!1:t===n||t.startsWith(`${n}/`)},a=W(t),o=a.filter(e=>!i(e));o.length!==a.length&&K(t,o);let s=G(t),c=s.filter(e=>!i(e));c.length!==s.length&&ac(t,c)}async function um(){let{startedAtMs:n,buildFlavor:i,desktopSentry:a,sparkleManager:o,setSparkleBridgeHandlers:s,setSecondInstanceArgsHandler:c}=e.r(),l=e.C(i),u=e.c.shouldIncludeSparkle(i,process.platform,process.env),d=e.c.allowDevtools(i),f=i===e.c.Dev||i===e.c.Agent,p=e.c.allowDebugMenu(i),m=e.nt((e,t,n)=>{a.captureException(e,{level:t===`fatal`?`fatal`:`error`,tags:n.tags,extra:n.extra})}),h=t.app.getVersion(),_=e.s(i),v=_==null?h:`${h} • ${_}`,y=new e.it({source:`codex-desktop`,env:i,codexAppSessionId:e.o,buildInfo:{version:h,buildNumber:e.a.value},fetchImpl:(e,n)=>t.net.fetch(e instanceof URL?e.toString():e,n),reportFailure:e=>{m.reportNonFatal(`Datadog log sink failure`,{kind:`datadog-log-sink-failure`,tags:{failureType:e.type,reason:e.reason},extra:{failure:e}})}});Me(m,y,e.o,l);let b=e.cn(`startup`),x=(e,t,r={})=>{let i=Date.now();b.trace(e,{safe:{phaseElapsedMs:i-t,startupElapsedMs:i-n,...r}})};x(`bootstrap handoff complete`,n,{appWhenReadyResolved:t.app.isReady()});let S=process.platform===`darwin`,C=process.platform===`win32`,w=!t.app.isPackaged;C&&t.app.setAppUserModelId(_e(i));let T=new e.at;t.app.setAboutPanelOptions({applicationVersion:v,copyright:`© OpenAI`}),e.an().info(`Launching app`,{safe:{buildFlavor:i,nodeEnv:process.env.NODE_ENV,enableSparkle:u,allowDevtools:d,allowInspectElement:f,allowDebugMenu:p,platform:process.platform,packaged:t.app.isPackaged,agentRunId:process.env.CODEX_ELECTRON_AGENT_RUN_ID?.trim()||null}});let E=Date.now();await t.app.whenReady(),x(`main app.whenReady resolved`,E),E=Date.now(),e.d(e.u(__dirname)),x(`registered app protocol`,E),wu({buildFlavor:i}),E=Date.now();let D=await mp({moduleDir:__dirname});await hp(D),D.show(),D.focus(),x(`startup window loaded`,E),E=Date.now(),await Dd(),x(`shell environment hydrated`,E);let O=xd({moduleDir:__dirname}),k=Kp({buildFlavor:i,allowDevtools:d,allowInspectElement:f,allowDebugMenu:p,errorReporter:m,globalState:O.globalState,getGlobalStateForHost:O.getGlobalStateForHost,desktopRoot:O.desktopRoot,preloadPath:O.preloadPath,repoRoot:O.repoRoot,isMacOS:S,isWindows:C,isDevMode:w,disposables:T}),A=e=>k.isTrustedIpcSender(e.sender,e.senderFrame??null),j=Su();s({onUpdateReadyChanged:e=>{k.sendMessageToAllRegisteredWindows({type:`app-update-ready-changed`,isUpdateReady:e})},onInstallUpdatesRequested:j.allowQuitTemporarilyForUpdateInstall,isTrustedIpcEvent:A});let M=em({disposables:T,isTrustedIpcEvent:A}),N=_u({app:t.app,globalState:O.globalState,getGlobalStateForHost:O.getGlobalStateForHost,windowServices:k,repoRoot:O.repoRoot,errorReporter:m,desktopSentry:a,datadogLogger:y,maxLogLevel:l,sparkleManager:o,filePreviewManager:k.filePreviewManager,threadOverlayManager:k.threadOverlayManager,hotkeyWindowHotkeyController:k.hotkeyWindowLifecycleManager.getHotkeyController(),gitManager:M.gitManager,requestGitWorker:M.requestGitWorker,gitWorkerInvocationSampler:M.gitWorkerInvocationSampler,updateWorkerSentryUser:M.updateWorkerSentryUser,appEvent:M.appEvent});k.setHostBindings({getHostConfig:N.getHostConfig,getContext:N.getWindowContextForHost,getOrCreateContext:N.getOrCreateContext});let P=yd({buildFlavor:i,globalState:O.globalState,errorReporter:m,sparkleManager:o,gitManager:M.gitManager,localHost:N.localHost,upsertHostConfigs:N.upsertHostConfigs,windowServices:k,ensureHostWindow:k.ensureHostWindow,getElectronMessageHandlerForWindow:N.getElectronMessageHandlerForWindow,allowDevtools:d,allowDebugMenu:p,enableSparkle:u,isMacOS:S,appVersion:h});N.setHostActions({refreshApplicationMenu:()=>{P.refreshApplicationMenu()},ensureHostWindow:k.ensureHostWindow}),M.setGitWorkerMainRpcHandler(async e=>{switch(e.method){case`worktree-cleanup-inputs`:return N.getWorktreeCleanupInputsForHost(e.params)}}),M.registerWorkerBusMessageHandlers(),c(e=>{P.deepLinks.queueProcessArgs(e)}),E=Date.now(),P.deepLinks.registerProtocolClient(),x(`registered deep link protocol`,E),Cu({desktopSentry:a,hotkeyWindowLifecycleManager:k.hotkeyWindowLifecycleManager,codexHome:O.codexHome,nativeContextMenuIconSearchRoots:[(0,r.join)(O.repoRoot,`webview`,`public`),(0,r.join)(O.desktopRoot,`webview`)],getContextForWebContents:k.getContextForWebContents,ensureHostWindow:k.ensureHostWindow,navigateToRoute:P.navigateToRoute,isTrustedIpcEvent:A}),Tu({isWindows:C,disableQuitConfirmationPrompt:process.env.CODEX_ELECTRON_DISABLE_QUIT_CONFIRMATION===`1`,quitState:j,windows:k,applicationMenuManager:P.applicationMenuManager,ensureHostWindow:k.ensureHostWindow,hotkeyWindowLifecycleManager:k.hotkeyWindowLifecycleManager,globalStatesByHostId:O.globalStatesByHostId,flushAndDisposeContexts:N.flushAndDisposeContexts,disposables:T,appEvent:M.appEvent,errorReporter:m});let ee=await k.ensureStartupWindow();await k.waitForWindowToFinishLoad(ee),D.isDestroyed()||D.destroy(),ee.show(),ee.focus(),E=Date.now(),lm(O.globalState);let te=N.getOrCreateContext(N.localHost);T.add(fe({appServerConnectionRegistry:te.appServerConnectionRegistry,gitManager:M.gitManager,globalState:O.globalState,hostConfig:N.localHost})),x(`local host context initialized`,E),E=Date.now(),await sm({isWindows:C,isPackaged:t.app.isPackaged,executablePath:process.execPath}),x(`windows folder context menu registered`,E,{isWindows:C}),E=Date.now(),await P.refreshApplicationMenu({triggerProviderRefresh:!0}),x(`application menu refreshed`,E),E=Date.now();let ne=!ee.isDestroyed()&&!ee.webContents.isDestroyed();ne&&k.sendMessageToWindow(ee,{type:`startup-loading-state-changed`,isComplete:!0}),await k.ensureHostWindow(g),x(`startup loading screen completed`,E,{hostId:g,startupWindowClosed:!ne}),E=Date.now(),await P.deepLinks.flushPendingDeepLinks(),x(`pending deep links flushed`,E),x(`startup complete`,n)}exports.runMainAppStartup=um; -//# sourceMappingURL=main-Dl6lTb0_.js.map \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 42805ab..0000000 --- a/package.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "name": "openai-codex-electron", - "productName": "Codex", - "author": "OpenAI", - "version": "26.309.31024", - "description": "Codex", - "main": ".vite/build/bootstrap.js", - "scripts": { - "compile": "pnpm exec tsgo -b", - "tsc": "pnpm exec tsgo --noEmit", - "dev": "pnpm run rebuild:sqlite && pnpm --dir .. run generate:third-party-notices && cross-env NODE_ENV=development electron-forge start", - "build": "rm -rf out && PNPM_YES=true pnpm run forge:make -- --platform=darwin --arch=arm64", - "forge:package": "pnpm run rebuild:sqlite && pnpm run rebuild:forge-natives && electron-forge package", - "forge:make": "pnpm run rebuild:sqlite && pnpm run rebuild:forge-natives && electron-forge make", - "forge:publish": "pnpm run rebuild:sqlite && pnpm run rebuild:forge-natives && electron-forge publish", - "format": "oxfmt --check", - "format:fix": "oxfmt --write", - "lint": "pnpm exec oxlint --tsconfig ./tsconfig.json --max-warnings 0 --type-aware --type-check", - "lint:fix": "pnpm exec oxlint --tsconfig ./tsconfig.json --max-warnings 0 --type-aware --type-check --fix", - "metadata-path": "pnpm exec tsx ./scripts/dev-metadata.ts path", - "metadata-probe": "pnpm exec tsx ./scripts/dev-metadata.ts probe", - "test": "pnpm run rebuild:sqlite && node ./scripts/ensure-electron-binary.mjs && vitest run", - "test:quiet": "pnpm run rebuild:sqlite && node ./scripts/ensure-electron-binary.mjs && vitest run --silent --reporter=dot", - "playwright:agent:repl": "pnpm run rebuild:sqlite && node --import tsx ./scripts/playwright-electron-agent-cdp.mjs", - "devtools:reset": "sh -c 'rm -rf \"$HOME/Library/Application Support/Codex/extensions/fmkadmapgofadopljbjfkapdkoienihi\" \"$HOME/Library/Application Support/Codex/Service Worker\" \"$HOME/Library/Application Support/Codex/Code Cache\"'", - "build:sparkle-native": "node ./scripts/build-sparkle-native.mjs", - "rebuild:forge-natives": "pnpm rebuild macos-alias", - "rebuild:sqlite": "node ./scripts/rebuild-sqlite.mjs" - }, - "devDependencies": { - "@electron-forge/cli": "^7.11.1", - "@electron-forge/maker-deb": "^7.11.1", - "@electron-forge/maker-dmg": "^7.11.1", - "@electron-forge/maker-msix": "7.11.1", - "@electron-forge/maker-rpm": "^7.11.1", - "@electron-forge/maker-squirrel": "^7.11.1", - "@electron-forge/maker-zip": "^7.11.1", - "@electron-forge/plugin-auto-unpack-natives": "^7.11.1", - "@electron-forge/plugin-fuses": "^7.11.1", - "@electron-forge/plugin-vite": "^7.11.1", - "@electron-forge/shared-types": "^7.11.1", - "@electron/fuses": "^1.8.0", - "@electron/notarize": "^3.1.1", - "@sentry/cli": "^3.1.0", - "@types/electron-squirrel-startup": "^1.0.2", - "@types/lodash": "^4.17.20", - "@types/memoizee": "^0.4.12", - "@types/mime-types": "^3.0.1", - "@types/which": "^3.0.4", - "@types/ws": "^8.18.0", - "cross-env": "^7.0.3", - "debug": "^4.4.1", - "electron": "40.0.0", - "electron-installer-dmg": "^5.0.1", - "fb-dotslash": "^0.5.8", - "node-addon-api": "^8.5.0", - "node-gyp": "^10.2.0", - "playwright": "^1.58.2", - "typescript": "^5.9.3", - "vite": "8.0.0-beta.15", - "vitest": "4.1.0-beta.5" - }, - "dependencies": { - "@sentry/electron": "^7.5.0", - "@sentry/node": "10.29.0", - "@tanstack/react-form": "^1.27.7", - "app-server-types": "workspace:*", - "better-sqlite3": "^12.4.6", - "bufferutil": "^4.0.1", - "electron-context-menu": "^4.1.1", - "electron-squirrel-startup": "^1.0.1", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "memoizee": "^0.4.15", - "mime-types": "^2.1.35", - "node-pty": "^1.1.0", - "protocol": "workspace:*", - "shared-node": "workspace:*", - "shlex": "^3.0.0", - "smol-toml": "^1.5.2", - "socks-proxy-agent": "^8.0.5", - "tslib": "^2.8.1", - "utf-8-validate": "^6.0.0", - "which": "^4.0.0", - "ws": "^8.18.3", - "zod": "^4.1.13" - }, - "codexBuildFlavor": "prod", - "codexBuildNumber": "962", - "codexSparkleFeedUrl": "https://persistent.oaistatic.com/codex-app-prod/appcast.xml" -} diff --git a/preload.js b/preload.js deleted file mode 100644 index 2c43be7..0000000 --- a/preload.js +++ /dev/null @@ -1,2 +0,0 @@ -let e=require(`electron`);function t(e){return`codex_desktop:worker:${e}:from-view`}function n(e){return`codex_desktop:worker:${e}:for-view`}var r=`electron`,i=`codex_desktop:message-from-view`,a=`codex_desktop:message-for-view`,o=e.ipcRenderer.sendSync(`codex_desktop:get-sentry-init-options`),s=e.ipcRenderer.sendSync(`codex_desktop:get-build-flavor`),c=new Map,l=new Map,u={windowType:r,sendMessageFromView:async t=>{await e.ipcRenderer.invoke(i,t)},getPathForFile:t=>e.webUtils.getPathForFile(t)||null,sendWorkerMessageFromView:async(n,r)=>{await e.ipcRenderer.invoke(t(n),r)},subscribeToWorkerMessages:(t,r)=>{let i=c.get(t);i||(i=new Set,c.set(t,i));let a=l.get(t);return a||(a=(e,n)=>{let r=c.get(t);r&&r.forEach(e=>{e(n)})},l.set(t,a),e.ipcRenderer.on(n(t),a)),i.add(r),()=>{let i=c.get(t);if(!i||(i.delete(r),i.size>0))return;c.delete(t);let a=l.get(t);a&&e.ipcRenderer.removeListener(n(t),a),l.delete(t)}},showContextMenu:async t=>e.ipcRenderer.invoke(`codex_desktop:show-context-menu`,t),showApplicationMenu:async(t,n,r)=>{await e.ipcRenderer.invoke(`codex_desktop:show-application-menu`,{menuId:t,x:n,y:r})},getFastModeRolloutMetrics:async t=>e.ipcRenderer.invoke(`codex_desktop:get-fast-mode-rollout-metrics`,t),triggerSentryTestError:async()=>{await e.ipcRenderer.invoke(`codex_desktop:trigger-sentry-test`)},getSentryInitOptions:()=>o,getAppSessionId:()=>o.codexAppSessionId,getBuildFlavor:()=>s};e.ipcRenderer.on(a,(e,t)=>{window.dispatchEvent(new MessageEvent(`message`,{data:t}))}),e.contextBridge.exposeInMainWorld(`codexWindowType`,r),e.contextBridge.exposeInMainWorld(`electronBridge`,u); -//# sourceMappingURL=preload.js.map \ No newline at end of file diff --git a/shared/README.md b/shared/README.md new file mode 100644 index 0000000..788c800 --- /dev/null +++ b/shared/README.md @@ -0,0 +1,16 @@ +# Shared Workspace + +This directory is the Kotlin Multiplatform workspace for Pindrop's shared transcription logic. + +Layout: +- `build.gradle.kts`, `settings.gradle.kts`, `gradle.properties`, `gradlew`: Gradle workspace root +- `core/`: shared domain types and cross-platform ports +- `feature-transcription/`: shared transcription policy and orchestration logic + +Common commands from the repo root: +- `just shared-test` +- `just shared-xcframework` + +Direct commands from this directory: +- `./gradlew :core:jvmTest :feature-transcription:jvmTest` +- `./gradlew :core:assemblePindropSharedCoreXCFramework :feature-transcription:assemblePindropSharedTranscriptionXCFramework` diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..cfe77c6 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + kotlin("multiplatform") version "2.1.20" apply false +} diff --git a/shared/core/build.gradle.kts b/shared/core/build.gradle.kts new file mode 100644 index 0000000..b2253ba --- /dev/null +++ b/shared/core/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + val macosArm64Target = macosArm64() + val macosX64Target = macosX64() + val iosArm64Target = iosArm64() + val iosSimulatorArm64Target = iosSimulatorArm64() + val iosX64Target = iosX64() + + val xcframework = XCFramework("PindropSharedCore") + + listOf( + macosArm64Target, + macosX64Target, + iosArm64Target, + iosSimulatorArm64Target, + iosX64Target, + ).forEach { target -> + target.binaries.framework { + baseName = "PindropSharedCore" + xcframework.add(this) + } + } + + sourceSets { + commonMain.dependencies { + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/shared/core/src/commonMain/kotlin/tech/watzon/pindrop/shared/core/TranscriptionContracts.kt b/shared/core/src/commonMain/kotlin/tech/watzon/pindrop/shared/core/TranscriptionContracts.kt new file mode 100644 index 0000000..1edd9cf --- /dev/null +++ b/shared/core/src/commonMain/kotlin/tech/watzon/pindrop/shared/core/TranscriptionContracts.kt @@ -0,0 +1,133 @@ +package tech.watzon.pindrop.shared.core + +enum class TranscriptionProviderId { + WHISPER_KIT, + PARAKEET, + OPEN_AI, + ELEVEN_LABS, + GROQ, +} + +data class TranscriptionModelId(val value: String) + +enum class TranscriptionLanguage { + AUTOMATIC, + ENGLISH, + SIMPLIFIED_CHINESE, + SPANISH, + FRENCH, + GERMAN, + TURKISH, + JAPANESE, + PORTUGUESE_BRAZIL, + ITALIAN, + DUTCH, + KOREAN, +} + +data class TranscriptionRequest( + val audioData: ByteArray, + val language: TranscriptionLanguage = TranscriptionLanguage.AUTOMATIC, + val diarizationEnabled: Boolean = false, +) + +data class StreamingTranscriptionConfig( + val modelId: TranscriptionModelId, + val language: TranscriptionLanguage = TranscriptionLanguage.AUTOMATIC, +) + +data class DiarizedSegment( + val speakerId: String, + val speakerLabel: String, + val startTimeSeconds: Double, + val endTimeSeconds: Double, + val confidence: Float, + val text: String, +) + +data class TranscriptionResult( + val text: String, + val diarizedSegments: List = emptyList(), +) + +data class EngineCapabilities( + val supportsStreaming: Boolean, + val supportsSpeakerDiarization: Boolean, + val supportsWordTimestamps: Boolean, + val supportsLanguageDetection: Boolean, +) + +enum class ModelAvailability { + AVAILABLE, + COMING_SOON, + REQUIRES_SETUP, +} + +enum class ModelLanguageSupport { + ENGLISH_ONLY, + FULL_MULTILINGUAL, + PARAKEET_V3_EUROPEAN, +} + +data class ModelDescriptor( + val id: TranscriptionModelId, + val displayName: String, + val provider: TranscriptionProviderId, + val languageSupport: ModelLanguageSupport, + val sizeInMb: Int, + val description: String, + val speedRating: Double, + val accuracyRating: Double, + val availability: ModelAvailability, +) + +data class TranscriptionSettingsSnapshot( + val selectedLanguage: TranscriptionLanguage, + val selectedModelId: TranscriptionModelId, + val aiEnhancementEnabled: Boolean, + val streamingFeatureEnabled: Boolean, + val diarizationFeatureEnabled: Boolean, +) + +interface TranscriptionEnginePort { + suspend fun loadModel(modelId: TranscriptionModelId, downloadBasePath: String? = null) + suspend fun loadModelFromPath(path: String) + suspend fun transcribe(request: TranscriptionRequest): TranscriptionResult + suspend fun unloadModel() +} + +interface StreamingTranscriptionEnginePort { + suspend fun loadModel(config: StreamingTranscriptionConfig) + suspend fun startStreaming() + suspend fun processAudioChunk(samples: FloatArray) + suspend fun stopStreaming(): String + suspend fun cancelStreaming() +} + +interface SpeakerDiarizerPort { + suspend fun diarize(request: TranscriptionRequest): List +} + +interface ModelCatalogPort { + fun allModels(): List + fun recommendedModels(language: TranscriptionLanguage): List + fun isModelDownloaded(modelId: TranscriptionModelId): Boolean +} + +interface SettingsSnapshotProvider { + fun currentSettings(): TranscriptionSettingsSnapshot +} + +interface TranscriptionEventSink { + fun onStateChanged(state: SharedTranscriptionState) + fun onPartialTranscript(text: String) +} + +enum class SharedTranscriptionState { + UNLOADED, + LOADING, + READY, + TRANSCRIBING, + STREAMING, + ERROR, +} diff --git a/shared/feature-transcription/build.gradle.kts b/shared/feature-transcription/build.gradle.kts new file mode 100644 index 0000000..cf068da --- /dev/null +++ b/shared/feature-transcription/build.gradle.kts @@ -0,0 +1,43 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + val macosArm64Target = macosArm64() + val macosX64Target = macosX64() + val iosArm64Target = iosArm64() + val iosSimulatorArm64Target = iosSimulatorArm64() + val iosX64Target = iosX64() + + val xcframework = XCFramework("PindropSharedTranscription") + + listOf( + macosArm64Target, + macosX64Target, + iosArm64Target, + iosSimulatorArm64Target, + iosX64Target, + ).forEach { target -> + target.binaries.framework { + baseName = "PindropSharedTranscription" + xcframework.add(this) + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":core")) + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachine.kt b/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachine.kt new file mode 100644 index 0000000..a80825b --- /dev/null +++ b/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachine.kt @@ -0,0 +1,206 @@ +package tech.watzon.pindrop.shared.feature.transcription + +enum class MediaTranscriptionStage { + PREFLIGHT, + IMPORTING, + DOWNLOADING, + PREPARING_AUDIO, + TRANSCRIBING, + SAVING, + COMPLETED, + FAILED, +} + +sealed class MediaTranscriptionRoute { + data object Library : MediaTranscriptionRoute() + data class Processing(val jobId: String) : MediaTranscriptionRoute() + data class Detail(val recordId: String) : MediaTranscriptionRoute() +} + +data class MediaTranscriptionJob( + val id: String, + val requestDisplayName: String, + val stage: MediaTranscriptionStage = MediaTranscriptionStage.PREFLIGHT, + val progress: Double? = null, + val detail: String = "", + val errorMessage: String? = null, +) + +data class JobProgressUpdate( + val stage: MediaTranscriptionStage, + val progress: Double? = null, + val detail: String? = null, + val errorMessage: String? = null, +) + +data class MediaTranscriptionFeatureSnapshot( + val route: MediaTranscriptionRoute = MediaTranscriptionRoute.Library, + val selectedRecordId: String? = null, + val selectedFolderId: String? = null, + val currentJob: MediaTranscriptionJob? = null, + val setupIssue: String? = null, + val libraryMessage: String? = null, +) + +object MediaTranscriptionJobStateMachine { + fun showLibrary( + snapshot: MediaTranscriptionFeatureSnapshot, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy(route = MediaTranscriptionRoute.Library) + } + + fun beginJob( + snapshot: MediaTranscriptionFeatureSnapshot, + job: MediaTranscriptionJob, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy( + route = MediaTranscriptionRoute.Processing(job.id), + currentJob = job, + setupIssue = null, + libraryMessage = null, + ) + } + + fun selectRecord( + snapshot: MediaTranscriptionFeatureSnapshot, + recordId: String, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy( + selectedRecordId = recordId, + route = MediaTranscriptionRoute.Detail(recordId), + libraryMessage = null, + ) + } + + fun selectFolder( + snapshot: MediaTranscriptionFeatureSnapshot, + folderId: String, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy( + selectedFolderId = folderId, + libraryMessage = null, + ) + } + + fun clearSelectedFolder( + snapshot: MediaTranscriptionFeatureSnapshot, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy(selectedFolderId = null) + } + + fun updateJob( + snapshot: MediaTranscriptionFeatureSnapshot, + update: JobProgressUpdate, + ): MediaTranscriptionFeatureSnapshot { + val currentJob = snapshot.currentJob ?: return snapshot + return snapshot.copy( + currentJob = currentJob.copy( + stage = update.stage, + progress = update.progress, + detail = update.detail ?: currentJob.detail, + errorMessage = update.errorMessage, + ), + ) + } + + fun clearCurrentJob( + snapshot: MediaTranscriptionFeatureSnapshot, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy(currentJob = null) + } + + fun setSetupIssue( + snapshot: MediaTranscriptionFeatureSnapshot, + message: String, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy( + setupIssue = message, + route = MediaTranscriptionRoute.Library, + ) + } + + fun setLibraryMessage( + snapshot: MediaTranscriptionFeatureSnapshot, + message: String?, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy(libraryMessage = message) + } + + fun handleDeletedRecord( + snapshot: MediaTranscriptionFeatureSnapshot, + recordId: String, + message: String, + ): MediaTranscriptionFeatureSnapshot { + val nextRoute = when (val route = snapshot.route) { + is MediaTranscriptionRoute.Detail -> { + if (route.recordId == recordId) MediaTranscriptionRoute.Library else route + } + else -> snapshot.route + } + + return snapshot.copy( + selectedRecordId = if (snapshot.selectedRecordId == recordId) null else snapshot.selectedRecordId, + route = nextRoute, + libraryMessage = message, + ) + } + + fun handleDeletedFolder( + snapshot: MediaTranscriptionFeatureSnapshot, + folderId: String, + message: String, + ): MediaTranscriptionFeatureSnapshot { + return snapshot.copy( + selectedFolderId = if (snapshot.selectedFolderId == folderId) null else snapshot.selectedFolderId, + libraryMessage = message, + ) + } + + fun completeCurrentJob( + snapshot: MediaTranscriptionFeatureSnapshot, + recordId: String, + shouldNavigateToDetail: Boolean, + ): MediaTranscriptionFeatureSnapshot { + val updated = updateJob( + snapshot = snapshot, + update = JobProgressUpdate( + stage = MediaTranscriptionStage.COMPLETED, + progress = 1.0, + detail = "Saved transcription", + ), + ) + + return updated.copy( + selectedRecordId = recordId, + route = if (shouldNavigateToDetail) { + MediaTranscriptionRoute.Detail(recordId) + } else { + MediaTranscriptionRoute.Library + }, + libraryMessage = if (shouldNavigateToDetail) null else "Transcription finished.", + ) + } + + fun failCurrentJob( + snapshot: MediaTranscriptionFeatureSnapshot, + message: String, + returnToLibrary: Boolean, + ): MediaTranscriptionFeatureSnapshot { + val updated = updateJob( + snapshot = snapshot, + update = JobProgressUpdate( + stage = MediaTranscriptionStage.FAILED, + errorMessage = message, + ), + ) + + return if (returnToLibrary) { + updated.copy( + route = MediaTranscriptionRoute.Library, + libraryMessage = message, + ) + } else { + updated + } + } +} diff --git a/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt b/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt new file mode 100644 index 0000000..1644f57 --- /dev/null +++ b/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt @@ -0,0 +1,389 @@ +package tech.watzon.pindrop.shared.feature.transcription + +import tech.watzon.pindrop.shared.core.ModelDescriptor +import tech.watzon.pindrop.shared.core.ModelLanguageSupport +import tech.watzon.pindrop.shared.core.SharedTranscriptionState +import tech.watzon.pindrop.shared.core.TranscriptionLanguage +import tech.watzon.pindrop.shared.core.TranscriptionModelId +import tech.watzon.pindrop.shared.core.TranscriptionProviderId + +data class TranscriptionRuntimePolicy( + val selectedProvider: TranscriptionProviderId, + val selectedModelId: TranscriptionModelId, + val streamingFeatureEnabled: Boolean, + val diarizationFeatureEnabled: Boolean, + val outputMode: String, + val aiEnhancementEnabled: Boolean, + val isQuickCaptureMode: Boolean, +) + +data class SharedOrchestrationPlan( + val useStreaming: Boolean, + val useSpeakerDiarization: Boolean, +) + +data class SharedModelLoadPlan( + val resolvedProvider: TranscriptionProviderId, + val shouldUnloadCurrentModel: Boolean, + val supportsLocalModelLoading: Boolean, + val prefersPathBasedLoading: Boolean, +) + +data class SharedTranscriptionExecutionPlan( + val selectedProvider: TranscriptionProviderId, + val selectedModelId: TranscriptionModelId, + val useSpeakerDiarization: Boolean, + val shouldNormalizeOutput: Boolean, +) + +enum class StartupModelAction { + LOAD_SELECTED, + LOAD_FALLBACK, + DOWNLOAD_SELECTED, +} + +data class StartupModelResolution( + val action: StartupModelAction, + val resolvedModel: ModelDescriptor, + val updatedSelectedModelId: TranscriptionModelId, +) + +enum class EventTapRecoveryAction { + REENABLE, + RECREATE, +} + +data class EventTapRecoveryDecision( + val consecutiveDisableCount: Int, + val action: EventTapRecoveryAction, +) + +enum class SharedSessionErrorCode { + ENGINE_SWITCH_DURING_TRANSCRIPTION, + MODEL_NOT_LOADED, + INVALID_AUDIO_DATA, + TRANSCRIPTION_ALREADY_IN_PROGRESS, + STREAMING_NOT_READY, +} + +data class SharedStateTransition( + val nextState: SharedTranscriptionState, + val errorCode: SharedSessionErrorCode? = null, +) + +object SharedTranscriptionOrchestrator { + fun beginModelLoad(currentState: SharedTranscriptionState): SharedStateTransition { + return if (currentState == SharedTranscriptionState.TRANSCRIBING || currentState == SharedTranscriptionState.STREAMING) { + SharedStateTransition( + nextState = currentState, + errorCode = SharedSessionErrorCode.ENGINE_SWITCH_DURING_TRANSCRIPTION, + ) + } else { + SharedStateTransition(nextState = SharedTranscriptionState.LOADING) + } + } + + fun completeModelLoad(success: Boolean): SharedTranscriptionState { + return if (success) { + SharedTranscriptionState.READY + } else { + SharedTranscriptionState.ERROR + } + } + + fun beginBatchTranscription( + currentState: SharedTranscriptionState, + hasLoadedModel: Boolean, + audioByteCount: Int, + ): SharedStateTransition { + if (!hasLoadedModel) { + return SharedStateTransition( + nextState = currentState, + errorCode = SharedSessionErrorCode.MODEL_NOT_LOADED, + ) + } + + if (audioByteCount <= 0) { + return SharedStateTransition( + nextState = currentState, + errorCode = SharedSessionErrorCode.INVALID_AUDIO_DATA, + ) + } + + if (currentState == SharedTranscriptionState.TRANSCRIBING || currentState == SharedTranscriptionState.STREAMING) { + return SharedStateTransition( + nextState = currentState, + errorCode = SharedSessionErrorCode.TRANSCRIPTION_ALREADY_IN_PROGRESS, + ) + } + + return SharedStateTransition(nextState = SharedTranscriptionState.TRANSCRIBING) + } + + fun completeBatchTranscription(): SharedTranscriptionState { + return SharedTranscriptionState.READY + } + + fun stateAfterUnload(): SharedTranscriptionState { + return SharedTranscriptionState.UNLOADED + } + + fun stateAfterStreamingPrepared(currentState: SharedTranscriptionState): SharedTranscriptionState { + return when (currentState) { + SharedTranscriptionState.UNLOADED, + SharedTranscriptionState.ERROR, + -> SharedTranscriptionState.READY + else -> currentState + } + } + + fun failStreamingPreparation(): SharedTranscriptionState { + return SharedTranscriptionState.ERROR + } + + fun beginStreaming( + currentState: SharedTranscriptionState, + hasPreparedStreamingEngine: Boolean, + ): SharedStateTransition { + if (currentState == SharedTranscriptionState.TRANSCRIBING || currentState == SharedTranscriptionState.STREAMING) { + return SharedStateTransition( + nextState = currentState, + errorCode = SharedSessionErrorCode.TRANSCRIPTION_ALREADY_IN_PROGRESS, + ) + } + + if (!hasPreparedStreamingEngine) { + return SharedStateTransition( + nextState = currentState, + errorCode = SharedSessionErrorCode.STREAMING_NOT_READY, + ) + } + + return SharedStateTransition(nextState = SharedTranscriptionState.STREAMING) + } + + fun validateStreamingAudio(isStreamingSessionActive: Boolean): SharedSessionErrorCode? { + return if (isStreamingSessionActive) { + null + } else { + SharedSessionErrorCode.STREAMING_NOT_READY + } + } + + fun completeStreamingSession( + hasLoadedModel: Boolean, + hasPreparedStreamingEngine: Boolean, + ): SharedTranscriptionState { + return if (hasLoadedModel || hasPreparedStreamingEngine) { + SharedTranscriptionState.READY + } else { + SharedTranscriptionState.UNLOADED + } + } + + fun planSession(policy: TranscriptionRuntimePolicy): SharedOrchestrationPlan { + val useStreaming = shouldUseStreamingTranscription( + streamingFeatureEnabled = policy.streamingFeatureEnabled, + outputMode = policy.outputMode, + aiEnhancementEnabled = policy.aiEnhancementEnabled, + isQuickCaptureMode = policy.isQuickCaptureMode, + ) + + return SharedOrchestrationPlan( + useStreaming = useStreaming, + useSpeakerDiarization = shouldUseSpeakerDiarization( + diarizationFeatureEnabled = policy.diarizationFeatureEnabled, + isStreamingSessionActive = useStreaming, + ), + ) + } + + fun shouldUseStreamingTranscription( + streamingFeatureEnabled: Boolean, + outputMode: String, + aiEnhancementEnabled: Boolean, + isQuickCaptureMode: Boolean, + ): Boolean { + return streamingFeatureEnabled && + outputMode == "directInsert" && + !aiEnhancementEnabled && + !isQuickCaptureMode + } + + fun shouldUseSpeakerDiarization( + diarizationFeatureEnabled: Boolean, + isStreamingSessionActive: Boolean, + ): Boolean { + return diarizationFeatureEnabled && !isStreamingSessionActive + } + + fun normalizeTranscriptionText(text: String): String { + return text.trim() + } + + fun isTranscriptionEffectivelyEmpty(text: String): Boolean { + val normalized = normalizeTranscriptionText(text) + return normalized.isEmpty() || normalized.equals("[BLANK AUDIO]", ignoreCase = true) + } + + fun shouldPersistHistory(outputSucceeded: Boolean, text: String): Boolean { + return outputSucceeded && !isTranscriptionEffectivelyEmpty(text) + } + + fun supportsLanguage( + support: ModelLanguageSupport, + language: TranscriptionLanguage, + ): Boolean { + if (language == TranscriptionLanguage.AUTOMATIC) { + return true + } + + return when (support) { + ModelLanguageSupport.ENGLISH_ONLY -> language == TranscriptionLanguage.ENGLISH + ModelLanguageSupport.FULL_MULTILINGUAL -> true + ModelLanguageSupport.PARAKEET_V3_EUROPEAN -> language in setOf( + TranscriptionLanguage.ENGLISH, + TranscriptionLanguage.SPANISH, + TranscriptionLanguage.FRENCH, + TranscriptionLanguage.GERMAN, + TranscriptionLanguage.PORTUGUESE_BRAZIL, + TranscriptionLanguage.ITALIAN, + TranscriptionLanguage.DUTCH, + TranscriptionLanguage.TURKISH, + ) + } + } + + fun providerSupportsLocalLoading(provider: TranscriptionProviderId): Boolean { + return provider == TranscriptionProviderId.WHISPER_KIT || provider == TranscriptionProviderId.PARAKEET + } + + fun planModelLoad( + requestedProvider: TranscriptionProviderId, + currentProvider: TranscriptionProviderId?, + loadsFromPath: Boolean, + ): SharedModelLoadPlan { + val resolvedProvider = if (loadsFromPath) { + TranscriptionProviderId.WHISPER_KIT + } else { + requestedProvider + } + + return SharedModelLoadPlan( + resolvedProvider = resolvedProvider, + shouldUnloadCurrentModel = currentProvider != null && currentProvider != resolvedProvider, + supportsLocalModelLoading = providerSupportsLocalLoading(resolvedProvider), + prefersPathBasedLoading = loadsFromPath && resolvedProvider == TranscriptionProviderId.WHISPER_KIT, + ) + } + + fun planTranscriptionExecution( + selectedProvider: TranscriptionProviderId, + selectedModelId: TranscriptionModelId, + diarizationRequested: Boolean, + isStreamingSessionActive: Boolean, + ): SharedTranscriptionExecutionPlan { + return SharedTranscriptionExecutionPlan( + selectedProvider = selectedProvider, + selectedModelId = selectedModelId, + useSpeakerDiarization = diarizationRequested && !isStreamingSessionActive, + shouldNormalizeOutput = true, + ) + } + + fun recommendedModels( + allModels: List, + curatedIds: List, + language: TranscriptionLanguage, + ): List { + val ranks = curatedIds.withIndex().associate { it.value to it.index } + return allModels + .filter { it.id in curatedIds } + .filter { supportsLanguage(it.languageSupport, language) } + .sortedBy { ranks[it.id] ?: Int.MAX_VALUE } + } + + fun resolveStartupModel( + selectedModelId: TranscriptionModelId, + defaultModelId: TranscriptionModelId, + availableModels: List, + downloadedModelIds: List, + ): StartupModelResolution { + val normalizedSelectedModel = availableModels.firstOrNull { it.id == selectedModelId } + ?: availableModels.firstOrNull { it.id == defaultModelId } + ?: availableModels.firstOrNull() + ?: error("availableModels must not be empty") + + val downloadedSet = downloadedModelIds.toSet() + if (normalizedSelectedModel.id in downloadedSet) { + return StartupModelResolution( + action = StartupModelAction.LOAD_SELECTED, + resolvedModel = normalizedSelectedModel, + updatedSelectedModelId = normalizedSelectedModel.id, + ) + } + + val fallbackModel = availableModels.firstOrNull { it.id in downloadedSet } + if (fallbackModel != null) { + return StartupModelResolution( + action = StartupModelAction.LOAD_FALLBACK, + resolvedModel = fallbackModel, + updatedSelectedModelId = fallbackModel.id, + ) + } + + return StartupModelResolution( + action = StartupModelAction.DOWNLOAD_SELECTED, + resolvedModel = normalizedSelectedModel, + updatedSelectedModelId = normalizedSelectedModel.id, + ) + } + + fun determineEventTapRecovery( + elapsedSinceLastDisableSeconds: Double?, + consecutiveDisableCount: Int, + disableLoopWindowSeconds: Double, + maxReenableAttemptsBeforeRecreate: Int, + ): EventTapRecoveryDecision { + val nextCount = if ( + elapsedSinceLastDisableSeconds != null && + elapsedSinceLastDisableSeconds <= disableLoopWindowSeconds + ) { + consecutiveDisableCount + 1 + } else { + 1 + } + + val recreateThreshold = maxOf(1, maxReenableAttemptsBeforeRecreate) + val action = if (nextCount >= recreateThreshold) { + EventTapRecoveryAction.RECREATE + } else { + EventTapRecoveryAction.REENABLE + } + + return EventTapRecoveryDecision( + consecutiveDisableCount = nextCount, + action = action, + ) + } + + fun shouldRunLiveContextSession( + aiEnhancementEnabled: Boolean, + uiContextEnabled: Boolean, + liveSessionEnabled: Boolean, + ): Boolean { + return aiEnhancementEnabled && uiContextEnabled && liveSessionEnabled + } + + fun shouldAppendTransition( + signature: String, + trigger: String, + lastSignature: String?, + ): Boolean { + if (trigger == "recordingStart") { + return true + } + + return lastSignature == null || lastSignature != signature + } +} diff --git a/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt new file mode 100644 index 0000000..4a0d932 --- /dev/null +++ b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt @@ -0,0 +1,113 @@ +package tech.watzon.pindrop.shared.feature.transcription + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull + +class MediaTranscriptionJobStateMachineTest { + @Test + fun beginJobMovesFeatureToProcessingRoute() { + val job = MediaTranscriptionJob(id = "job-1", requestDisplayName = "demo.mp3") + val snapshot = MediaTranscriptionJobStateMachine.beginJob( + snapshot = MediaTranscriptionFeatureSnapshot(), + job = job, + ) + + assertEquals(job, snapshot.currentJob) + assertIs(snapshot.route) + } + + @Test + fun updateJobMutatesStageAndProgress() { + val initial = MediaTranscriptionJobStateMachine.beginJob( + snapshot = MediaTranscriptionFeatureSnapshot(), + job = MediaTranscriptionJob(id = "job-1", requestDisplayName = "demo.mp3"), + ) + + val updated = MediaTranscriptionJobStateMachine.updateJob( + snapshot = initial, + update = JobProgressUpdate( + stage = MediaTranscriptionStage.TRANSCRIBING, + progress = 0.5, + detail = "Halfway there", + ), + ) + + assertEquals(MediaTranscriptionStage.TRANSCRIBING, updated.currentJob?.stage) + assertEquals(0.5, updated.currentJob?.progress) + assertEquals("Halfway there", updated.currentJob?.detail) + } + + @Test + fun completeCurrentJobHandlesDetailAndLibraryRoutes() { + val initial = MediaTranscriptionJobStateMachine.beginJob( + snapshot = MediaTranscriptionFeatureSnapshot(), + job = MediaTranscriptionJob(id = "job-1", requestDisplayName = "demo.mp3"), + ) + + val detailSnapshot = MediaTranscriptionJobStateMachine.completeCurrentJob( + snapshot = initial, + recordId = "record-1", + shouldNavigateToDetail = true, + ) + assertIs(detailSnapshot.route) + + val librarySnapshot = MediaTranscriptionJobStateMachine.completeCurrentJob( + snapshot = initial, + recordId = "record-1", + shouldNavigateToDetail = false, + ) + assertIs(librarySnapshot.route) + assertEquals("Transcription finished.", librarySnapshot.libraryMessage) + } + + @Test + fun failCurrentJobCanReturnToLibrary() { + val initial = MediaTranscriptionJobStateMachine.beginJob( + snapshot = MediaTranscriptionFeatureSnapshot(), + job = MediaTranscriptionJob(id = "job-1", requestDisplayName = "demo.mp3"), + ) + + val failed = MediaTranscriptionJobStateMachine.failCurrentJob( + snapshot = initial, + message = "No audio stream", + returnToLibrary = true, + ) + + assertIs(failed.route) + assertEquals("No audio stream", failed.libraryMessage) + assertEquals(MediaTranscriptionStage.FAILED, failed.currentJob?.stage) + } + + @Test + fun selectionAndDeletionStateIsManagedBySharedSnapshot() { + val folderSnapshot = MediaTranscriptionJobStateMachine.selectFolder( + snapshot = MediaTranscriptionFeatureSnapshot(), + folderId = "folder-1", + ) + assertEquals("folder-1", folderSnapshot.selectedFolderId) + + val recordSnapshot = MediaTranscriptionJobStateMachine.selectRecord( + snapshot = folderSnapshot, + recordId = "record-1", + ) + assertIs(recordSnapshot.route) + assertEquals("record-1", recordSnapshot.selectedRecordId) + + val afterRecordDelete = MediaTranscriptionJobStateMachine.handleDeletedRecord( + snapshot = recordSnapshot, + recordId = "record-1", + message = "Deleted", + ) + assertIs(afterRecordDelete.route) + assertNull(afterRecordDelete.selectedRecordId) + + val afterFolderDelete = MediaTranscriptionJobStateMachine.handleDeletedFolder( + snapshot = afterRecordDelete, + folderId = "folder-1", + message = "Folder deleted", + ) + assertNull(afterFolderDelete.selectedFolderId) + } +} diff --git a/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt new file mode 100644 index 0000000..e0dc736 --- /dev/null +++ b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt @@ -0,0 +1,354 @@ +package tech.watzon.pindrop.shared.feature.transcription + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import tech.watzon.pindrop.shared.core.SharedTranscriptionState +import tech.watzon.pindrop.shared.core.ModelAvailability +import tech.watzon.pindrop.shared.core.ModelDescriptor +import tech.watzon.pindrop.shared.core.ModelLanguageSupport +import tech.watzon.pindrop.shared.core.TranscriptionLanguage +import tech.watzon.pindrop.shared.core.TranscriptionModelId +import tech.watzon.pindrop.shared.core.TranscriptionProviderId + +class SharedTranscriptionOrchestratorTest { + @Test + fun stateMachinePreventsLoadWhileTranscribing() { + val transition = SharedTranscriptionOrchestrator.beginModelLoad( + currentState = SharedTranscriptionState.TRANSCRIBING, + ) + + assertEquals(SharedTranscriptionState.TRANSCRIBING, transition.nextState) + assertEquals(SharedSessionErrorCode.ENGINE_SWITCH_DURING_TRANSCRIPTION, transition.errorCode) + } + + @Test + fun stateMachineValidatesBatchTranscriptionPreconditions() { + val unloaded = SharedTranscriptionOrchestrator.beginBatchTranscription( + currentState = SharedTranscriptionState.UNLOADED, + hasLoadedModel = false, + audioByteCount = 128, + ) + assertEquals(SharedSessionErrorCode.MODEL_NOT_LOADED, unloaded.errorCode) + + val emptyAudio = SharedTranscriptionOrchestrator.beginBatchTranscription( + currentState = SharedTranscriptionState.READY, + hasLoadedModel = true, + audioByteCount = 0, + ) + assertEquals(SharedSessionErrorCode.INVALID_AUDIO_DATA, emptyAudio.errorCode) + + val started = SharedTranscriptionOrchestrator.beginBatchTranscription( + currentState = SharedTranscriptionState.READY, + hasLoadedModel = true, + audioByteCount = 128, + ) + assertEquals(SharedTranscriptionState.TRANSCRIBING, started.nextState) + assertNull(started.errorCode) + } + + @Test + fun stateMachineManagesStreamingLifecycle() { + val begin = SharedTranscriptionOrchestrator.beginStreaming( + currentState = SharedTranscriptionState.READY, + hasPreparedStreamingEngine = true, + ) + assertEquals(SharedTranscriptionState.STREAMING, begin.nextState) + assertNull(begin.errorCode) + + assertEquals( + SharedSessionErrorCode.STREAMING_NOT_READY, + SharedTranscriptionOrchestrator.validateStreamingAudio(isStreamingSessionActive = false), + ) + assertNull(SharedTranscriptionOrchestrator.validateStreamingAudio(isStreamingSessionActive = true)) + + assertEquals( + SharedTranscriptionState.READY, + SharedTranscriptionOrchestrator.completeStreamingSession( + hasLoadedModel = false, + hasPreparedStreamingEngine = true, + ), + ) + } + + @Test + fun streamingTruthTableMatchesMacOsPolicy() { + assertTrue( + SharedTranscriptionOrchestrator.shouldUseStreamingTranscription( + streamingFeatureEnabled = true, + outputMode = "directInsert", + aiEnhancementEnabled = false, + isQuickCaptureMode = false, + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.shouldUseStreamingTranscription( + streamingFeatureEnabled = false, + outputMode = "directInsert", + aiEnhancementEnabled = false, + isQuickCaptureMode = false, + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.shouldUseStreamingTranscription( + streamingFeatureEnabled = true, + outputMode = "clipboard", + aiEnhancementEnabled = false, + isQuickCaptureMode = false, + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.shouldUseStreamingTranscription( + streamingFeatureEnabled = true, + outputMode = "directInsert", + aiEnhancementEnabled = true, + isQuickCaptureMode = false, + ), + ) + } + + @Test + fun diarizationTruthTableMatchesMacOsPolicy() { + assertTrue( + SharedTranscriptionOrchestrator.shouldUseSpeakerDiarization( + diarizationFeatureEnabled = true, + isStreamingSessionActive = false, + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.shouldUseSpeakerDiarization( + diarizationFeatureEnabled = true, + isStreamingSessionActive = true, + ), + ) + } + + @Test + fun normalizationAndHistoryPersistenceMatchMacOsPolicy() { + assertEquals("hello world", SharedTranscriptionOrchestrator.normalizeTranscriptionText(" hello world \n")) + assertTrue(SharedTranscriptionOrchestrator.isTranscriptionEffectivelyEmpty("[blank audio]")) + assertTrue(SharedTranscriptionOrchestrator.shouldPersistHistory(outputSucceeded = true, text = "hello")) + assertFalse(SharedTranscriptionOrchestrator.shouldPersistHistory(outputSucceeded = false, text = "hello")) + } + + @Test + fun languageSupportMatchesParakeetAndEnglishOnlyRules() { + assertTrue( + SharedTranscriptionOrchestrator.supportsLanguage( + ModelLanguageSupport.ENGLISH_ONLY, + TranscriptionLanguage.ENGLISH, + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.supportsLanguage( + ModelLanguageSupport.ENGLISH_ONLY, + TranscriptionLanguage.SPANISH, + ), + ) + assertTrue( + SharedTranscriptionOrchestrator.supportsLanguage( + ModelLanguageSupport.PARAKEET_V3_EUROPEAN, + TranscriptionLanguage.SPANISH, + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.supportsLanguage( + ModelLanguageSupport.PARAKEET_V3_EUROPEAN, + TranscriptionLanguage.SIMPLIFIED_CHINESE, + ), + ) + } + + @Test + fun recommendedModelsFollowCuratedOrder() { + val base = ModelDescriptor( + id = TranscriptionModelId("base"), + displayName = "Base", + provider = TranscriptionProviderId.WHISPER_KIT, + languageSupport = ModelLanguageSupport.FULL_MULTILINGUAL, + sizeInMb = 145, + description = "", + speedRating = 9.0, + accuracyRating = 7.0, + availability = ModelAvailability.AVAILABLE, + ) + val medium = base.copy(id = TranscriptionModelId("medium"), displayName = "Medium") + val tiny = base.copy(id = TranscriptionModelId("tiny"), displayName = "Tiny") + + val result = SharedTranscriptionOrchestrator.recommendedModels( + allModels = listOf(medium, tiny, base), + curatedIds = listOf(tiny.id, base.id, medium.id), + language = TranscriptionLanguage.ENGLISH, + ) + + assertEquals(listOf("tiny", "base", "medium"), result.map { it.id.value }) + } + + @Test + fun modelLoadPlanningKeepsWhisperKitPrimaryForPathLoads() { + val plan = SharedTranscriptionOrchestrator.planModelLoad( + requestedProvider = TranscriptionProviderId.PARAKEET, + currentProvider = TranscriptionProviderId.PARAKEET, + loadsFromPath = true, + ) + + assertEquals(TranscriptionProviderId.WHISPER_KIT, plan.resolvedProvider) + assertTrue(plan.shouldUnloadCurrentModel) + assertTrue(plan.supportsLocalModelLoading) + assertTrue(plan.prefersPathBasedLoading) + } + + @Test + fun transcriptionExecutionPlanningDisablesDiarizationDuringStreaming() { + val plan = SharedTranscriptionOrchestrator.planTranscriptionExecution( + selectedProvider = TranscriptionProviderId.WHISPER_KIT, + selectedModelId = TranscriptionModelId("openai_whisper-base"), + diarizationRequested = true, + isStreamingSessionActive = true, + ) + + assertFalse(plan.useSpeakerDiarization) + assertTrue(plan.shouldNormalizeOutput) + } + + @Test + fun startupModelResolutionFallsBackToDefaultWhenSelectedModelIsUnknown() { + val defaultModel = ModelDescriptor( + id = TranscriptionModelId("openai_whisper-base.en"), + displayName = "Base", + provider = TranscriptionProviderId.WHISPER_KIT, + languageSupport = ModelLanguageSupport.ENGLISH_ONLY, + sizeInMb = 145, + description = "", + speedRating = 9.0, + accuracyRating = 7.0, + availability = ModelAvailability.AVAILABLE, + ) + + val resolution = SharedTranscriptionOrchestrator.resolveStartupModel( + selectedModelId = TranscriptionModelId("missing-model"), + defaultModelId = defaultModel.id, + availableModels = listOf(defaultModel), + downloadedModelIds = listOf(defaultModel.id), + ) + + assertEquals(StartupModelAction.LOAD_SELECTED, resolution.action) + assertEquals(defaultModel.id, resolution.resolvedModel.id) + assertEquals(defaultModel.id, resolution.updatedSelectedModelId) + } + + @Test + fun startupModelResolutionUsesDownloadedFallbackWhenSelectedModelIsMissingLocally() { + val selectedModel = ModelDescriptor( + id = TranscriptionModelId("openai_whisper-base.en"), + displayName = "Base", + provider = TranscriptionProviderId.WHISPER_KIT, + languageSupport = ModelLanguageSupport.ENGLISH_ONLY, + sizeInMb = 145, + description = "", + speedRating = 9.0, + accuracyRating = 7.0, + availability = ModelAvailability.AVAILABLE, + ) + val fallbackModel = selectedModel.copy( + id = TranscriptionModelId("parakeet-tdt-0.6b-v2"), + displayName = "Parakeet", + provider = TranscriptionProviderId.PARAKEET, + ) + + val resolution = SharedTranscriptionOrchestrator.resolveStartupModel( + selectedModelId = selectedModel.id, + defaultModelId = selectedModel.id, + availableModels = listOf(selectedModel, fallbackModel), + downloadedModelIds = listOf(fallbackModel.id), + ) + + assertEquals(StartupModelAction.LOAD_FALLBACK, resolution.action) + assertEquals(fallbackModel.id, resolution.resolvedModel.id) + assertEquals(fallbackModel.id, resolution.updatedSelectedModelId) + } + + @Test + fun startupModelResolutionRequestsDownloadWhenNothingIsDownloaded() { + val selectedModel = ModelDescriptor( + id = TranscriptionModelId("openai_whisper-base.en"), + displayName = "Base", + provider = TranscriptionProviderId.WHISPER_KIT, + languageSupport = ModelLanguageSupport.ENGLISH_ONLY, + sizeInMb = 145, + description = "", + speedRating = 9.0, + accuracyRating = 7.0, + availability = ModelAvailability.AVAILABLE, + ) + + val resolution = SharedTranscriptionOrchestrator.resolveStartupModel( + selectedModelId = selectedModel.id, + defaultModelId = selectedModel.id, + availableModels = listOf(selectedModel), + downloadedModelIds = emptyList(), + ) + + assertEquals(StartupModelAction.DOWNLOAD_SELECTED, resolution.action) + assertEquals(selectedModel.id, resolution.resolvedModel.id) + assertEquals(selectedModel.id, resolution.updatedSelectedModelId) + } + + @Test + fun eventTapRecoveryRecreatesAfterRepeatedDisableBursts() { + val decision = SharedTranscriptionOrchestrator.determineEventTapRecovery( + elapsedSinceLastDisableSeconds = 0.2, + consecutiveDisableCount = 2, + disableLoopWindowSeconds = 1.0, + maxReenableAttemptsBeforeRecreate = 3, + ) + + assertEquals(3, decision.consecutiveDisableCount) + assertEquals(EventTapRecoveryAction.RECREATE, decision.action) + } + + @Test + fun liveContextSessionRequiresAllThreeFlags() { + assertTrue( + SharedTranscriptionOrchestrator.shouldRunLiveContextSession( + aiEnhancementEnabled = true, + uiContextEnabled = true, + liveSessionEnabled = true, + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.shouldRunLiveContextSession( + aiEnhancementEnabled = true, + uiContextEnabled = false, + liveSessionEnabled = true, + ), + ) + } + + @Test + fun transitionAppendSkipsDuplicateNonStartTransitions() { + assertTrue( + SharedTranscriptionOrchestrator.shouldAppendTransition( + signature = "a", + trigger = "recordingStart", + lastSignature = "a", + ), + ) + assertFalse( + SharedTranscriptionOrchestrator.shouldAppendTransition( + signature = "a", + trigger = "periodicRefresh", + lastSignature = "a", + ), + ) + assertTrue( + SharedTranscriptionOrchestrator.shouldAppendTransition( + signature = "b", + trigger = "periodicRefresh", + lastSignature = "a", + ), + ) + } +} diff --git a/shared/gradle.properties b/shared/gradle.properties new file mode 100644 index 0000000..c7e3b25 --- /dev/null +++ b/shared/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +kotlin.mpp.enableCInteropCommonization=true diff --git a/shared/gradle/wrapper/gradle-wrapper.jar b/shared/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 zcma&NW0WmQwk%w>ZQHhO+qUi6W!pA(xoVef+k2O7+pkXd9rt^$@9p#T8Y9=Q^(R-x zjL3*NQ$ZRS1O)&B0s;U4fbe_$e;)(@NB~(;6+v1_IWc+}NnuerWl>cXPyoQcezKvZ z?Yzc@<~LK@Yhh-7jwvSDadFw~t7KfJ%AUfU*p0wc+3m9#p=Zo4`H`aA_wBL6 z9Q`7!;Ok~8YhZ^Vt#N97bt5aZ#mQc8r~hs3;R?H6V4(!oxSADTK|DR2PL6SQ3v6jM<>eLMh9 zAsd(APyxHNFK|G4hA_zi+YV?J+3K_*DIrdla>calRjaE)4(?YnX+AMqEM!Y|ED{^2 zI5gZ%nG-1qAVtl==8o0&F1N+aPj`Oo99RfDNP#ZHw}}UKV)zw6yy%~8Se#sKr;3?g zJGOkV2luy~HgMlEJB+L<_$@9sUXM7@bI)>-K!}JQUCUwuMdq@68q*dV+{L#Vc?r<( z?Wf1HbqxnI6=(Aw!Vv*Z1H_SoPtQTiy^bDVD8L=rRZ`IoIh@}a`!hY>VN&316I#k} z1Sg~_3ApcIFaoZ+d}>rz0Z8DL*zGq%zU1vF1z1D^YDnQrG3^QourmO6;_SrGg3?qWd9R1GMnKV>0++L*NTt>aF2*kcZ;WaudfBhTaqikS(+iNzDggUqvhh?g ziJCF8kA+V@7zi30n=b(3>X0X^lcCCKT(CI)fz-wfOA1P()V)1OciPu4b_B5ORPq&l zchP6l3u9{2on%uTwo>b-v0sIrRwPOzG;Wcq8mstd&?Pgb9rRqF#Yol1d|Q6 z7O20!+zXL(B%tC}@3QOs&T8B=I*k{!Y74nv#{M<0_g4BCf1)-f)6~`;(P-= zPqqH2%j0LDX2k5|_)zavpD{L1BW?<+s$>F&1VNb3T+gu!Dgd{W+na9(yV`M7UaCBuJZg1Y)y6{U}0=LTvxBDApz@r>dGt(m^v|jy&aLA zdsOeJcquuj3G^NkH)g)z@gTzgpr!zpE$0>$aT^{((&VA>+(nQB!M(NnPvEP}ZRz+6 zE!=UW!r7sbX3>{1{XW1?hSDNsur6cNeYxE{$bFwZzZ597{pDqjr%ag85sIns_Xz%= zqY{h#z8J6GA~vfLQ2-jWWcloE5LA62jta=C*1KxAL}jugoPqj4el4R4g3zC4nE#2-NeS{c3#!2tIS|1h8*|kpw2VSH9OcIQZx0Yh!8~P&p}fI$4Bj9Z zr5Yv?i-PfO#<}clM>mO(D0wHniZZdv8pOuJFW z+-u}BH84PQCgT~VWBM88vtCly1y$uEGJ<7vnW%!2yV>l>dxA0X0q{cN6y3u$8R-*f z-4^OlZ1HmxCv`dFW%quP<7xzAbtiFxvY0M1&2ng&A}QXAVR=prc_5m(D+_?hv#$M^ zG#MQ#fHMc!+S%HgU^Qv7Z9eu6eNqpSr3e8(;No*YfovbJ;60LjCzv9O~^>gFKO>t zGZg9`a5;$hksp*fHp{7&RE@DM&Pa@a>Kwk%*F7UGO|}^Z0ho1U$THOgX9jtCW6N$v zLOm}xcMBtw)CC(;LLX!R9jp|UsBWGfs@HaMiosA3#hFee7(4vLY}IrhD++}>pY zo+=_h+uJ;j^CP*OGQ9$0q+%}UB`4`5c766d#)*Czs<91wxw)jI^IdvyjT%<8OqI=i zNn0OUqW#POg^4ma)e2b?*Xv;dri*N0SJ7_{&0>;S!)!YV1TQuiT1C3ZFDvThe}yTCmErx#6yyQ4X@OAbHhdEV!K2%;7J>tiUZF)>Z|eRVDwtDC~=J z*M8|WEgzsyNH@-5lJE+P6HrurgY!PqtWk z^69SOHZ*}xn|j2FDVg`qRT}ob*1XiGo=x8MDEX)duljcVO}oJjuAbB$Z+f&!{z3k< zO6+{@O#2^s4qT`6k}Nw?DKV1DU~}0jVA)(kNz$c-p`*FNG#Gb&o?ko70F||R^y*hD z6HD|hJzF)G&^K=vuN$@b2fIfHVFw@hC_-0hPnB!1{=Nn~ran4VeTMM(Xx2A3h95U} z&J#Kw4>*V(LHOA<3Dy{sbW-9k5M2<%yDw~ce0+aez8 z04skG8@QEESIL;m-@Mf_hY!)KkEUowHu(>)Inz(pM`@pkxz z1_K#Qs6$E^c$7w=JLy>nSY)>aY;x2z`LW-$$rnY0!suTZSG)^0ZMeT#$0_oER zfZ1Hf>#TP|;J^rzn3V^2)Dy!goj6roAho>c=?28yjzQ>N-yU)XduKq8Lb3+ZA|#-{ z?34)Ml8%)3F1}oF;q9XFxoM}Zn{~2>kr%X_=WMen%b>n))hx6kHWNoKUBAz?($h(m(l;U*Gq7;p5J{B;kfO^C%C9HhtW!=O3-h>$U zI2=uaEymeK^h#QuB8a?1Qr0Gn;ZZ@;otg2l>gf= z$_mO!iis+#(8-GZw`ZiCnt}>qKmghHCb)`6U!8qS*DhBANfGj|U2C->7>*Bqe5h<% zF+9uy>$;#cZB>?Wdz3mqi2Y>+6-#!Dd56@$WF{_^P2?6kNNfaw!r74>MZUNkFAt*H zvS@2hNmT%xnXp}_1gixv9!5#YI3ftgFXG20Vt1IQ(~+HmryrZI+r0(y2Scl+y=G^* zxt$Vvn&S=Vul-rgOlYNio7%ST_3!t`_`N@SCv$ppCqok(Q+i_?OL}2@TU$dr6B$c8 zQ$Z(lS6fp%7f}ymQwJAIdpkN~8$)O3|K7Z;{FD?hBSP-#pJgq0C_SFT;^sBc#da0M z;^UuXXq{!hEwQpp(o9+)jPM6ru1P$u0evVO(NJ;%0FgmMNlJ+BJ zf^`a|U*ab?uN*Ue>tHJ$Pl~chCwRnxi3%X06NxwlIAKa*KReLL^y1B^nuy|^SPj3} z5X|?1divh3@zci;648jb2qEOm!_8Tjh3gi;H%2`d`~Q(IL{Wcl1C18+&P>tU&0!nO z&+7mpvr2SsTj=@sX zxG=;T^f7Rg=c=V*u8X(fo)4;RYax^+=quviOJ{>r6{wgf)g){I&qe`=HL}6J>i6Ne zSZ*h9f&JG>Y`@Bg5Pb&>4&UqFp9I<8o`n4W_V=4AugM`RqUeS-!`OyNLyKMqa_Ct| zON-hyk#-}{lZZx>B1F@dF^8S>x|C*QAjKqn&Ej9H#z@Q#KA*ckBX@^;gIP&?aK15l z*EY@kG57oUcm(d{NyXg6$Kj#xR5XdZ1EBCT+Zy!gyXwN&b_zI&$$>7R#{ zh8U@H8NY-cA*CBfH$OCs^priPwtwrzFjDO}DBn#mgbI~hn}cp2U{yv@S)iy|jR9+E zgd(hF|1cyC#te0P;iFGqpNBqc(k<{p^1>wHE_c8Tr4|&NV4mzpzFe;Cr)C~qpVNjl z^u(^s5=kj{QBae)Y*#^A39jT4`!NuIUQzD#DOyfa!R=PrX6oS@x@kJV)Cn$!xTK9A&VI#F-Slt8I4|=$bcjaC5h=9E{51g8X5q1Qfg~~G>qAgy*7h4-WuqE zlIEx?Hu*%99?$6TheLAD4NIMO=Q@*;gaXDl6yLLXfFX0*1-9KQm42c%WX*AXFo$it z?FwnWn2tBHY&Qj6=PV?ergU$VKzu+`(5pCRqX}IoSFo?P!`sff%u1?N+(KsoL+K={ zi*JGl%_jiuB;&YW+n%1o^%5@!HB9}OlIdQZ*XzQ%vu!8p2gnKW+!X>@oC{gp3lNx^ z82|5Jdg9-B<1j|y(@3J;$D-lqdnf0Q6T~q7;#O}EMPV3k(bi$DpZwj9(UhU%_l&nN zR}8tN_NhDMhs)gtG*76~+W2yQ{!kDTE@X4gft2?W;S$BLp9X z;sh2jpm!mkfPX>Vuqxyt76<@f4fyY%&iuDfS1@#PHgzHqG;=X^`X}t2|Alr^lx^ja z1rhvG(PH(a0THitc?4hk=P*#IS;-`fjOKqJ4kgo@dAD@ob*))H)=)6s3cthp&4Q55 z4dQRdG0EveK*(ZUCFcCjILgS#$@%y=8leYxN-%zQaky@H?kjhyBrLYA!cv>kV5;i1 zZ^w&U7s&K8fNr4Pfy9GyTK2Tiay4Y_PsPWoWW5YA8nfUkoyjU)i@nKj@4rY13sxO6 z_NzYdG=Vr<@08Xi#8rnX&^d{Bl`oHXO6Y3!v2U~ZV>I*30X3X&4@zqqVO~RyF)6?a zD(<+33_9TqeHL)#Y?($m4_zZvaJXWXppZ4?wo?$wF)%M6rEVk2gM=l9k+=*Q+((fI zIUBH6)}M?ahSxD4lgmJ30ygk#4d!O@?%WNEONommx`ZK81ZV)mJpKB`PgQ}F>NGdV zkV|>^}oWQd6@Ay7$&)6!% zOu_p~TZ3A#G_UqiJ85&*$!(+!V*+*{&-JXb53gtc9n3>8)T$jUVXe+M6n$m633Mi? zlh5{_+6iZ<%gMWMrtHyDl(u-hMl^DViUDc50UD;0g_l$F`Hb(F=o+?94B0fjb;|?Q5c~TWX>t8i1RP@>Ccgm z?2=z0coeb?uvn44moKFb^+(#pAdHE7{EW(DxJE=@Z0^Am`dpm98e`*S+-~*zmhdQ7 zCNig0!yUu5U#>KKocrg-xMjQoNzQ`th0f{!0`ammp_KMFh?_zF4#YhF35bPE&Fq~_ z#VnniU6fso{!3Z^1C57q?0i!ok(a zL;-f$YlDk%qi%n637_$=Gw=bBY}8#meS~+#X}Oz~ZKd%q(UE>f%!qca?(u}) z!tLTuQadlAN;a#^A?!@V=T?oeJ1f7yRy)H1zn_+wARewYIYr`zD=^v+D|ObvH4rOB zT@duqF>$Dk6&i|pZh?%Wq-7_kyP4l)-nqBz#G0lqo3J2D%zmbU)>3)5e?sTZy8|~B zPC7!`eD+deR?L6$6 z-e{!ihef=f<4HPZ9rSt&yb=5Q)BFAXWPR^~a&Zru?8146wvlm;<)ugbd|!}O6aE0t z6`#KqcH#S#*yz-K90+!Fhv+ zKH+?!_0yl|gWXSaASLcB9a8g7i%qz*vbO)YW`Q@Nxpp*6TZ*OO8Z|5-UWihd@CUXF zY!aTAZ$c^?4hiaq34=s2il}#Pxu=#c2^=(PbHNAyUqy__kR+n?twKrQe^8l6rk=orf}Mk80viC1NZ^1q zeF~g*iGp0=jKncK%s@#jZcn6=EiR<8S#)yiEOuwbG;SV$4lB^R?7sxOf8)oq$sT)) zA&nBCFJxsnci+)owdCHV#cjP2|1j22xIRsxHrLLBk3GI|OppUv3%r>#;J|26!W>xC z9gq@NQWJ`|gH}F{-QG#R6xlT<;=43amaDT>VaG*;GfPZJ&W*rO8WAQQc^JGw-fz-| zzAe&RAnC(gAP#FoJtt~ynR3Z<)m_<9Oo)XW}CWd50^eI4!1p4}s(zLhBIDi5r zr{UH>YIz2!+&Cy(RI(;ja_>SUC2Q`ohWPlI+sK-6IU}*nIsT)vLnuVPFM%~gdel}S zUlY%>H$?-rQRGTdUM^p^FEkqnwC{^BGl|gM)h9zkXplL90;yOcgt(8&LJwOj!5Qgy zu$@^*k%9JoAzwj@iSB^SNu#YVl@&*g$uYxxsJBvIQ>bfuS97JccQcS7&a z)`1m2^@5c9pD`P$VqH*O*fxkvFRtH-@Pd0@3y2!jW>i=jabBCJ+bW@wwUkWjwx_WR zHH5*XR4hbQ1`D@4@unmyEX)!?^~_}~JQNvP4jO&F)CH9srkFhf8h*=P z;X1&vs_&v03#BGc`|#@!ZONxVj9Ssb#_d63jxA6dX_RBt(s;ig3#s(YU3P3klF;mc z%%@^IJUAlGE=cnsTH+(qb1SxN@HzfAjYcUCb(VU)JV^3ZC;#k!t?XjaC!|68eLE zU_hlvOSNj7Qlr{x)y$S$l^2DPCMA=pzapcSkjfk*r!iWU%T{?<3#Hw6s1ux1^Ao6o zR@5DIfo-|c9AaFw848Y!BVG-+vURe;I29F#hLu$9o}oSa9&2sgG#;lj@@)9|2Z3 zon?%NV&AYSVnd~eW~v0yoF$X^1FR@i2kin0mFLG8-aA>hYK;B%TJ~7%P4?_{Bu<0t zvmI)Uk-MRncVb)A890>OqnYf=wu-J5A~^%4jpK~*xp)=h0BZB4*5uWrP>iRV+|kMX zv+BEskY~(P-K)-!JSHR`$brY)HFI|L@YyrxheT3cgHu}KtF%s%k3B`X)E_lA=E>M4 z2VV3M{c0*)`qZAsJ==)F#D~2Ndzm@hKhSBL_Sf3{ctckh-rB`gkfC?Dp6FdM?p;vv z#UlQMp3H5*)8o#Ys@-aj7O#brUfgQ7BjG`7 ztoE7v-tH2%KVC$xKYf%uvZD!_uf3x>h?8r!zYHkcc7$Gdn(6cDmYL&p3pCfaSfY4$ zG|yuujr6!Wl0}V%* zQ;nY##kEdvo8YY=SVDb)M>^Ub9e#4c$O&urD$uaRtxm-UH=6_s0m^^5y^_+F^Q?;8 z+Fd?+De}er^2EmFNn&e8SyS*`*`e;KFIG&+x5iWCsrEyH*0SFBCMx?`m5~hl1BrT> zr8W3*3}Fwsx@%UOuxNoCSoL%AM{Uj|v@>l{pYYI&D$j`&**;?X`cuOOk~?;U{~xvDUjaiH^d`A+gQL#Z?*lm)x_n6R-S% zf6*=Q1m>mq5|Niefl8s=5F={ncn5S;6~&Ns2)yGZ@wt&u4c+)Sk?hdfI^b77@K-=y zM_k=j5hp&u`2nkJK+2Lw`uLypr4dO?Bm3BTZdtWnQa5unCoTKIiG81t4bG`epBU5| zG{toT`)LE}&j{P+AFj`YZrjF-^>k+`zCM`QcQz^Ba4BEte@S}j=Q_Opx14jq|DB}& zNB44BOJ`?GJM({v`gh9pzbg8-%Un=E@uLfJwGkagLEM^!`ct3s5@-xqq*xd+2C@eu z*1ge`retZK)=bPO<`>@62cLN?^S%v#EsiPQF`cg&I7{}l?)}O$!^wNJp4Zd;1yBbQ zv@_7x7d6aXJvGHkNNcOg?A};m_Nq7H=(+zqf9)e3&yP^EU63Ew!NW4CYj_!=OTVb* z-ijSrv0M)u=MF=@+`3ldT-hzOn$Ng><)WL0vqQ&jH>W7EmLLQY+c?%i9~f_x&{OYX z{?kyyNZ&gT*m$(%-OeDAJeC^c)X!k${D*c;c}9)0_7iWMbfu)!j3+{*!Dj|?C`sGz z2xWha)#`9@p*{-X2MN2a;%FM-WqB2h)GTqQH$ZsGD#Wi`;+$i?fk;23fLpYI^3TT3 z5+Zn3cu-_2Ck*@%3^L3}JpVN`5ZJ;gmKn>gm(Z)b%!v|RYf(qrmGL#0$WHQFw4mJqQ85w=$tn^7(z|eJ$3R0} z2k9^EU<^-$ygq!ZR+7wT0KViK8qkAO7xs*e@1dq{=M3haulHwA0~BYNytr7k2K*(W z755P9a^;Hdl2X;K{c}yWr|QH?PEuh6x)9n{^3m2QUfC_Q*BW&<9#^ZVwOolx@6y9- z-YF=S;mEypj68yxNxfJ56x%ES`z-5$M${V1HX(@#R>%$X`67*Ab8vC6UzvoDOY*P= zFbPXany0%>rqH1gi7d>e`=PWZTG>^=#PQf&iJjJ0&2dO(4b8) zCl%8xJg1mg4__!?t|y_roExn~%u@Eu|p9YFb`8_qP@v#KW#kFs4eVetJ+Q+s|Y0?#D z@?dt_BA7C4tGpjOB~*LFu0!5oU(_xj7xA$meN)Z;q4Z_Rb7jY1rJBzJPr0V=(y99F zh=V-NbK+64rd#ltw~7X-%kP$R896DxRuj)p7Zj@8&>IlP&}ME3s9eV2R>SpUnSxeg zmpm?HQJ^u1T;pvwvlc4F_)>3P~jlTch4+u6;o{@PtpnJcn~p0v_6Po%*KkTXV#2AGc) zv)jvvC?l#s$yvyy=>=7D3pkmV24xhd7<5}f_u5!8gmOU|4555dv`I=rLWW!W!Uxg| zFGXpH3~)9!C2|Y6oB~$gz(;$CTnw&R&psa+E!KNgrE1+WkLM6SOf$>sGW+Y{>u?Fw zTc!xG{pa3c#y@d$d0e7a9~e_xjGcaw5f6Fk>lg$Jm}cFd%BO_YT(9s+_Q;ft%1*k$ z_cXkf&QHkaQr9U?*Gr$r6|bCV>2S)Cedfk3rO?JbyabY zgqxm#BM7Sg6s-`5%(p@SxBJzR6w`O6`+Kuo36wwBzwf6K{0HENVz^^w|E$r zdZM%T0oy8OK|>>2vSzw5rqoqEroCZ%(^OmOSFN84B2-8Z?R1)Pn9|5Xkui(fQRl^zA35EH^(JbuQd@Uh z2FJ6C(5FDD(++_NLOG)1H<+X~pt68d@JiB8iUQSZ+?qc;Jr+aJ8bKF3z`K&zSl&C7 zEgl&!h?sc=}K7 ziEC(3IrY?h7|d= zVjh{@BGW^AaNcdRceoiKmQI+F$ITdcM$YigXtH)6<-7d@5DyyWw}s!`72j`A{QC~e ze-u0a6A;QSPT$vqf3f(kO1j^%GYap*vfWQ@X=n{lR9%HX^R~t+HoeaT5%L7XSTNn` zCzo})tF@DMZ$|t6$KTx+WQqu~PXPa9FL&shBGx3C>FlGz}7gjfv}(NKvjR#r5PL$a1>%asaylWA8^g!KJ=$}_UccHmi zAZd5c{I&Ywpi3a1#27C6TC~zm3y8D>_1an8XHGNgL?uT$p+a<5AdWLR6w9jdhUt9U zz?)93=1p$x;Qiq!CYbX&S}+IITWLkfu%T6X5(pk9-fs8lh9z8h?9+>GlFeFcs*Z>u zJSaL!2?L8LbOu_Ye!=4~ZKL?643lcsNn8>qUT|q&Rv+(z>Z9=tyG&5}zZK&Q?S!nG zR;Ui^<406=jLYA>zl!a-OXH#J-pP4A`=)r%9HV5m1qGZ1m*t^wi>3$JRcH)3Q(LQz z(3}~y3=QsUu!PN$$N~#yBP@=aJ+Bkp_hx8^x1Ou6+(Kk9l1CXr4p~IQvq@AUePuAj zcq5>YDr(JTmrAuLwn6sgohTR-vc^y^#I{grF7 zg}8?&5!^$|{X`C;YrZ7?rKH#`=n0zck(q37+5%U;Hmds2w+dLmm9|@`HqQ<5CUEz{I1eNIL?X~rd{f71y z>_<94#1G+j`d5|fKK@>QDK6|HRR|9UZvO6HdB1afJvuwUf8bw>_Fha)Ii8I}Gqw}p zdS~e^K4j{d%y+A#OBa1C4i0)sM=}tjd8fZ9#uY}{#G7rJp{t6?*5*A^KKhim06i{}OJ%eA@M~zIfA`h_gJ_o%w;FaFQMnVkBT|_ z(`m9r+11~EPh9f7>S=$F7|ibj=4Pt>WVzk6NfGRvI_aG66RHig-(S%WKRLP%_h0He``xT))N^RI@6!ADl=*vsqVb|7 zr~Lwl6qn|u!%is<{YA`Mde2Z${@EAHC^t>4`X;F9za=RC{{$4OcGmw%9+{$i@!cCn z;7w~r8HY->M@3OzYh+L7Z2Lc8AcP*FZbl6VVN*_sp}K zQP|=g@aFthq}*?|+Gm4@wbs_?Fx-HD2%)_UDJ);X88~7ch~d0cJ!<7;mv>iv!RS$a z;(-cYTW=K=|F0gIg3EW0%u2CSr(Kx}yLoki|KSIt$#P(O!=UjBGRzb3L3-?NGr7!! z^VC7_Q(GhT;C*(bLivfhlRDVdz7=h%ABuLA2g$qy)A}U@Kj_L-Jd|--fy#-*ESRo| zgu?*?jGEgs9y>1`t}|^Ucd1I=1N=mOo{8Ph zwZS(F%G?nfI{#%sGayNItK9J5P)Qk+^4$ZoXZJ0G1}hwcckJ0g-QJ<)3%`bF8}(ahYIjKFYMtg3X;e7J18ZvDkV@N=nxvDl zo?}lXoT3pZY;4$QKI`~GFuQKv;G6b<8;o89Hd2yu+|%sU(9C=h8ibwZ zARqZ#lk@kp4*#URe-YmpRc&=-b&QP>5b{9{(tH*)(@ZPKfOslBgwCPx6d*{XMX|Q{y0F!5a^ScCE;h8bQmTJR3*}A>aGcDF0?tU)Tnml z#DgruwAva-fiU3s*POY_ZHiJyW%v+733X`&ocwHz$uqJCOhrM;#u*V2eK$D5HiN(` zII{BEg(PV6#_Nv3rZBUyd+TI!>L72KW_Oml6L=pNv#aOl( zgpYxAH^@2aJQu3urlrCeanwSpHHD_Cxb+=cm49{ZU5Z@;{^{okEJ6&fpDD31w~$`% zcz@_REsC~Vq>3YF7yJ41ZEPBW&%|OwlnfG|QNpiX;fGR0f^3?PEf|-33P&LFGe`8^ zaX3M+*h+?6;s|=$j*d|S-r6PSHnmLqm9oshPNpGzlxV21cFrxcQLidd2%h>n%Mc4{ z|JWBvtbb;(-nhWpPO95hR>(e(H$n%*pCh0k4xE#I%xu=#B)zXSaH+azwCI;0@bY<*-10-Qyaq%5NxSlq_@YJUUwy z*d;qPjW^cuKxdXiOWwP}5FN6SZW~NqB%4?|WifPNZr&XNVkzF0n#Y)pbaEodqNO4F z2Bq#^Gr^Ji3!T9`_!D;a1lW$?!LQ-iYV_A{FQ~^C-Jp`_5uOC)6+mzBr4Nl3fHly% zcXeU3x-?#J`=p$6c~$T~V^!C0Bk_3#WYrtoFCx9_5quCQ*4*?XG0n_9%l_!n`M85^ z7}~Clj~ocls6)V&sWGs?B<`{Ob>vnbXZwdda%ipwbzOJ(V`W>KBF5zdCTE8;mc&xU z^clCzd0(T#8*(})tSYSNP1N{FnNVAU^M1S_pq4VEQ*#5nv`CoYSALMEB zf6egyuRMzK2?r^M0hCD*sU;On6c0^Vh|#tRG*n1p5R)QyVw%Va37nMSV%9&uq^hp| zCHeu}y{m=NsA=naDy;q`fd9t)I$Qd-A1Il$#0KyDc>X)hKJViqNB{HnQyf5D(ZJ*J z{-oGB-%Q|QZ%Pqu34>fCy)Asi}IY7luNR9ebgH4DAjCVvSWfa%PE16 zkC7EIuEK}?IR!jgP%eX%dcxk4%N!zIjW4wYMfIq@s%GetDs^g!^p}DH46EP`Nh_wD z4Rwc4ezh1U$Mc)Fe6ii6eD^*iB2MFp-B-HhGTR0tC2?bq$#^J!v1r+Z0y+& znVub*k=*^0yP(c#mEvX}@Abx%&}!W(1olcWEHAVgskbBrzx(f2v&}4~WkVN?af#yi z4IE-(_^)?4e3(d{F@0<~NV5|e0eaB!?(g%l&Hq$UqzC_Enuest?CL+IrSD`tv8|{C z=79vnL=P6ne+}6X1&cd$kam=jCcv`~^y#R{doTh?6D?H)^M7-P+=D@?H;bt$*V+)K z?+?Ex3Z@8JE3c4eHDYItB^tSot;@2p_fuZ8mW^i^a(L;Xn6K+1GuG0n$v(38;+<78 zC?eMzbQCW2%&;U>j}b>YEH5>RkP44$QlG6k(KwXtq{e#13wnx5Jh=uH?lQIl8%Qxr zq%pDC)mYYKa?N>%aF%YwA}CzV@IOV9&a81d9eiU-6F&lGvz68~%{&4LuwV_5{#km3(tf`fejjs%`{Y`|0p!6|-U z8XQA9Sl=*kM|(2KA!LWOCY3Qq4sZ7r&}__rR*Sj(9W8R1_RxI&4TI+_7RSJF&-363 zJvczH?1(`Jb+RDJL9$Whnj8qJRI+Mz9=Qjvubb=Lz8nWVXG{Te;$%s9-D#$)-!{~w zIM(vkr#OM>2F7W$$Lq%fEYl%e|Tsc>9rB9c8 zQoi4nXomx3&sBI9AwaHkoOp%SMDf2@T#73Bi?|!r!Q?wc(^b_u4ranezYx~=aRV-a zD|_WPK^iJh&=)~h{t<>_$VMXsee;{r-|`#H|1?DZgWvuc*!&C2*(yv(4G5s{8ZRzt zZMC~5gjiU@6fPGMN%X~pL};Q`|IfPfs0m9;RV}xSxjb)*gmvGO1`CQb~W1M1{KwXBLyPz0JQG=JkVX zlPq&zNZS59gf-?*5Z0IFitTX4T$1Oo#_~V%4q2vI?Y@UkSHh}H9xZ1va}^oBrCY{+ z3wwj*FHCsS2}GdSG7W(|k+MWu9h1Qs6cft~RH)n*!;)5HmPX1DqrJ3-Cs%i4q^{$N zC&skM7#8f{&S!9Eq-WqyY$u?uTgrSDt#NU%{3bQZtUSkUof4`Z1P8aLOKJ+^dKh%n zfEfQ zO|P*J>;{=`9@D)qpnt`#NH>}sir*&oFC+W!HR)ecHcPwjF-|)}8+tR#@A+~CLl+Ab zCqp+=Cuc(&VGC1ZYg4CxIXYL>33p^wjIWJSh6R=oq)jD52q3~KVGt=w_z(arS!gx^ zSd|?!rzDu1$>0o0Y0+!iZU=ew^Hr+cq(I(C>9}^sBc++0+S#I;js@_NLD9>MH(tN3 zE5F+J_bYdPfYm5%7-e=lm?!-xlvX~nDkBqu!Zf0ra65JD&@tYDW+c@P3W-YyWe4^6 zhW?FUJ;c{^?b`N)03>!@#JI)r2&!6An27q?*^wyUx3T4uyeIl4*(4CV5OTK#RSnYt zq<+RKCdrYIJtdmNC-NtfH)K&pytbM^Mi6JWjkzJo0TdX>HOjJaIQmQ?Q;l2)8oN@d zVyT=%y@TihQaJX7#B2wY#_ufuaF55-sWO{OwUx$2zRyW$YM(CFBs4Y;YmBk(4u&u- zEf@rIR~4#}IMeq$?T%z3s3RAR7m%M?8No;a=1HXKP?ia#uwy!`4v0GFSjZiMii@ib z#xRmA-v~CSVl8z9cEWVEk;9_BKPS6Y2|bk#PAb|}gPxHs-dt*k`5tU#FZL)FLodY8 zmb!m`DagEJ#q1VKwO~%zmw7;LESf5u!KJNm829pbY_w$P2}16`Bb?0uoL3~V71;_U z`B~wKOB7Bp!Vn!M@o?RHydmah!dHPaT`&idV83kQPxA>E=~YgJC<)rdM1#B$JIgnq z0V{p|Cm3eeMaO58Wrv^9-kAOJ+*HR!;;A9z&>78VsYmF9$U^*ZE=K%d7=MZ~G?~Hz zSHlKWK!Us^%?uE6`E|_XI+nC354jkbUPvedHbh(DkKGkquYf}=-EEB1g>RC{O9ORL371y8V*CR5EW z@lmFq%MWEBdeHR7%(Rpf!Yg52vX%D7#@*^M`fy7Srb z^Ta9wcwf$89uL61@qeg2vc&TAGKSLV>YKI3#5lfs#q5Zm`~Ogef!!CoWWyiA=J;js z%X_n!njeF2MZgaVoMh@S@8%lR)AsYyzmqkj+C8ghxI4G6O7ovK$udULO!2$(|__`2~6JjuoERet}kenJ%I0pU_O@tU*Fsd4gm&hV?p%Y{!;r}{S^Fv z_4EJbVjFv7>+dE9{rBS@8&_vbx9>4!8&g4JV^e2mSwlNR^Z&ujriy)b3jzqfYb35o z!;J+c>%LY+?P!IticwSrP;x2|k>j3Sxg2X%E2%57

`Lem|V$A>eR0uN8Y&sdjtu z%-lD<@61@6?qUPjUg|mF7!P7`hx+st`i!^L7HVHtzwnM z)LuOANIzT#9tU4)C^WIXhZWqrO;jr_O5aErkklzt)R-JmAh8xHMJ>x>OvTiuRi}FY z-o@0kFwwl7p|ro=*2q*cFRX5GCq-v!LPD)Sq+Uz~UkOwx-?X&!Q^4H)$|;=n9{idC z0mJl`tCTs3+e_EFVzQ}s`f_4fijsucWy5y zarHoT>Q06Z4yI1RPNpW`@4hSzZT|J`MU3i(GqNhm*9O@MndJ{31uA^i zXo&^c`EZ}5W)(|YMl##@MuSK#wyZ3dwJEz*n@C(Ry$|d`^D=thayXFqxt*WW&sWdI zdm1wv#VCKa<7d2Qc#qzvUvivhK5wq*djL7Wqjvf}-c~}d#G)eG`(u<`NGei`BFe4Q ztTSs?Gc8Ff%_5T4ce&J0v*FT`y_9r!Po=sPtHs5~BlV6VEUNzxU+)+sX}ffdPTRI^ z+qP}ns9yQgjY^t0ddMx1Yd`|OB{sHnUC-B;qum1|`tR#P_@llx>d z=qpNN&?nZib(t90A9F*U%1GbB+O;dq!cNgmmdCrK=(zS1zg*9(7VMfv)QMkt_F=wz zHX2p4X-R*=tJI4A)3SrL`H^peBNHh&XC#sVR3D zt17qeF>BaCZNlQO7n@@BuWs&l(FtRjaVn~wW^x-GsjpFH!ETyl7Od{Wf;4=bzL5nj zW9c^ZodMnN{3Jkz2j2;qhCm1ede*6891vR9?(Dy)N|iENw}HKLIOrjB0x)pEs-aS{ zZR$tEyZxbP(;(l43^KjRtSuirNmw~Bg&6p;)vqM*>S#L>0+Pw5CU%4@&)8OX2ykYQ z^f^hk-5%!QzuzYniL*1Gs#S5Kp_*ld1EAmkInP+^w?#(?rbC2Bm&0c5Ko@6`_ zi!Nvd391nu^@AmpZ$_0fPR2~kQGJS7lSGwA7U>s@+!d_`(P5y;MT#U~_ONSo9d+bf zVj6MgWN=|%#Qn;vl*TNLE$Mw|*89{yJ=WN>j{?T*vqa$U$2_dg46R)8wl&CNS&iK{ z>HDBC9e3b3roJd}gK!T>takKP);KLj_9T;%knG_fN^S$4hb`E|)qy__^=mm&Z{~CF zhc*PxdrJ@xRkQ-8lbh3Ys@2ZaR)Q3z**-VSgeMHE>c5AH1bpSUor&dgTiMd5Wn|(# z8Rwb{#uWZG(Jo0co98|mg5zF}M*d>gAg|Zdex@}Ps&`51({MmNyHF;GD4EBT`oP|X zd=Tq9JYz*IP%@2oujruVrK#jAT97|%ww60Ov2He^5zA4)VihJ$-bxoaqE7zU$rmK) z#O!xp&k$!TOEiC8+p6`Q)uNg4u8*chnx*aw=#oP~05DS&8gnL>^zpBkqqiSQA{Ita z%-)qosk1^`p&aB@rZ#)&3_|u{QqZO z{f{A3)XMprL}2{=pM$*`z*fY;{=4e=u7&=s+zI)ANd+V!L%#^2hpy@#N-WbB%U2Zl zgD_E0AVVWdMiFi_u2qqxeAsRzD%>l|g-|#$ayD3wHoT{EUS2Qe zEq=ryLi%iMZ`b}tSYzHInTJ{mY{OXy0)T&Rly3ippqpTk%A{T+e?K}j zURM^%!ZIWxW$32?Z&q9)Rao;#KQuLv+^ft>o|6c@QD=_}ql%5Th=cR{P)_51Qxjh# zRJW<|qmpRn3(K1lMwU-ayxjsgKS`Q7J5m0kw|LQb=CbyahnoQTWY z?g8-#_J+=*r`Jc|A0(MOvTc0kT-tBLIIFCd6Y5iCr>cqubJu0`Ox+FkDWs^L{;0mc zxk-nf?rxh(N<1B;<;9PSrR4D<*5!DvA()O7{vl9sps3x_-Y_w>qC3OI!_Wyza8K|E zAvJvWYyu)(z*TK7e+Q#dFWd_7%;fn4Ex*lEY2$X%SP9K9d6yWC2M!3>3>tu}g4R*V zRMC!~oYyF#Izu$lGjfQ?q}KD$rpDMRjF?f>6kuBlE`z4Yxy(Y(Y+Dr#PKA}UsSWD? zm|ER_O==Y22{m%cO1jhu`8bQ05@MlII86NP>-_`<|Q4g1f7Jh*4%=yY_ zafIlUJ2zA?dT8&WTGLE&gvPl|<0zKa=DLzzPOU7i#nate!Z3u|9R6E(6FZ|(EZ%+b zsB!MEkGz1K*oXGdp^tGOWyF0SI{tq>^nbgX|L>uTert_v9gIv#Ma|5OTy0(c_qQUz z!2+;T+eysD^IV+aC=aX$FPzbq+lZ7Gsa%r9l;b5{L-%qurFp89kpztdmZa8Uo!Btl zu7_NZMXQ=6T6+OFOCou6Xc_6tf!t+bSBNk)mLTlQ5ftr247OV6Mc0v+;x&BNW0wvJ zjRR9TWG^(<$&{@;eSs-b796_N#nMB4$rfzYM1jb>Gu$tEpL8-n>zGXVye2xB-qpV z&IZjhW#ka?h8F{QJqaK&xT~T;$AcKQD$V>$$-$x~1&qfWks(mJ8#7v7m4zpWw(NS( z5j0d&Bs4g)>{7yzl-7Fw`07Sj6{vw5nwVyVt8`;Rg5bzISP26=y}0htlPKRa8CaG# z=gw7__ltw`BWvICf>5(LFDFzC7u-Ij7*OKwd7685%wb6a=QD1CjpQs$^2~cx`@xS` zNMz6?Q4OgIR8LYa&m`q*QJ%!CbD#=ha?38!M&7yLA1Wn}M{$nV3-G0@@bD#WjCYI) zKFZ`bf$tFF#}GYZ7MK2U4AKI-GY*y(&DCt~4F1!3!{>cK+7XAfKw<)Jv$b1vHkpC;gl=VNy?f-RI(r=&j z@Dy@&vHYi$GBI*-`1j-=qpI@{qwt%et&>`VuG+PYzF>DUM1!h|8sz~*0>sA7|IH_y zskL`MJ4Yw|Ru~}gzgCOOEDSyuM+ivsjt@13h-SLD|INP2zRO|RKEDz$_zlt)ZWYQg zKHk`_;gygz9b$7*)WKC(<}zQUY8M94a#Tu_OEyX$Lej=Cs`b}zjTYvv-Jt6E^_bV) zCt>gvm2{y2tK8Uy*;ruhTa_?lSIlV;r8b zX?jME!z32pO8`g9ga%`RQ*v=F0O`bnPZebx@b#ZfQWvqZPAb@zl>ORo<_o7Dp&F?6 zP(tBH@~c-Zfx?Ulkb{F`C1S8y3F;;)^MwWBiBPQ1D=;yC{M-i~ILSfh3K!Ai{5c?J zdLm0OmDsWuV>%}MT*Qf<$UT+M=7pMVdJGRi-rdW>7iM&2UO%v@>_!inA`JD)lrKC& z75Y)Lg~PVq0Ge}-g$8cy0w@sHjUuwMm1|~u6X!*fGG>%bAbv5cEU3nR6&6o03J2ff z)*M)kj|gyvZ6Md8Y!m#IuWuP0<9daW2gPDp*=aQA2qm)VLJ($UUQ>-4&3LX|)=-g5 zDTzngTm?JwMM46$Z22o7jlr3Vp3K15k^@=c7JJx9WQg*XbLRkdC zYapmoZr8J8X5n5}a2xjY35bC^@Ez{}9JA&aex@>JiMr#&GtJGn$)Tt=HVKx@B+w50tPaNkh{N0!^9>r<#h(fr3kP@a(N1!O)$rdf&Dd!hhJNtXD zIbx!f3YSHV50oNza38Kzd9Vze|NZlyBd{fKzZOSB7NqO*qDh)*>XW~VnmJ^ zji(MF3D>tHCk-^y37b-c7t1Zrt)VBlefNnY+NH0u=9IPbDZ1z8XbK{5_W?~aGs@o& zTbi2gdn~PB;M%^{Q*d9xWhw;xy?E}nCbBs0rn@{51pJ@6e=LQg2dvlq_FM0;Iel9= zz?V~4Y+a&wJIgvt5@%1FDtB9(A<-f!NpP^nl51v_hp$v8$w{ z=Rh2*Y?stNGlx7wbOLqrFbxg3lqpaaN{@9c)nNxe#D=Xouh@g7Wd}stZ!B8jrc4HPmOW%Xt^a!LcN8M4^efD8wWziBkha6&KggDq^9beRoiLH_z9 zGUiqkIvsoqX!3F)6qr+_HfB$D%@)T=XV3YUews|Tg-Hwn^wh3)q=N>FC*4nHJ+L$K zpR;I6Gt%?U%!6mxrP$mlEEiT&BVf$x(VJRuEIXdqtS+qfX^-@UKefF=?Q z(jc2Y2oyEyr3_bP|F%)C?~RzdfbNXgw%b_zaAs2QbA_QL+IyP^@l+{#{17?2dn80k zljl~W{3$~wO4E?SSij&`vnbpKCUzN%8GY^!-wNR8=XKiz>yng^Xj99@bTW|TDw5XGfDje2@E z*~-mJF8z}cI1eTpHlg*7?K(U5q3H%{y84gCiDbksT+HB=ca!YVTu zgPDuJzB@76rs{is=F^_95WD#mg}F*~wRr~vgN4^*Gy=hUUD_~f0QPh!&J7XP9zv&H zY}Zm4O#rej< zQmBNK_0>1jXd)Y3cJi(*1U|!mL(;nU#j_WV33)oK-!s$XS(mQqWqQ7&ZZ54iT5+r| zi|MH>VJs`1ZQr<{eTMqC#Y~41>Ga4BuQynUV!QuZeaFa6aP(B)SxC~V-r0K5 z5BJ<3nuAkX12%0k5qI=#D*PNg{NNjn>VUnvH!{DfD}FX=e%E5lw-IZgDqD$1an(zv z95TXS9wGg?Bl{w91nOC8HvvD1&ENr~L>4u{^bNaBD>ZHXIw1Ko!;wjz1%zZMbWE8# z7f5xlDTQWK%rH+)0KY&O>*EHs@Ha5t9ltEE{qv`K0tO?W=jgzciZhHZ4As;i<7{@M(!#&K$4UGQ?~d6rbu|rCYd`D!Bgha2*v# z?6){N62Wq7br9`S=y(rk$xKExQsyv0H~Z<~f!Z7~Wt6SlJBO4_KeNahC?2rxh%Z14 z{6vx|=@Pd?8vwjCEbf?V*zgc>36eg4u4w8WMluPe+qB=i60{qnN+XKmud{LfKvd^Rf{8@jDa#RaXtvGeC92KvnMDV3m2 z4Xt7QB96VazV=Z?RrMXb$#mb85@y7X+OE;c6PL94T|ssUhD|n8IM`GhqU%%}=6E(! z@O+LF*%Uy084M_#De*pBSU<)G3|%go1vt<|<(ZKk{3&*44f?ftxS-a(+@u_92o7ot zYq%I+Ztyt1x5RPt_1it>&+05XbK1B{-T~aA+FN6BiF@>|QCJ`#y*u z@e*p+J|+Jzl4qtDnLJPde6Gl8Qfu5eP#Lr_}cyBzGaR912ca0h5s# zbgocm38uvIstvyAPMEgVj^>{XqR&db7$(XJRTRiR@!lH>>CTe{+zRJEgcn{?M627> zsw6}Y)J+s3)u#g*Mo19)oWp785&T@;fee1**^o5#bgS4epuPWP>~Y2v-~{)-me7SK zd!AQUXsd{A=;C;8>vRTE5Dol&>XJ&AYMijyXV3|_46Fr#lz`uF9dT^PhX2e>lDN?r z>wx*9-Pr~siloVs7@`dn*kGmY0xP)2odnz6S437Hi&}MSb1iiwEiwfy=f;yg# zDZojIe7{n|lnmh@$rU>6-%oUGrG#^0y%z_Niq4LG38Yq&Dq<~B-3qLMHLbL;&A)i3w zq0}L%{J2P1a z2OC$%f4j5C`~!#oBU=IP{19v?%zqxLR77sUDKZWk1TEdClEz1yHB10F7>l{;9l0L|=ADc&?i zK#F90YE|)m(u4LGC%M^0?53NrH3M`xl2{P!5+fC(H)Yt|t=X~m+os4b6}Wj|nDvL8 z8n=Bhi`Mq$&2sm(8n4F2)~_ylMf-R2rn!V)Bfzhv7v2SF{79o}>ITpgUpe=zcRpds zp^3fse>q!&ohi{7gYJM|qD$1?s^vyP1XP=26O)1AFu)?|OCYHCJm*LP4*zJ8Raq1u z)9(U+oYRkni_C&!f4&%ORK?w$g6<;rT((@LunPCC_#2P zxJ&Q13mCI_U+H?IvV89Y)i_#NnNt!>xavHwF$|O zXuHG5oCo;G6F&W`KV4I0A-(zyjQ;ws!05mAr~eli{U77e_#bTiA4Hr~$mBnaBxQ^3 zlOJG&4aI|YIUi&Z#TBHjLS(GmY^z5R28NolKW$l^Ym#0I3|0lI-ggSR?CgqX8f;MBaPl&YzSG} z4(9gprQ%M^N3g+r;f^a0BNw0BQ9}e{Op$ssU!0cTdbP z1%BNUh*RkAe#+jya`#(*p*uQ|spESDMarSs8h3e`E#gtvYi=8d#ADvy9g>R@*^D~F z2t#h@kzA0JK)w;AMPg^lWi2XAU}jpiDF!akXK|rSi6}wmaK)KT*81I6M}f%l3XCMR z-&LC;?s53?Q?B;UuDeB{5^S+oOfSGE^CnkvgEc9^13~<4(iGap$VY8}3$6;-sL}t1 z4d0l&nxB@pZuYHH` z{ONm|SH}iy2^)Zg%Ou?*Q?I+u&ZmckE<;nVG0STB`M9GzLE5UAMeRQQJzJxXBBwA&_T6LHe4yGpP7i~lax~#Ub5BlJE zg>YF0Yn0Wcsv`EJIW^d7i>M?PO5_+)OxDS;9?zPfCH;#_rpR4-*9!|aogttErPHlR zUf2d~4Xa7AEaZSe)Mn9=Nd;=@JUDKUaJU-Rx~HXERZPZJTiBwHdXup>tP-Z$yw6H? z{D8e~w09((x@w&~)75oSpJ7o&u#DUKXAP}9afG;3qf=+XWeC!=Ip8PJvw~{@B3H)k zZr>U-w?x^Y3%$zAfoF_*V2Mlr?I=_C57F2k-rurm=_3`CHmW^yY`ye5aJG#E#oU&y z^R4vJ!2z7aF;V5BD1dbHn6(R25;-0cu1Cet+$J~Uw}=H_%79gf!-W2#1g=S`%zSN- zwVT1}5o>Hi-DpkU76(;YW&Y92O;@cEU^coXt>XfiRWI$}_*t&RQ_K?A8!$gpQKZe> z6VsBW458Q0>X1E#m*K&U%))^SmEntSPBAZb7VW{C@EA7Plo3r-`7EMb;;WeQn0bRTSxW7MTSYNoW=(qCsKsMVCbY?$#Z{|k#%NHM zA*6=sc(VKVE`UVqumIooHMGYRSh$SD{ErAy8%i_*n<=4ODdFErVql6WIx-X4fyaoz&jU+aYlbi=W`&5GJ~zS*@5IRv9cn<|il?|!d8>N94!OI0)aLF!Q0nlhtv zV$SFv61Ek9=p#mMT*~J{BfjK)?1ss~7B8LE@RPM6>=Q&sCt<9ZWOlek61x3T53zDy z_Ki;P_XP~dr)aCdrp;^Xx&4zy791bkXYcFE&ul#uoMVnctVZzl-Azp*+fw1N@S40^ zWBY6U4w+j|T8!q!)5)=7rk~;72u(J{qztk$Rb^WOCbU62Z^s|pn=)TqT4{gYcX?y1 z?|~>Cvir?R7Ga#&UI_thW{axhKZmGsOKK2*Z5|H*2nrEoD6q0cA?LAuQGqE#iVxT) zkKFW#vDut&E=}&^_xyn@nKhBk4S$!WNK~%$ z0c&2{SDdyuxlzV0ph!Peph$e2NH|n4;u};Z5-fDRQCkV`hd9~Qhw#l z5yeB&7zlX?y>QU?3e8P%Gzk1X934Q9LPIvcZi~Q>$tU#A^%^O!FsqRvO1M){#{wo# zBk9bs(!8G_zMYJ-^KkkOmXlld6&M}R+at4#TYfha^(?3_OqFsw=T6Gudap+sqFPF0 z*6D8MYBS6E;rkj8{7GbNPpnUPv9*l#u0T^M#yAbod>pw)srdC}u6;9n!}f|*m@!$~ z1aL-1&ei+i_Mkf0!?>5p@ss}z+(4GaIZ0Tu^mr{+M1{}bS8k3r~HKz!?C`p>TW)1H#Yg*vr z7Y{a{9Z}e1N<7QR%urOa_cLshyVKNaKNU@l7j~j>PeI7MIZZ|r0*YSjU6P_&ia|jH zDoChFYF-JCkoNDw*&*{QG3x+J%2L5_4`n1Tg9hatvloFoYL01#hFFj~!}MRSdgSSl z=m-yq{#uwWUIpuCs@%BEy5ob11|s~&TVX8~-XV)oMfeNdXD?Z9E10-tP#Krhiv$@dBpKj5J%t@Y2xI!*8s~Z z29}0zR`_9s&89Brq4Tru3F{G&uQu{ujBFqN`NY$Hb>qnXc(a!g%hbv!R@n6sNonM) zg649UVVIiIE)_J6eMZ?R^6HGdRMn-UD36*c8_Z2r&xc^Cs2p^v6x-_j{J)k91n!wt9I-~_PA$GNiLi=u7ixtk`YUQ4uIF+`SI~U z1J;MiD+DHLSA)nBsc8CJW1Z4F5uFXI0GzFHhs4egAoxF&>1&8*Nl_OA^!wW4GJCRO zwS%7>sOyj*5EN! zUpux=mBP|Q*_J!@%f6V&EZf{?`H}D&1^^@HO#Gta8P{W+FkdO5OW;fnD1|4&tlh3} z@YGnJ3d(Y0t#ep+bksNs#e?8*u-V=@#Dvz21#EB=jam5x3MtG&IuRHU$pr(K+Y-AX zn7FqKEk!?hw{HWBS~^ioY8Dbe(VtwFva+1h5$-}M9!~UYHGIL>zwFFN1`lcLe zwaMY%;tKHw`EL=C_^}jKY3YhWzg-&!anlG&@4E|`Vl}0q!EvCtT1I@}=Ug2;8OzB) zmllrTJ}RHtO2N@|-7)oaf*v0`{>2c|j?-t&WbDWOUDsBIUR24HnS0{I;>(%9+r)y* zg2K$nGPerx{E6HXH@h?eRQC~Y44A2^$`xKRwnOj_7pT5_!?K%>JT+F+ z6(@ZUF%FqvCBG2v8WL04A5>D=m|;&N?Hzcdj=|%{4JK2j_;hMKOfU}I+5PVH87xo# zc>v2%1gFE>V^6x3$7#ymLM62}*)(ex+`ImB7=eUwa2O&zcN_th9iPz)#fXNbq_VnK zg>+Fagfb53(>-Y^v23^|gST@kT%3pG*YUyrd-zn|F0Cr_;Qh)MO;mTE$%x&%B^Oc= zO-<|3$Nplt0sdxXQO`|RVIbVxm_^24G_6XuTxk&{Yyl+?OeXa-!t}8&fuTGLZpS|{?$S9qu^8TDrgtdOu`4*Sqx20lCJ(;z6u7&0EbrB@495}e zvjfw8yG7#Eo7QX+`k$3*tbTCwGm9LGOvTam&Kk&4&(T!!b0d-h(+s160p@Pn+_M|) zwasiA7r)El>t5DJfiBLb@2=gQDN0N*FfYuh&F<6BNcc)=oqju*S(+ucbzy4pyN1%s zgS@}T`xoCKJdeoM>hW-Zt9xSNRYI8RfX^{UPSJ}y8$_k~4-2G8KZDJQl``0lf>>)j z^q^y@`VIX~W%W-QAF*8U#?c|>tGQ{a09;)CL{-NfEv_2<$o(R8`V7xFRTl$)d~KX! zxG^v#xd(Z9R*`P* z8NwYSrl;qaYDzF0iB%{|A(v0($}TDr##;!y6paThkw{fnuKExakKusCdM>46hESJo z6Z4inrJpt`IzSB{l1R?`XS)o3@M9OZsiP&{y4g5QBH!U*Fvdd|9inn^a}Nz>2&)`? zh!|tcpGBMA4e|H2Y3)~7iyNUBsc|aN0$HM9Uc2MDIL(61;J!I)NmIwv>&&25`&+6M zq1}!I%Azc>=L(6nYlCWwU59Ea*szPa>sE|5)2pJsAnOmce3ZqxF(4^b@uZ6D1K#-5 zD6|eu@+l+j4}V7yxluQ@oX?sla^=5dw}yP&j6E+69hswg1L1c=)OyvZ7^wHQJl;ml z_2lX#$i;=Fs}vkh=ukc4y2Vj2Lu7vAHQ*E%@5?3`^a{BzDVU zF)O4|`;uuAO@)kfdwp~fqS#rR$4Oj@c*zBS`-fL6qu8<7qzl8rl--^kjiCV!(vbxC2vIdMo2I^X@+ID zcT&$52_`~JOBXh&mXX+ceO*m*0_=9ArqG>xjMR;+M=q{e-N#QEj-BCAzAVeGSrXNh zCV`uX4qS?7l$u+*J~5P?9xlU2%6rgo30lJ)cd|FHtEmloD@8tO@5y7N5t*NZN|hrm z*0FP5k0_1u5$>dp#I>8az>my1NoIAqBZ!Lx(!ohP^U@&Vmqd8 zH=75V+`}JpR;Wj8!j6BT1WSjMs>H+3_*52JYs(04P<@$3WEVZ7V%N-CLN$onNB~*- za-hT{!s~K{EUyaw7zDbp7n5T~SRV3$*>Zhpg-*51L=Zj|oeHx)1Mr4juj_5;_<5%8 ziMWWR&MhgdLq0$}U0q=ol1xb)TQBdcV!(3$iF4x~ue+F-gFAGMn^|`*YBjuP=jx!~ z06>UuQAq?Ix&zn0^To|<4!CSXZW7o6VrM}5dYxV+Q~8-h^Y9DzNs{5%+kyFy5cysy za}2EkZyRxQ^Rgq)T6r=({uw7y@%D4S?wd{Ck@D0(;mjg4NbY$Z$xd6rCGrNITO04Y zO%6aZ!9hMp%kU=V6dLc($d`AHMbf`&G9BXY%xr$$hovCbBj@|K2-4_HjW4Xn{knIL zaKV)PQkC?JIKYK?u)1`rzd)G(eO222!%q#U6QaT;SUl*MO9AvJ_$WC-@uTOjb58L_ zQo63V8+G)0D~=S&a%3>qqG`7N+Wfi$Logc=SXGBq3&TV|=!!;Nzi4VeqP9=hV>H5k ziX8p2v_i>9nc1rQm(7T8t#sTSGnI9T#Ms(_k_%sm3mT6gc=YrdUm@Ip6xRqL0H93*Yx0O!3Qw+_Y!81*n-ovS%iBlXx62TFNbk8K-j=LOV=1s zwc7i_TsS%sk!R7r81r4v*Ec`Rrl_m zr2$@wBrDGJ1`%wG6Ar259e%+MkZzK88-X>M^WgfA@HcWJmPUeFdO?d0>gvCTn0-ZWgb;$}~gdQiffS0?*jk$T`izb=V-&N#O_U4yp?Y!Mdlk09!o82t}+5dEvSj%vN5 zCBperFlf(sXr6C$n?zYvm=YYyz=~W1tkhvu1wODh>tKoBEiRB9*Py%96luTxm11-k?Q=g$c>y=q9%J< zVbw|kc=&DAiz8G*&G@8XlevEthbWV6a7nM1@VjKNkP|sl%x3(c9h#|9HIdVuC_??C z!MaVTrRI4=oMEugDa}D)#f1zPsr&vLR0Zy!7;QA4?x1w?=X%tH7o_(2z@8LjA`t^# zft3pe@**E=P;MFXEB+)Zh$?+;5%i6ECfT?A^~N`o&QHR5@V8a13HuA~omH+0(xm&s zJn#ru(@aCcl%uY66t2-NPi-*^o`hAyJ}I5kdqib+qh*CNP|jg>f!Wj#HJ<4r?4uCX zvkf`dDbhurH>#bk@3|Ap%0+kV-0PkcrZb0Q6)EJKBfaiae*!zLC7wkQ?cY#avSAHH z-b1`V^N9SgFL7-JrVQZS2rsHMA5v)j^@ga==T4XfE9yy6w7~pXILh8O)Le{Zg)9`|o`-$nca zc~hvlgOB$pGXop$oW3PzOuUbE^uRf@bo%^%%GEHQ}3uc0E<9SxbN+Fk6DEin>4 zHcD4f(K{ENOe$J0HJ#urqwE!{iYCcrgQT6kUmRQ&pZsx(U*x5m938GK3cceA-25P7 z?4_>Rtm;@LOJc>-Es0d2lZed7(#_R8eGm|eZ(xhjbvF{TQvs1jaS#K%R>_hqN0n}TZ* zkc089?X9=$pO*FdJ8a~1LwKU&Tl*+PUpFFBdK=aX&m5jxjDg5G1pXXNL&FXtQoDIi z%I2VE+_J15PN$4XB^X2Yje8=^qT3Q6Up)7auJ|SXIn8t2lJM#_5ql$SZ|nXfb&U<5 z+WD;cxsrkAy@tew0gl8PHWX0(qf>97u#=sJz7BD=`gp*W%GmlPa|+rCER@9rjcWg_ zl26OYrAyJyc>(x*jhp9DekXff;UF2NN;Ui}MJ?5ICzv@f9ALbJ?E#ZUr9Ic3 zzA*o$&I=Ta@JfZOEAMmeNUz9k93p!8X=>FBD$#aW*rJBSOJG_{E4u;M3A)vn3ZA*FCGn+Fg(4w7}cEUuvHYjNe3srT? zjGbTt%LY~=@?&|zrxYJ%v<6_xj4<+!VwleU+BF+z4)}b&?KFik zy?KZ%qJSTxm)WSC(-)vC z_LTIFihr!^y%i5PBEEPCOyW1(0O<=Ad}++TAQlUVUet+p^E3c}!Hm6Ker0kttjBIWHFAYVE28@r68QPb>)Vg<;d0ndg zIOg|&%Z^&B5koUj%;;F55>#Cd>y`X1^41GHDSIjVmR%4uBt$XKaBh6+p3un1m6DKK zM5nC$KuQFHa!O+A!tnBN$&WmSvCPz#nQaEXC!g(?sW+Y@AB1kdg2dM^(Gjmzs6*J zi>IYc&r4tXJ{{+;xx*UGux7GmUyf}GKo{&yc+i^CQk+fM5xwnR=XN< z!u~>Gl{|8NtTsKC_us}+!JbSFv?wd*)?I^VPt2vT`c;a6orPS2Qhe`>N1KB~dB}yP zspLQzZ>`?Hbq-7qJC#l@Vh{gOd0-=i*!QkM8LpL1X8-}g1mS#mh6v^#lwH+V0EAht zLRoZn@;eAS)m=80s0Jn#+sLq@zuIq|XFXByZxLIoN4=#LqQuVVkJJJoqdv}YdIi8` za&=Ppx)n$aP&MKW_^PY6l=m-iPXIGakyd*1%=})EsxHySwRk^AE?qcrR8hTjF`nFh z)+UT>wL0VXkVCY=24X|7B}!a=Gf)c2+1jXZ;lwogP%J5l_LHb4lWDj;(dv}Vr1IJ% zBzmFhafX~i#<1bqv&puIYKuHOPY|K%X&v{<{=yTL{$8uDcy(HHi}VDVjHC}Z7W0`b zEvA9p60jBWkkB5Rk#%5BJPS(P7jy(H&ZM=!PzvrzF1=cb@j0B{!WqXMl>4hvAUG#n zJd@sf-hvm66(tgSb~I9O>_*OH9ggr<9(jkPzpUP5U;9oi{-`RXFkT6&7UzshGl7YK z=w!GA{fajfE6<@$!92K|Md|hQp!i-X2J~nt=D;7#M2;}9l3LG<6`3C2w+L(}Swn*C-B*?`-k7j87(HI0e zOg>|2NSSo0G$Db|yJ=}l3XfUHc3P)1NIM4OhMgn9utTLY8mQE#BnS7N{&WXwxbPTC zj>^Vmu=6JO$5zNwB5NNSl0w;}jb@J-VA6wNi{X~PSBBYYx)&mpWiwGyMd~%>340*O<^m+;13xv+nsl@@4vWer8?fJpf?QLDsIAYG$AW; zLaEVbXdlU68j5l)of@<#27i#8e9acN)RqV5SD02bMKnOYW!RB{72(fvCCTBSVi?ru zbgDA#*GRW68N(c0E>5u>u(SP<+gV#x)7`Bp@SBKiVu<5JAQnY_TkLETuOirHXdSvS zvj3FIepQF6dAlF4aI!UHW_6)6yAM7CrBvn^#Qb^(|KMPUas1SycQijlWVnLIlvayxabGnXVuaQ^dHa@y9)=$QZH>SPegN=OO*~ zE)SFDbmX`%K>u)QKvO4)0Q6_1yp?lfgooarhtt<$z~YTO+(JVl(~ASc`owLsRkis`U_?MIJW!nR@Mo{TY+o9Pv7gjq0Br6 z69CC^k3Y>byZiTYSu$_l7lJPB2#srl$j1$McL;9;1JwOOnTj&h4}mWH-Vn?pBA#s3 zjm-omv~5W85u0g%GVKXOn)WQaVM*sXOrslhX;tKH6?3k};k`m#5;f?oYG{A|jfzVI zEawoElA5$S+%=j>B{ljl6OB6dMOtiz$z|zws<7A7tg64qMADNf&^>0E_v(v4Xo_qH zV^U-nQmvG1&4lmI`ITySApjtTHJlbWG-M3T*jAxeFp8eXd~QuT_;Rtxq6gbbb-=tw zoQ(PY91W&wSS2@?%S!N+c&XI*-Qe>8h;>EoRGL|8iL5JVmPFo`8mCcY@G7$%vVy7X z7@ReiXO;L?;tk6Mm3?VrP%a+9@9N45(_m|XD$^pZCLI=|=N&b3Eye{UTf~qseLt&P z!#sl$Vu>mfVC$4UM*S1iA&A8WT0&j2yWtx^d_y<4cNyNemon|ChjXI5IDRb_6+)L6 zHL>y7N+Zt&p4YiL#W9q4j^;U#_Uo|iALm532s#R|g|RtF1ga%u9(|3q*VEV07-Y_# z={jfTg|b)%84CRox5B4Px#rve>wV`e>F+Ihvw2o<_Q-Nv6Oskz6Xf0(P5Qe*HQ7l- zcH%D^p0}1DkU?Oh5Luxsh!wO zKUM!6-)%F>W(*eN%I<=x(m0rDftloG$@?ufi_0FJPvZ3#aSQ)qBP??BlZ)n3kR!u( ztnUxe)+T0*JsBGnx*NQaQ*rbN@u7$&a*QhLA>#~Ru<77+YbIJviqYiex1fq>1{FT# zFdi=DsQwOIHD+foydCEv&;U6m{f)}zJS3hga=b91my!N=YxAFN>}t3rbzl6j(22F3 zN=wsJ^$u!O$eS~g%{1`E%Z4(MfN(74t3fvCmpBFL^Zwb}W|;;%1`>f&|3*$y)Z>cJ zb4L4u3{QiD>q8`;X78t!poKbPNQ3F!N5@gjzIaM@VHUUjjLWq@kvi9sqbqS?nXGE8 z#+GiOoSb3agPl)kT>OYk63q+oSkS>R1&~Kn8mWrR@Ghg2kK(O=B0gr7cqQS&ZU#=n z!fuWk@yB<^!ZQXKgv|$6V&t7P%_Pw;Z6eX>n7u0VO2tT?Md1A_{XTzc4f!^fy@J`@ zL_xHu4pQ2%+0gi2MYpK?iQ^gAY+ZY~Gl4zpRA+4JCqhte=){_!sS#6~-(u2O33{G&qyu-3N|Q&_I& zrYu8ewgXs?(VGq;pSXyDqUfrqm8MV7=*kn-gajV?A&2rCKCU2b%V#8DjIS?*Vby zKbhSHwl(aey@M#B8n8X&2S?C9fc+T=k|2m>1p1jE^8a*p7GPC1+y5t}yFEv0biZjerCkVf)}=vc*AQeLaes5@b#F77Z6qAz%l-99zN7!krPb@WE@*haV*6;&%ac`t z$p+!J!?T5Q(0fA5a}OU8+PZ!Ndhf30kT((m^9FiJ79WS^vcFZ6gGuSj{S`e2Q%u8$ z*$=`FNUwnT3MQXg2wm@iypIy_wtTRvyLm345nt~Hjh{W&yk9bNXi)x$TYOmqRkBjR z62UrkX=#b5CsQ=dI{nd9hLOmmydWim_?39xb1J`JjsCP(>wNM~^8+bwt(VJK^`0=s z%97EYPT=bjs((ZFX-|N_y>DS zvWRyIuDcghz}MpyZE#*nQw|a4uW0zgqtA>*CLBdpjUhRD`mJFRa&;l=cRkT3S(l<+ zO8=_HSCLh~y|ftK(ajUECd|EE=Wy?Hb%c%#nHYPZLw9akcR7u!w5#-PioD>8RhE)< zt{&UjCzWN|o#^vd8j;6KXf=4}kMkCW| zVSxvE=u0vh*r$0-S(9P7Q5CW%^7bKVu=| zk>ZOJ}2*@xw z%?i%k;pi|RUQ44_+hrd+)y{B|7lfBZp}F!E)I)8)h6ld30f2zQD zTA+dMr02cDX+vCzfK9iwIK=x(6Jyzg^uR7;c;;@nWi3y`O@AqwhJ>;X- zN7gfZGgG5gwbGh~E(12E`qln~DWZnEFRDh%yxmP)2=<8>_4(`U0+5>T-4EU{^0T?< z`+eP>KTJFH+2mikxF_l^Z@%c<4BZl2RS?NPZ1r~7eLM)%xk}0y=Acd)Cm(z~Xvwb0 zQk7zx^wnc%U@M7vM_a$zg(1pPLqISuKU(`;+GHB;XjQ`ED5yW)tP!0z#M2FKs+Ds` z@d($Yzm}Bw#6VTT%Ge5*n?cNZ-1wB^I44Q442Ll-=xb?uqN`n``RUrAJG2xmJW}#I zW1SCEJv%R%*ur!4a{!F-lTBUWI$4=GO;;xgrKZ*Jp3sa<>ilJ{rnNT~(~B#*XEmiU z1~Ed`QBgYpk>YsHbLx#%E)o9--i+ZC9f^_7T3q*re!~_iq1d4WhP8%?V(#=QM(g^7 z>2+F74STNRx~BuypUTi!+)M{gS@jyMH($ZDu zKjsY7wy_tY=^3B$W08}!&<@2c!l~K6&#D)VB-K$kGlCyqCHZOrNP@szFIP8$SAP6l zAIjazY5FRXfEyma)Kg?SYc6gqIrvj&$otnW`!RzBpQi4fq)s=P5CdQP@)yndY7bUH zan{vp_Qu7}wY$KTn$j1%Y@h6=n?MZNqDJhm%WboRANR6CQby3{gRzTJfUkwKimRra z>v20v{=}dJ`%D)e01bVn*OnnAnvxkDMidvnnJEF&DTbM&P+`Ujq+6c9syhcdm!joG z*1W2nVX)Y4=7jc_kF3u24hP6*6e_ugdd-Zx2G;^;ugxy^C3B;tZE{9i)S#}n+Tm^Wl z^%KpO#g^>$))G%Ak1-6LUD#ZTRTn(7!9<4(>I$Q9zeW_j9T{_T6J6i{a*yI=rhgd@ z)gG{9+1{|l$zFGeY|`t&%G=$#LakN(kclKjR)UF-Ix%+c&+>+~j$d4Qmb}LruYMO@ z`qpSxlDi`75!wy{eqU`gG<%ZOL3iz#AK@!h!=>|j1B+Oe$GKu9eUZ!k_(1T+S7_kA zbJn;fO_sAts`Puo#$t6E;ze2?q_a>$w#+0nuk}*bYY8_IQmYk^aF^PtEnm9%vS?g- zl=f(*i$v;};DFLu)Ie}{;wBfYcRZ;#gqu}?q$J)G2lLswTD<(sxB!k1pp9in$Y8=k z^3JyAcETT9MmAB~bYMX>W~mpKeS-AdzQ{3eH)NL0Fva9G(r77Eq^5@T^jqfFHlZW6 zX`)orA@BS6J(?KBp+#ABTs)dY-6)A)m=B$=fl;)gp0w5h=kVgFEy%>zT==t#)Oswq zTr?{tmWGWFbDOksn&?;8ZO@~z1|4maoHqnx;)hZai1Oa97qKZ2`=>=Tqbi7E&k^Na zZ{=(CC~B6eo5t-^lBcfd9J7-)zKvBA>K}~;QMU(%+w1B)Tm0HTIfLh#lU;3Yn~+}d zUP0S|jo8kZ7+vu!d=$BZlVeRdZn#XTYejHx3KQ;O9%HU#dW(r^FcXBZC(y~Sm~%N} z2AJNk$S5a5XzSgPM7Rj`gO_&{#IQ+BaJI7%Cg(lRcrdBsB{DM zT8d*WSa9l7$|3s+xddzetVv2FvHpTmi>HO0ST5olCxQvl(GCf3Q9y&j7i|TuS52RC z$Mq$-RNqf4At8+FuTKP}#H=tDX#`r?5dsa5dEA@$R5+ZaAl)jTIpWtmtDot`nN#*n zhU~NvwXJ2@?Ng4=Ga)ngqKekQp9>riEd9DzgA}4BUwqIm0%Wss9jHUl$nKYqO;2N7 zknpSn9IQrcJR>i>8i4TbCiE{yOjELbLUDeF)~y3Xq^W(@CXkZSMd`R;HHADm=DLkJ zS;1I$?g$Acj(p>KT3D?`z_4LUo}Uvij?k=_H9S~+>bx^)AG{@fB`}K$xi6WJ!FPJGW zB~LoXg!SC`+S#|tF_WQeoMF^8u?W?f)9v=3VwpXM#@dD`br&6k3%WzaC(pjfR0`fM zChRRAn~rhB-s|T5e1XI1$7!j+-kyB4Yw?uPR@@9KfpTk%nATjRS13yeX_R>U?NRR* zYr(<$9=%ADVmjc*1V?@FRwNrtIjAjb6~xw zC-sWFLtc2tkj`HGvT-)9R$lY{zLj=HPa%BG;Eej@!{!SgZ7uQSkiTpuyam5P z5rGi-YQWO|GMX=FapkU`5NRBgpyZCbC47f9)TZ5%PIz1ivCfeoh~;Vbi@p|Pw7gM> zwb+um?aH84>hd{#m`B&9Hw?kAeS3;L=R7r;t*zfqC&7JCTJ}UUynqaE9fG)Oeo+9~ z<)#K&_ox+Nw&lB+9i|2E!p?w#If|`6#-*70{+ZT9cyNps75*mHJhbjb(M$RiL#Im7 zkt@=c&>5xhMt!=^u@mJ>AD$D_6u+1VyRkNNNm4B-5;&h9$MT0M8s71AN$h*tvfb!k&(H`x-=+RpQI>om@b>eBy%{M}3KN2#u_7ZsoV&Xy#uDxoRl2 zhZ9oKR?*q};PbY(m7gWgt{z{7YV^%w zc`Y^X^W2*`zFzR@pZ`FAYXD7ajJxrE>}I9XGO?tURZlH3Izhh)mjN#;L|i9=q<*Nz zeJ$l3es%o;Vkm2YSg0p_sEJfD;4905eJ~)3KL*>sr?_0fwyGKtmV*Mx?gOY(=^nPy z75*rmkv2($3TAtHYhv>G)jB4hBOwj?+DEI7B7nKguhhz2Yd1 z5R{LN%C|hj+rB0#%?eMKUp2KkGARiM^w%6HC3B_ajcD)SC*>BKm^LzSenJ0Ao&OwF zP*SjP9n;qLfKIW#zSsN6#KjQ=N9BF<<&EVWEqo{0Wy95oba_&mA2}DQZ?GFIAE4+$ zTSWyjBPuJ{I>+2{`XjGQUK|-8z?*tIei@>sC0eceal?yJ)H4CGLcpm&tzj$W8yN`# zWW`Z58t<@KB$*M=mUB3S1Ewuu;KvZt)Q44I^sc9(<6KD zz8jzDcL^6W2q>?&+~@GAhGm!bSVyKo4FcZIG@w+Qpt=z*Ug35;iTEV_r3KuuIY@AP z86i%AyiC(GJ?msLDzV2q&uEWf<036blx`(bK34rhL@TD$CD~KAPmc@j?tv4i(U$`9 zcWk#E6!Y?LEsmMJ0&nlU1XdZxd)a(3uMfNLXuUp;?^_>tzV(jaTa$0?-?6+ps6I8M z^B+WMTXsb|tcon?N_dCOn5B9n=!X7x%?0 zTWoPArre~5nAqwvGIZK;G@h1ctA0q9aR>+@?}8?$AnXuMICs=!+GRwXA9E?Tb*cs~c2&|aJbq|eJ7f#q| zoxW$gW$NCNCCs5dI)Z^%IkU1tA%66_qyJRWe0$h5=C+eor|YD9VtX=mo9i~)qd6;iM;BM3`Er9%Vbh*xkQP$9s^g?<6<&loxpnjh84ZhlM9LxMJBc zLXJ0K3!L}(&LVO@gM{JDV-#1QVN~`dv!T2 z2Qn;Li&$}sd(ekuw=gm4*!C?zfH%!{5U? zO_#Y7qV!K-j*(lr3xK97+d&CUgC{~Jh<6M)O$r&FwN{1 z20nbi=4jRBh^n!*wjSy8azByNjBI_hrIYM>2DjX@lKe#Cjb~HNQHwH_8rD&4I!0l; z_yD1aD4HlIRpaTe{;-Dp(o62$P92GK;Vp2_eF?x?niw86wX|gzR^&6S9>(;XlZu!P zg%R|xezBab&$a_p^tvy_W@JtUC?XN}cgE^{$r@Jj0O-eGw1y~*_g%tgOnARkghNuL z-{~{vK;QbpL8{T(kM6bO^)h}ux~es@-LTd;R=9)sxy<}5O;v>vrHj%91Z$l;<`Y(w zbdlOcHl_DeY2!3@#q;ILT9*;B7%PjE-TI@nj;lVk>o~L@x38XcbQ>sb4Q_ergjle2 z=1TP)RfEaI9>j4(%Pj#eMlOU;E^SAsx1HlY$8Ha+YL5x9-9of5SP~`Q!TTkHjuEe( z^@Be9fgW2rMRKH_{6?-ncAL`peXi#-uUai?&<79D<|qcq#{*VhfR0^Bu#$m}waU-a zf?oVYeZ&@3KR+@Wsj@7H(vYJuPF8)?g;g1qgAbPp;Ih|4hUftITYkRimR-QPGaWd7JcGhKSRpMGT&ZPF3KZi+UYK+VsaLymr zv>(Eeqzvw$N+M$wu# z>3e49=_k#bazg|41_rGVT0nT<(dcOP7(s1Ur0>eqr0e92dZHT8*{A<=?8f_)wMpo0 z{|aanXhtrN0z4$6y^uuRVHQ*`pV$MvaOW$EvoxJGG@+{pg z{B(^TDMUY~v>>L4)O#sr#wBegOIOE&*2iEbQW`BhEFF0u>@prRi!1xGtL|1g#KAS$ z2z`cSn6L;ja0_%*HV*2mK3AE;kjTw^YqTooD;21_$*D_&YbZt7kr0YIgDiIM+h3av zgXsG{{f0}-p6NrnC_K3|jZ}V2#|Q~}&q&yQGGhGuzGQpOxN92O13je4X(I|k==cr~ z){SHv(u91WcbB0wZRt+%i7bMlv;!;=?yyQRrb<4vGj{OKNm9nxng!4NsvZZwIjObb z@KC~nsdPY69@6BqZ5_xo2)t2U7f?&S-~;ZL?M-P+2NvUqJyv1rd0k&{^ggm|X#DvU zA1-EY8=0$XfC4GdfipYcF7$esav-K`gw%(SpA#*Orbj6niv@8kHC8^~J1)}`9(X#r zWe+dN@#5LahIxdUkkOvtdVCuX)hsK*ev-=yc~?~I&5QnUdA&FOi2aQH#JHqpMANea zI;p)iNmoZdlH(Y%N7`Q z$tJQ{7&y_+s7g)E&Jh({721M{ps2~O(9SBcraCmcZ0}dc5$rEJ!v9Pbl&6ubxH@S& ztYob|2_`2;c^Oa>H*AXv!H4p7jIMDi7;0~m>)a$fmh^tqSUKkGutJV0J%@winXVE} z1%Efz)uZZ}4@jH2eb^k(9K)`8{RrURx2bPm4BcAoetOQG1Yd9lGtN|#HSUjX16N>h zgp&z_RHqL2#CB%Ab+D{k$HbPfS>)o3Tge}(!1u2$?BrpEgXExq>_cGo??dcNzwR(V z`2az=)m9(}T9VsMQ)TcvTmoO*co=y?Ehmv68vM8`XAYc}We zjk&~={oCs$W&`ksP}g8;6e0#Qzfi1(I;sI<8?wAN#=S{q>b48Z8FtBqMe3Lo?t!EY z^itX@b~44Vwu5KIb~f1^NSYKTZoKLnZZe6uiSTR9JbuYG=>r+hd$|$O8?Z9?6eW!k zTvcHux%(;faiU}^r84lESQ4bMI=%MtQE>xOs(mCe>RrTGIvDfQnE0D5LQjK%wz@pq z{80dAMVzvl{BgUGwK)lIPb$1`LijJNSCwa+)WkhJcWqqlj9V`-C$fYU5EheRA zYafq_r_hB0^C}Z2UoB0XSs!8%AUq)yVUO) zwX6RI_&)zfJ?O}QN})B zszeLFN+26+QHH@RthaWS#8B>Gj$1KjY3qnj(efg95O48)}Hn;x28!H&jZ`_1+LeOo1{$L zw1a-o%V@mzgD3f2q79xeeEC1aKOyC7B61gS*S?_Zh`&^p>&?}@RO{q0!(DW^ec6;M zYT#36iu`t^u4YK394UnkPHrG6(vS#2#W7^a)DseTl(SK{_mRx$SSO(;R_bGn<;tZ{ z)`77$`ig8YMyqtHF!Oe^VW=Tk_L10)5Fg6Lmp5r4<(4)Vuimrx8er5B(n2pC(7r5? z#p<4o`2yc+!ZWADaFv&@35Yi_ve!%T@*JOz%$|SD0Vg&dWx_ie8OD<1#3l8(_F|Jo zCmXF1Uv%5xfF-Fk3?4k)4sbvl&!T!idJn0sbY#s!A+COh21I8hGu6fXK(MHhwc<^7 zjk#}tUy&wBpV8PzVY|f#+K#Y!YbCTm*g~AP zgs!E>RURoH8CYZ1E6;(H%K|7or+2N9^-bbqr-9b9nv)Xdd--LXSApu89O>+r&{j(e zsoCK3=YM5>U@;s1%m%t8n8Ez6Tl$-szkla^0A(mQvov>gGWtbU4d3`(1<+GX_por* zJEnKK!ZAfXWakj?oanK>w98Y9u$CH^O}GD3ny%d#s%lo*wAAtBn7P_V4@?f6B`EFdP27|nUbv{J6fxz z&di#|ozz#*%c7NKR-|Rr$zJ`G^W7UZb$KrG$#u0iQ!4Pom1;dBDrR`K5>p%fuIim| z)uO7-JkL@}EF$p2sMc%(@TkgyPCk7K`eakofj`y_h6>Tv{FFOv?|n8K1nWY~c$J7O zo$OnJ8VwVPt8`m#*V2+6*PL2&p-b36MazIZ^`hSGmUdct9ltF~lGm8yY_CPrcVPqF zbm=0sw{Pc%=v4NPkOWx#dk#Lxd4?Z0s9pr?U_k))RlmZg8}zO3szcme$P5m32;ToK?74f|_(j%4_CBhdvdOZ zAAS*wBz1AnzmDxfU@^OsTn#5a;%Jrku_al3e{

1bvi{DS7E@q1{$_8->K{_OWv2 zCZTgG2Pr3n8|ec9kIu&uC|d?k4-cQ4#}Z`qDX5Y2mhC(jR1Ms;UG4Ho$DE|+SeJ@{ zJQQhAXj|<)*t3KiOWTuh{Wd^mS{u{&ERV)OpZwiQ%#1->r9p zSK_^*U~=?ywH~4IUxb}{0J!SmL!z2Tzq_PpetoC^_az1JFg0=gMcQADuOP%3=H1hH zH_=dG(PD;d*037Ov5G1924U#Zns?~fs+eh1%-bWqa%ssm3=nio1r3J<4G0IBETtr? zycs~0JIOn;MecYG=~OQsYHIrf?~A5>_ob%8+uOrVA+VCJw}{lygrBBdY1k<8B^wf6 zl|<%N$7)fOZX$%y>4ueco_Gb1H@B%XrKVwrn6hUOecnc^PU0rFuCB5=*2;|u-`o(@ zL*tr4bnQzXYLc4XqFbv5sK0}A)`}`8iM8ehtj#Oc5DrE;0VxbPmL@BUa_BQwa$EW~sU#-LP0?sGmqfUGhGWcciGZ*4(}u3z=@b>Ow9DQe7lcO3K}BG3j(t& zH10>sK!&4Q5-=gN@Nxj6{|*nuyqw7KZJ1?p)NUJ?U0bOigGdsOk}Iz&9PmN_5=W*Z9M zy^pA`&dX0oo6?CSuhE~(pYbLuTPp1a1Fa@e3Lu&mmgd$;D}&g-i=D-{sv?J9kIr9r zrX&Z)aFGK^kNY{LxrotP0}k*;uN12i_2a_JJhKwh zBt{D-JRxC$8U+-`u1xD>gJ^H4lbW;7spI-=H506i=ncdK;xq*L6f7jVz$XGMg5aQk zHRJY&$@g}i_SP##iC?lR?ltnWUTT-UDlq(*BTQaYNkg zNG#sNoo{WmP+Vl}U~?+T?g25b$E-7iwhu=VVgw3JdFXm~ba+LC4p>CP3~rNTiNBl7 zL{RfLLepNPEtZj}yL_#R{(^MqIlG)c0Va}>U|9Pl&B_3tV;Ps{r)WqBznD7FcTlP4 z`JQe2DvGhmeeHGGX39zGyOOxZ3tq~Dft(BQ;mDXwwJi?sBtxo$Gf1SS2w*eQ0p&RVMNVi@d zY8v4J0(n}%6*Rw(g~l@sUuxpiJ*Y}7TzBQyU+>-qWm*InUeGt@)T9g^0J#z4){Lw* zT;69if~U9DXBR9fgVPlYy7aDhJU)gDC?_GHQtwa6QXNaah7-CzA|Fx-lH7d@N9>38 zX(F&fd3w7AkZ+ha8-gKfX%@_~<#HDs?kBg5zW>V3%Xw5jwPs6uni{7r zd`EfPYrA*SU;xDtm@E>5TrJKlg5o=h;NSXk)pt4K)GbpP0xkUg>2o|oG=`UnX7^Un zb&@8d6Fj1cBWW^c(K#Csc8xEBa4KfHY>8Lp^77-lhzgWr9kR9_p+g|-9r?VSv?qA%^1O;cqgke)%AqHlR$B{!Y1Mq zj|)Ecg?{_!>kGDAwGa7%cwSUb{BcayJihkv$}ql+yu=O}jVvAFdC{Hjh$4}u+$mx% z5V$sUiGCX%D3A>bKwY8HR)Gv*lisI4q^3vJ*nDwj|mtr!0r!~+Qoe2cw^jPCXkT7tI*01|w@ z&gPC`?O1w7hQ%=&bcHi7(fqhY3${~JepA7y@^aLwHpew^Yk$;R4v{ASHjXjXtaTc_ zuz5*nXB&PrcyWx#gQ%?HyxawmS+Wu(7ssvB1UMh!1$to&o(mv_f=9~!9@VsJCGxpu z`>g5Sp=xDhpsiCy^y>=fI0DON$&pb7o7^d{@@&hj3!6PUd=vA;G;#7&8ChamsE{`^ zY8pDra8Jntp62Ivi)Y`*XbpM60s06v@Rz^-g)TW_F@B!~y7!4AJ>37mAuz!(!C+xQ zSR61?u!{N|qHWOeR%$RXRL~vpN0SGri7-klNHEJuivbi=0qSbdV4&ghf4i|7?$>z( zI{qH?i}`~a7GyB6|8pZRq982+P*r1+m-t&(%U5#ZWFQd-(CXKLHeN@y(c z;wqq1hzE@q1b$GG0VQ_)`{MeylBlVfy%UHR=;Z98>T3M&;{0i?+0T-Bck?I)AUQrz zeF**_iGu$JlCpLnFv`D9?q6R51jKPM{Rd6!0FF#KP=O|b3iQX*TqXSjO?gXaXAmLr zU#g&%@+XpjVArlGkfaPKk^PUSnMLsjlK<9nH*zxl^V2-jGC$4+HGE%?F3%4|y9>HN z|FJgz*HW$VwU8$RNtuBf(2vdZhW3x;R6%eoJM(|2zvKebxCh$s5J-*fhZ75B_yeUs zFTrToFiB^SNH?gV2>l?G&h!UD>UP%uKh1L;Er59!q&NoZRe$VEf?5Ar^&iUad&2gQ z&WE`E%lTg=_3XQT@gJOjkAi-Hbbqrl{(pA<>_GH4O8+xI^=IAhS#v+$vmgOK=>C!~_xFg-pLM>6kUfy=zL|u~KkNJ< z$L?p*?;%(Ze6w%%M(zjE|4dH&5$)_}mG3z{KUQ6s!Y@_+kInPH;kAC&{T^5HKmqz@ z@+!aA{YNIy&r;uKTz=r6e6v>d-%9<%_4R!+-iN^8H#0N(rQbiu-u&}-|2`q@k1agM zdHkW_1&%VDD_|I;NpK*OZfAjAb z`Ttl8km0{|{F`kWKWltH$^Ech;G2y`{7&N^%H;d0$cGv7Z^oJNOSiwAFaP<=em}wX z<8AA6<}bbeZc_7S=ii6PALi)3nOXL)o&Uj%-OnQ52M&L%(%ZaWiu^(R{b!Bu2WJl< h$Zw`p^gE5e2}ml*LW4$nU|{5+pXG<~Ugg7I{||-5t(pJ; literal 0 HcmV?d00001 diff --git a/shared/gradle/wrapper/gradle-wrapper.properties b/shared/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c61a118 --- /dev/null +++ b/shared/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/shared/gradlew b/shared/gradlew new file mode 100755 index 0000000..4ee5297 --- /dev/null +++ b/shared/gradlew @@ -0,0 +1,271 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +if [ -z "$JAVA_HOME" ] && [ "$darwin" = true ] ; then + if [ -x /usr/libexec/java_home ] ; then + JAVA_HOME=$( /usr/libexec/java_home 2>/dev/null || true ) + fi + + if [ -z "$JAVA_HOME" ] ; then + if command -v brew >/dev/null 2>&1 ; then + BREW_OPENJDK_PREFIX=$( brew --prefix openjdk 2>/dev/null || true ) + if [ -n "$BREW_OPENJDK_PREFIX" ] && [ -x "$BREW_OPENJDK_PREFIX/bin/java" ] ; then + JAVA_HOME=$BREW_OPENJDK_PREFIX + fi + fi + fi + + if [ -z "$JAVA_HOME" ] && [ -x /opt/homebrew/opt/openjdk/bin/java ] ; then + JAVA_HOME=/opt/homebrew/opt/openjdk + fi + + if [ -n "$JAVA_HOME" ] ; then + export JAVA_HOME + fi +fi + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/shared/gradlew.bat b/shared/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/shared/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/shared/settings.gradle.kts b/shared/settings.gradle.kts new file mode 100644 index 0000000..392419f --- /dev/null +++ b/shared/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "pindrop-shared" + +include(":core") +include(":feature-transcription") From 6c0cd8d42d3925909cf535ba238cf3895ec4de2c Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 15:19:24 -0600 Subject: [PATCH 2/9] Document Kotlin Multiplatform rewrite in README - Update project overview to reflect shared KMP core - Clarify macOS remains the shipped native app with platform-specific transcription adapters - Note planned Windows and Linux targets --- README.md | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a06188d..5599d80 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # Pindrop 🎤 -> The only 100% open source, truly Mac-native AI dictation app +> Open source native dictation with a Kotlin Multiplatform core and native transcription adapters [![GitHub stars](https://img.shields.io/github/stars/watzon/pindrop?style=flat-square)](https://github.com/watzon/pindrop/stargazers) [![GitHub license](https://img.shields.io/github/license/watzon/pindrop?style=flat-square)](LICENSE) [![macOS](https://img.shields.io/badge/macOS-14.0+-blue?style=flat-square&logo=apple)](https://www.apple.com/macos/) +[![Kotlin Multiplatform](https://img.shields.io/badge/Kotlin%20Multiplatform-enabled-7F52FF?style=flat-square&logo=kotlin)](https://kotlinlang.org/docs/multiplatform.html) [![Swift](https://img.shields.io/badge/Swift-5.9+-orange?style=flat-square&logo=swift)](https://swift.org/) ![Pindrop Screenshot](assets/images/screenshot.png) -**Pindrop** is a menu bar dictation app for macOS that turns your speech into text—completely offline, completely private. Built with pure Swift/SwiftUI and powered by WhisperKit for optimal Apple Silicon performance. +**Pindrop** is a menu bar dictation app for macOS that turns your speech into text completely offline and privately. Today, the shipped app remains fully macOS-native with SwiftUI, AppKit, WhisperKit, and other native adapters for transcription. Under the hood, Pindrop now also has a Kotlin Multiplatform shared core for cross-platform domain logic, which lays the groundwork for future Windows and Linux support without giving up native transcription backends on each platform. **[Download Latest Release](https://github.com/watzon/pindrop/releases)** · **[Documentation](#documentation)** · **[Contributing](#contributing)** · **[Community](#community)** @@ -19,26 +20,27 @@ While other dictation apps compromise on privacy, performance, or platform fidelity, Pindrop is designed specifically for Mac users who refuse to compromise. -| Pillar | What It Means | -| ------------------------------ | -------------------------------------------------------------------------- | -| 🍎 **Mac-Native** | Pure Swift/SwiftUI—not a web wrapper. Feels like Apple built it. | -| 🔒 **Privacy-First** | 100% local transcription. Your voice never leaves your Mac. | -| ⚡ **Apple Silicon Optimized** | WhisperKit + Core ML = 2-3x faster than generic Whisper on M-series chips. | -| 🏆 **100% Open Source** | No freemium tiers, no "Pro" features, no lock-in. Ever. | +| Pillar | What It Means | +| ------------------------------- | ----------------------------------------------------------------------------- | +| 🍎 **Mac-Native** | Pure Swift/SwiftUI—not a web wrapper. Feels like Apple built it. | +| 🔒 **Privacy-First** | 100% local transcription. Your voice never leaves your Mac. | +| ⚡ **Apple Silicon Optimized** | WhisperKit + Core ML = 2-3x faster than generic Whisper on M-series chips. | +| 🏆 **100% Open Source** | No freemium tiers, no "Pro" features, no lock-in. Ever. | +| 🌍 **Cross-Platform Foundation** | Shared Kotlin Multiplatform logic today, native Windows/Linux adapters later. | --- ## Comparison -| Feature | Pindrop | Handy | OpenWhispr | -| ------------------- | -------------------------- | --------------------- | ------------------------------ | -| **Platform** | macOS only | Windows, macOS, Linux | Windows, macOS, Linux | -| **Framework** | Swift/SwiftUI (native) | Tauri (Rust + Web) | Tauri (Rust + Web) | -| **ML Engine** | WhisperKit (Apple Core ML) | Generic Whisper | Generic Whisper | -| **Apple Silicon** | Native optimization | Emulated | Emulated | -| **Source Code** | 100% open source | 100% open source | Freemium (paid "Lazy Edition") | -| **Battery Impact** | Minimal (native) | Higher (web runtime) | Higher (web runtime) | -| **Menu Bar Design** | First-class native | Web-based UI | Web-based UI | +| Feature | Pindrop | Handy | OpenWhispr | +| ------------------- | ------------------------------------ | --------------------- | ------------------------------ | +| **Platform** | macOS today; Windows/Linux planned | Windows, macOS, Linux | Windows, macOS, Linux | +| **Framework** | Swift/SwiftUI + Kotlin Multiplatform | Tauri (Rust + Web) | Tauri (Rust + Web) | +| **ML Engine** | WhisperKit (Apple Core ML) | Generic Whisper | Generic Whisper | +| **Apple Silicon** | Native optimization | Emulated | Emulated | +| **Source Code** | 100% open source | 100% open source | Freemium (paid "Lazy Edition") | +| **Battery Impact** | Minimal (native) | Higher (web runtime) | Higher (web runtime) | +| **Menu Bar Design** | First-class native | Web-based UI | Web-based UI | **The bottom line:** If you want the best dictation experience on a Mac—maximum speed, minimal battery drain, and true native feel—Pindrop is the only choice. @@ -48,6 +50,8 @@ While other dictation apps compromise on privacy, performance, or platform fidel - **100% Local Transcription** — Runs entirely on your Mac using OpenAI's Whisper model via WhisperKit. Your voice never leaves your computer. - **Multiple Transcription Engines** — Choose between WhisperKit (Core ML optimized) and Parakeet, with streaming transcription support for real-time results. +- **Cross-Platform Core** — Shared transcription policies, model selection logic, and workflow state now live in Kotlin Multiplatform so future native Windows and Linux apps can reuse them. +- **Native Transcription Adapters** — Transcription execution stays platform-native. macOS continues to use native engines like WhisperKit and Parakeet, and future platforms will provide their own native adapters. - **Global Hotkeys** — Toggle mode (press to start, press to stop) or push-to-talk. Works from anywhere in macOS. - **Smart Output** — Text is automatically copied to your clipboard and optionally inserted directly at your cursor. - **Notes System** — Full note-taking with pinning, tagging, and AI-powered title generation. Organize and revisit your transcriptions as structured notes. @@ -65,17 +69,25 @@ While other dictation apps compromise on privacy, performance, or platform fidel - **[Swift](https://swift.org/)** — Apple's modern, fast, and safe programming language - **[SwiftUI](https://developer.apple.com/swiftui/)** — Declarative UI framework for truly native Mac apps +- **[Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html)** — Shared cross-platform domain logic and transcription orchestration policies - **[WhisperKit](https://www.argmaxinc.com/whisperkit)** — High-performance Core ML implementation of OpenAI Whisper by Argmax, Inc. - **[SwiftData](https://developer.apple.com/documentation/swiftdata)** — Modern data persistence framework -- **Just one external dependency** — WhisperKit. Everything else is Apple's first-party frameworks. +- **Native transcription adapters** — Platform-specific execution layers around local engines such as WhisperKit and Parakeet on macOS ## Requirements +Current app target: + - **macOS 14.0 (Sonoma) or later** - **Apple Silicon (M1/M2/M3/M4)** recommended for optimal performance - **Microphone access** (required for recording) - **Accessibility permission** (optional, enables direct text insertion; clipboard works without it) +Future targets: + +- **Windows** planned +- **Linux** planned + ## Installation Pindrop releases are now signed with the project's Apple Developer identity. After the app is notarized and stapled, macOS should open it normally: @@ -107,6 +119,8 @@ Since this is an open-source project, you can also build it yourself. Don't worr - `scripts/`: build and release helper scripts - `assets/`: images and other repository assets +The current shipping app is still the native macOS target in `Pindrop/`. The `shared/` workspace contains cross-platform logic that future native Windows and Linux clients can reuse while keeping their own platform-specific transcription adapters and UI layers. + ### Step 1: Clone the Repository ```bash From 4260fcb6938d411dd0dd619b2bc9b51ba0ea20df Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 15:38:12 -0600 Subject: [PATCH 3/9] Update KMP bridge and Kotlin Gradle version - Pass recovery timing as KotlinDouble in the transcription bridge - Bump shared multiplatform Gradle plugin to 2.3.10 - Refresh repo guidance for the KMP workspace --- AGENTS.md | 14 ++++++++++---- .../Transcription/KMPTranscriptionBridge.swift | 2 +- shared/build.gradle.kts | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f5ed502..943a1a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,13 @@ # Repository Guidelines -Last updated: 2026-03-24 +Last updated: 2026-03-28 ## Project Snapshot - App: `Pindrop` (menu bar macOS app, `LSUIElement` behavior) -- Stack: Swift 5.9+, SwiftUI, SwiftData, Swift Testing, XCTest UI tests -- Platform target: macOS 14+ -- Main dependency path: `Pindrop.xcodeproj` + SwiftPM +- Stack: Swift 5.9+, SwiftUI, SwiftData, Swift Testing, XCTest UI tests, Kotlin Multiplatform shared modules +- Platform target: macOS 14+ for the shipped app; shared KMP code is intended to support future Windows/Linux targets +- Main dependency path: `Pindrop.xcodeproj`, SwiftPM, and the Gradle-based KMP workspace under `shared/` - Entry points: `Pindrop/PindropApp.swift`, `Pindrop/AppCoordinator.swift` ## Source Layout @@ -19,12 +19,15 @@ Last updated: 2026-03-24 - Utilities/logging: `Pindrop/Utils/` - Tests: `PindropTests/` - Test doubles: `PindropTests/TestHelpers/` +- Shared KMP workspace: `shared/` - Build automation: `justfile`, `scripts/`, `.github/workflows/` ## Required Local Tooling - Xcode with command-line tools (`xcodebuild`) - `just` for all routine workflows: `brew install just` +- JDK 21+ for the Gradle/Kotlin toolchain used by `shared/` +- Prefer the checked-in Gradle wrapper at `shared/gradlew` over a globally installed `gradle` - Optional: `swiftlint`, `swiftformat`, `create-dmg` - Apple Developer signing configured in Xcode for signed local/release builds; CI recipes use explicit unsigned overrides @@ -38,6 +41,8 @@ just build-release # Release build just export-app # Developer ID export for distribution just dmg # Signed DMG for distribution just test # Unit test plan +just shared-test # Kotlin shared-module tests +just shared-xcframework # Build embedded KMP XCFrameworks just test-integration # Integration test plan (opt-in) just test-ui # UI test plan just test-all # Unit + integration + UI @@ -125,6 +130,7 @@ xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -destination 'platfor ## Release and Distribution - Local release helpers: `just build-release`, `just export-app`, `just dmg`, `just dmg-self-signed` (fallback only) +- macOS release builds embed the XCFrameworks produced from `shared/`; KMP is a build input, not a separate end-user artifact - Manual release flow is `just release ` (local execution, not CI-driven) 1. Create/edit contextual release notes (`release-notes/vX.Y.Z.md`) 2. Run tests diff --git a/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift b/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift index 9750050..5e589f2 100644 --- a/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift +++ b/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift @@ -531,7 +531,7 @@ enum KMPTranscriptionBridge { ) -> SharedEventTapRecoveryDecision { #if canImport(PindropSharedTranscription) let decision = SharedTranscriptionOrchestrator.shared.determineEventTapRecovery( - elapsedSinceLastDisableSeconds: elapsedSinceLastDisable.map(NSNumber.init(value:)), + elapsedSinceLastDisableSeconds: elapsedSinceLastDisable.map(KotlinDouble.init(value:)), consecutiveDisableCount: Int32(consecutiveDisableCount), disableLoopWindowSeconds: disableLoopWindow, maxReenableAttemptsBeforeRecreate: Int32(maxReenableAttemptsBeforeRecreate) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index cfe77c6..dbd7cb4 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,3 +1,3 @@ plugins { - kotlin("multiplatform") version "2.1.20" apply false + kotlin("multiplatform") version "2.3.10" apply false } From b8c0c92a4f44e9d718bfac7a94bacd6dd53efbee Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 16:02:37 -0600 Subject: [PATCH 4/9] Add compact hover handling to pill indicator - Track hover on the compact indicator state - Ignore hover while recording, processing, dragging, or in a context menu --- Pindrop/UI/PillFloatingIndicator.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Pindrop/UI/PillFloatingIndicator.swift b/Pindrop/UI/PillFloatingIndicator.swift index b5d7d81..3bb360d 100644 --- a/Pindrop/UI/PillFloatingIndicator.swift +++ b/Pindrop/UI/PillFloatingIndicator.swift @@ -528,6 +528,15 @@ final class PillFloatingIndicatorController: NSObject, ObservableObject, NSMenuD } } + func handleCompactHover(_ hovering: Bool) { + guard isVisible, !isDragging, !state.isRecording, !state.isProcessing, !isContextMenuOpen else { return } + + if hovering { + lastHoverContactAt = Date() + setHoverState(true) + } + } + func startRecording() { isHovered = false hideHoverTooltip() @@ -784,6 +793,9 @@ struct PillIndicatorView: View { } .buttonStyle(.plain) .contentShape(Capsule()) + .onHover { hovering in + controller.handleCompactHover(hovering) + } if controller.isHoverTooltipVisible { PillHoverTooltip( From e90efe33733e8ce1b977e70e3991222c2deb799c Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 16:18:03 -0600 Subject: [PATCH 5/9] Fix transcription trigger and unsupported provider errors - Align recording-start trigger naming across Swift and KMP - Preserve load failure state when local model loading is unsupported - Add tests for dedup bypass and unsupported provider handling --- .../KMPTranscriptionBridge.swift | 2 +- Pindrop/Services/TranscriptionService.swift | 7 ++- PindropTests/TranscriptionServiceTests.swift | 50 +++++++++++++++---- .../SharedTranscriptionOrchestrator.kt | 2 +- .../SharedTranscriptionOrchestratorTest.kt | 2 +- 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift b/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift index 5e589f2..2cdbd24 100644 --- a/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift +++ b/Pindrop/Services/Transcription/KMPTranscriptionBridge.swift @@ -584,7 +584,7 @@ enum KMPTranscriptionBridge { lastSignature: lastSignature ) #else - trigger == "recordingStart" || lastSignature == nil || lastSignature != signature + trigger == "recording_start" || lastSignature == nil || lastSignature != signature #endif } diff --git a/Pindrop/Services/TranscriptionService.swift b/Pindrop/Services/TranscriptionService.swift index 2bc44a6..a4b5036 100644 --- a/Pindrop/Services/TranscriptionService.swift +++ b/Pindrop/Services/TranscriptionService.swift @@ -112,7 +112,12 @@ class TranscriptionService { } guard loadPlan.supportsLocalModelLoading else { - throw TranscriptionError.modelLoadFailed("Provider \(loadPlan.resolvedProvider.rawValue) not supported locally") + let loadError = TranscriptionError.modelLoadFailed( + "Provider \(loadPlan.resolvedProvider.rawValue) not supported locally" + ) + self.error = loadError + state = KMPTranscriptionBridge.completeModelLoad(success: false) + throw loadError } error = nil diff --git a/PindropTests/TranscriptionServiceTests.swift b/PindropTests/TranscriptionServiceTests.swift index c8eb246..08b861e 100644 --- a/PindropTests/TranscriptionServiceTests.swift +++ b/PindropTests/TranscriptionServiceTests.swift @@ -688,15 +688,47 @@ struct TranscriptionServiceTests { } @Test func invalidProviderHandling() async throws { - // This tests that non-local providers throw appropriate errors - // The implementation should reject cloud-only providers - // Currently WhisperKit and Parakeet are the only local providers - - // Verify error type exists for this case - let error = TranscriptionService.TranscriptionError.modelLoadFailed("Provider not supported locally") - #expect(error.errorDescription != nil) - #expect(error.errorDescription?.contains("not supported") ?? false, - "Error should indicate provider not supported") + let service = TranscriptionService(engineFactory: { _ in + Issue.record("Engine factory should not be called for unsupported providers") + return MockDiarizationTranscriptionEngine() + }) + + do { + try await service.loadModel(modelName: "openai_whisper-1", provider: .openAI) + Issue.record("Expected unsupported provider load to throw") + } catch let error as TranscriptionService.TranscriptionError { + guard case let .modelLoadFailed(message) = error else { + Issue.record("Expected modelLoadFailed, got \(error)") + return + } + + #expect(message.contains("not supported locally")) + #expect(service.state == .error) + + guard let storedError = service.error as? TranscriptionService.TranscriptionError else { + Issue.record("Expected service.error to store a TranscriptionError") + return + } + + guard case let .modelLoadFailed(storedMessage) = storedError else { + Issue.record("Expected stored error to be modelLoadFailed, got \(storedError)") + return + } + + #expect(storedMessage == message) + } catch { + Issue.record("Unexpected error type: \(error)") + } + } + + @Test func recordingStartTransitionBypassesDeduplication() { + #expect( + KMPTranscriptionBridge.shouldAppendTransition( + signature: "same-signature", + trigger: ContextSessionUpdateTrigger.recordingStart.rawValue, + lastSignature: "same-signature" + ) + ) } private func makeStreamingBuffer(frameCount: AVAudioFrameCount = 320) throws -> AVAudioPCMBuffer { diff --git a/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt b/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt index 1644f57..3f32c14 100644 --- a/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt +++ b/shared/feature-transcription/src/commonMain/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestrator.kt @@ -380,7 +380,7 @@ object SharedTranscriptionOrchestrator { trigger: String, lastSignature: String?, ): Boolean { - if (trigger == "recordingStart") { + if (trigger == "recording_start") { return true } diff --git a/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt index e0dc736..17faf5c 100644 --- a/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt +++ b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/SharedTranscriptionOrchestratorTest.kt @@ -332,7 +332,7 @@ class SharedTranscriptionOrchestratorTest { assertTrue( SharedTranscriptionOrchestrator.shouldAppendTransition( signature = "a", - trigger = "recordingStart", + trigger = "recording_start", lastSignature = "a", ), ) From 66e522235f158ebdb29d46b7affa025530c42a7b Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 17:01:20 -0600 Subject: [PATCH 6/9] Unify media transcription state and slim KMP builds --- Pindrop.xcodeproj/project.pbxproj | 4 +- Pindrop/Models/MediaTranscriptionTypes.swift | 268 ++++++++++++++++-- .../MediaTranscriptionFeatureStateTests.swift | 28 ++ PindropTests/ParakeetEngineTests.swift | 2 +- PindropTests/TranscriptionServiceTests.swift | 263 +++++++++-------- PindropTests/WhisperKitEngineTests.swift | 2 +- README.md | 1 + scripts/build-shared-frameworks-if-needed.sh | 48 ++++ shared/README.md | 5 + shared/build.gradle.kts | 18 ++ shared/core/build.gradle.kts | 6 - shared/feature-transcription/build.gradle.kts | 6 - .../MediaTranscriptionJobStateMachineTest.kt | 17 ++ 13 files changed, 497 insertions(+), 171 deletions(-) create mode 100755 scripts/build-shared-frameworks-if-needed.sh diff --git a/Pindrop.xcodeproj/project.pbxproj b/Pindrop.xcodeproj/project.pbxproj index b3ddc2d..eb72b24 100644 --- a/Pindrop.xcodeproj/project.pbxproj +++ b/Pindrop.xcodeproj/project.pbxproj @@ -865,6 +865,7 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/scripts/build-shared-frameworks-if-needed.sh", "$(SRCROOT)/shared/build.gradle.kts", "$(SRCROOT)/shared/gradle.properties", "$(SRCROOT)/shared/core/build.gradle.kts", @@ -876,12 +877,13 @@ outputFileListPaths = ( ); outputPaths = ( + "$(SRCROOT)/shared/build/xcode-shared-frameworks.stamp", "$(SRCROOT)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/Info.plist", "$(SRCROOT)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/Info.plist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -euo pipefail\ncd \"$SRCROOT/shared\"\n\"$SRCROOT/shared/gradlew\" --no-daemon --console=plain -p \"$SRCROOT/shared\" :core:assemblePindropSharedCoreXCFramework :feature-transcription:assemblePindropSharedTranscriptionXCFramework\n"; + shellScript = "set -euo pipefail\n\"$SRCROOT/scripts/build-shared-frameworks-if-needed.sh\"\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Pindrop/Models/MediaTranscriptionTypes.swift b/Pindrop/Models/MediaTranscriptionTypes.swift index 2532300..f1ab69c 100644 --- a/Pindrop/Models/MediaTranscriptionTypes.swift +++ b/Pindrop/Models/MediaTranscriptionTypes.swift @@ -177,39 +177,53 @@ final class MediaTranscriptionFeatureState { } func selectRecord(_ id: UUID) { - selectedRecordID = id - route = .detail(id) - libraryMessage = nil + applySharedSnapshot( + KMPMediaTranscriptionBridge.selectRecord( + snapshot: sharedSnapshot(), + recordID: id + ) + ) } func showLibrary() { - route = .library + applySharedSnapshot( + KMPMediaTranscriptionBridge.showLibrary(snapshot: sharedSnapshot()) + ) } func selectFolder(_ id: UUID) { - selectedFolderID = id - libraryMessage = nil + applySharedSnapshot( + KMPMediaTranscriptionBridge.selectFolder( + snapshot: sharedSnapshot(), + folderID: id + ) + ) } func clearSelectedFolder() { - selectedFolderID = nil + applySharedSnapshot( + KMPMediaTranscriptionBridge.clearSelectedFolder(snapshot: sharedSnapshot()) + ) } func handleDeletedRecord(_ id: UUID, message: String = "Transcription deleted.") { - if case .detail(let detailID) = route, detailID == id { - route = .library - } - if selectedRecordID == id { - selectedRecordID = nil - } - libraryMessage = message + applySharedSnapshot( + KMPMediaTranscriptionBridge.handleDeletedRecord( + snapshot: sharedSnapshot(), + recordID: id, + message: message + ) + ) } func handleDeletedFolder(_ id: UUID, message: String = "Folder deleted.") { - if selectedFolderID == id { - selectedFolderID = nil - } - libraryMessage = message + applySharedSnapshot( + KMPMediaTranscriptionBridge.handleDeletedFolder( + snapshot: sharedSnapshot(), + folderID: id, + message: message + ) + ) } func completeCurrentJob(with recordID: UUID, shouldNavigateToDetail: Bool) { @@ -233,12 +247,18 @@ final class MediaTranscriptionFeatureState { } func clearCurrentJob() { - currentJob = nil + applySharedSnapshot( + KMPMediaTranscriptionBridge.clearCurrentJob(snapshot: sharedSnapshot()) + ) } func setSetupIssue(_ message: String) { - setupIssue = message - route = .library + applySharedSnapshot( + KMPMediaTranscriptionBridge.setSetupIssue( + snapshot: sharedSnapshot(), + message: message + ) + ) } func clearSetupIssue() { @@ -246,7 +266,12 @@ final class MediaTranscriptionFeatureState { } func setLibraryMessage(_ message: String?) { - libraryMessage = message + applySharedSnapshot( + KMPMediaTranscriptionBridge.setLibraryMessage( + snapshot: sharedSnapshot(), + message: message + ) + ) } func updateDraftLinkFromClipboard(_ candidate: String) { @@ -287,6 +312,25 @@ struct SharedMediaTranscriptionFeatureSnapshot: Equatable, Sendable { } enum KMPMediaTranscriptionBridge { + static func showLibrary( + snapshot: SharedMediaTranscriptionFeatureSnapshot + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.showLibrary(snapshot: coreSnapshot) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: .library, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: snapshot.currentJob, + setupIssue: snapshot.setupIssue, + libraryMessage: snapshot.libraryMessage + ) + #endif + } + static func beginJob( snapshot: SharedMediaTranscriptionFeatureSnapshot, job: MediaTranscriptionJobState @@ -310,6 +354,71 @@ enum KMPMediaTranscriptionBridge { #endif } + static func selectRecord( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + recordID: UUID + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.selectRecord( + snapshot: coreSnapshot, + recordId: recordID.uuidString + ) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: .detail(recordID), + selectedRecordID: recordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: snapshot.currentJob, + setupIssue: snapshot.setupIssue, + libraryMessage: nil + ) + #endif + } + + static func selectFolder( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + folderID: UUID + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.selectFolder( + snapshot: coreSnapshot, + folderId: folderID.uuidString + ) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: snapshot.route, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: folderID, + currentJob: snapshot.currentJob, + setupIssue: snapshot.setupIssue, + libraryMessage: nil + ) + #endif + } + + static func clearSelectedFolder( + snapshot: SharedMediaTranscriptionFeatureSnapshot + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.clearSelectedFolder(snapshot: coreSnapshot) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: snapshot.route, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: nil, + currentJob: snapshot.currentJob, + setupIssue: snapshot.setupIssue, + libraryMessage: snapshot.libraryMessage + ) + #endif + } + static func updateJob( snapshot: SharedMediaTranscriptionFeatureSnapshot, stage: MediaTranscriptionStage, @@ -413,6 +522,121 @@ enum KMPMediaTranscriptionBridge { #endif } + static func clearCurrentJob( + snapshot: SharedMediaTranscriptionFeatureSnapshot + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.clearCurrentJob(snapshot: coreSnapshot) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: snapshot.route, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: nil, + setupIssue: snapshot.setupIssue, + libraryMessage: snapshot.libraryMessage + ) + #endif + } + + static func setSetupIssue( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + message: String + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.setSetupIssue( + snapshot: coreSnapshot, + message: message + ) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: .library, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: snapshot.currentJob, + setupIssue: message, + libraryMessage: snapshot.libraryMessage + ) + #endif + } + + static func setLibraryMessage( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + message: String? + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.setLibraryMessage( + snapshot: coreSnapshot, + message: message + ) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: snapshot.route, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: snapshot.currentJob, + setupIssue: snapshot.setupIssue, + libraryMessage: message + ) + #endif + } + + static func handleDeletedRecord( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + recordID: UUID, + message: String + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.handleDeletedRecord( + snapshot: coreSnapshot, + recordId: recordID.uuidString, + message: message + ) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: snapshot.route == .detail(recordID) ? .library : snapshot.route, + selectedRecordID: snapshot.selectedRecordID == recordID ? nil : snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID, + currentJob: snapshot.currentJob, + setupIssue: snapshot.setupIssue, + libraryMessage: message + ) + #endif + } + + static func handleDeletedFolder( + snapshot: SharedMediaTranscriptionFeatureSnapshot, + folderID: UUID, + message: String + ) -> SharedMediaTranscriptionFeatureSnapshot { + #if canImport(PindropSharedTranscription) + apply(snapshot) { machine, coreSnapshot in + machine.handleDeletedFolder( + snapshot: coreSnapshot, + folderId: folderID.uuidString, + message: message + ) + } + #else + SharedMediaTranscriptionFeatureSnapshot( + route: snapshot.route, + selectedRecordID: snapshot.selectedRecordID, + selectedFolderID: snapshot.selectedFolderID == folderID ? nil : snapshot.selectedFolderID, + currentJob: snapshot.currentJob, + setupIssue: snapshot.setupIssue, + libraryMessage: message + ) + #endif + } + } #if canImport(PindropSharedTranscription) diff --git a/PindropTests/MediaTranscriptionFeatureStateTests.swift b/PindropTests/MediaTranscriptionFeatureStateTests.swift index 2700d0e..fe16be8 100644 --- a/PindropTests/MediaTranscriptionFeatureStateTests.swift +++ b/PindropTests/MediaTranscriptionFeatureStateTests.swift @@ -85,6 +85,34 @@ struct MediaTranscriptionFeatureStateTests { #expect(sut.selectedFolderID == nil) } + @Test func setupIssueAndLibraryMessageFollowSharedStateMachine() { + let sut = MediaTranscriptionFeatureState() + + sut.setSetupIssue("ffmpeg missing") + #expect(sut.route == .library) + #expect(sut.setupIssue == "ffmpeg missing") + + sut.clearSetupIssue() + #expect(sut.setupIssue == nil) + + sut.setLibraryMessage("Ready") + #expect(sut.libraryMessage == "Ready") + + sut.setLibraryMessage(nil) + #expect(sut.libraryMessage == nil) + } + + @Test func clearCurrentJobUsesSharedStateMachine() { + let sut = MediaTranscriptionFeatureState() + let job = MediaTranscriptionJobState(request: .link("https://example.com")) + sut.beginJob(job) + + sut.clearCurrentJob() + + #expect(sut.currentJob == nil) + #expect(sut.route == .processing(job.id)) + } + @Test func librarySearchAndSortStateRemainMutableDuringJobLifecycle() { let sut = MediaTranscriptionFeatureState() let folderID = UUID() diff --git a/PindropTests/ParakeetEngineTests.swift b/PindropTests/ParakeetEngineTests.swift index fa67f6f..f691f2e 100644 --- a/PindropTests/ParakeetEngineTests.swift +++ b/PindropTests/ParakeetEngineTests.swift @@ -10,7 +10,7 @@ import Testing @testable import Pindrop @MainActor -@Suite +@Suite(.enabled(if: ProcessInfo.processInfo.environment["PINDROP_RUN_INTEGRATION_TESTS"] == "1", "Parakeet engine integration tests are disabled by default. Run `just test-integration` to execute them.")) struct ParakeetEngineTests { private func makeEngine() -> ParakeetEngine { ParakeetEngine() diff --git a/PindropTests/TranscriptionServiceTests.swift b/PindropTests/TranscriptionServiceTests.swift index 08b861e..03a77f9 100644 --- a/PindropTests/TranscriptionServiceTests.swift +++ b/PindropTests/TranscriptionServiceTests.swift @@ -21,29 +21,30 @@ struct TranscriptionServiceTests { } @Test func modelLoadingStates() async throws { - let service = TranscriptionService() + let mockEngine = MockDiarizationTranscriptionEngine() + mockEngine.loadDelayNanoseconds = 300_000_000 + let service = TranscriptionService(engineFactory: { _ in mockEngine }) - // Start loading model Task { - do { - try await service.loadModel(modelName: "tiny") - } catch { - // Expected to fail in test environment without actual model - } + try? await service.loadModel(modelName: "tiny") } - - // Give it a moment to start loading - try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - // State should have changed from unloaded - #expect(service.state != .unloaded, "State should change from unloaded when loading starts") + + try await Task.sleep(nanoseconds: 100_000_000) + + #expect(service.state == .loading, "State should transition to loading while model load is in flight") + #expect(mockEngine.state == .loading) } @Test func modelLoadingError() async throws { - let service = TranscriptionService() - + let mockEngine = MockDiarizationTranscriptionEngine() + mockEngine.loadModelPathError = NSError( + domain: "TranscriptionServiceTests", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "bad model path"] + ) + let service = TranscriptionService(engineFactory: { _ in mockEngine }) + do { - // Try to load with invalid model path try await service.loadModel(modelPath: "/invalid/path/to/model") Issue.record("Should throw error for invalid model path") } catch { @@ -56,17 +57,9 @@ struct TranscriptionServiceTests { @Test func transcribeWithoutLoadedModel() async throws { let service = TranscriptionService() - - // Create dummy audio data (16kHz mono PCM) - let sampleCount = 16000 // 1 second of audio - var audioData = Data() - for _ in 0...size)) - } - + do { - _ = try await service.transcribe(audioData: audioData) + _ = try await service.transcribe(audioData: makeFloatAudioData(seconds: 1.0)) Issue.record("Should throw error when model not loaded") } catch TranscriptionService.TranscriptionError.modelNotLoaded { } catch { @@ -75,14 +68,9 @@ struct TranscriptionServiceTests { } @Test func transcribeWithEmptyAudioData() async throws { - let service = TranscriptionService() - - // Try to load model first (will fail in test environment, but that's ok) - do { - try await service.loadModel(modelName: "tiny") - } catch { - // Expected to fail in test environment - } + let mockEngine = MockDiarizationTranscriptionEngine() + let service = TranscriptionService(engineFactory: { _ in mockEngine }) + try await service.loadModel(modelName: "tiny") let emptyData = Data() @@ -101,40 +89,33 @@ struct TranscriptionServiceTests { // MARK: - State Management Tests @Test func stateTransitions() async throws { - let service = TranscriptionService() + let mockEngine = MockDiarizationTranscriptionEngine() + mockEngine.loadDelayNanoseconds = 300_000_000 + let service = TranscriptionService(engineFactory: { _ in mockEngine }) #expect(service.state == .unloaded) - // Attempt to load model (will fail in test environment) Task { - do { - try await service.loadModel(modelName: "tiny") - } catch { - // Expected - } + try? await service.loadModel(modelName: "tiny") } - - // Give it time to transition + try await Task.sleep(nanoseconds: 100_000_000) - // State should have changed - #expect(service.state != .unloaded) + #expect(service.state == .loading) + + try await Task.sleep(nanoseconds: 300_000_000) + + #expect(service.state == .ready) } @Test func concurrentTranscriptionPrevention() async throws { - let service = TranscriptionService() - - // Create dummy audio data - let sampleCount = 16000 - var audioData = Data() - for _ in 0...size)) - } - - let testAudioData = audioData + let mockEngine = MockDiarizationTranscriptionEngine() + mockEngine.transcribeResponses = ["first"] + mockEngine.transcribeDelayNanoseconds = 300_000_000 + let service = TranscriptionService(engineFactory: { _ in mockEngine }) + try await service.loadModel(modelName: "tiny") - // Try to transcribe twice concurrently + let testAudioData = makeFloatAudioData(seconds: 1.0) async let result1 = service.transcribe(audioData: testAudioData) async let result2 = service.transcribe(audioData: testAudioData) @@ -142,54 +123,51 @@ struct TranscriptionServiceTests { _ = try await result1 _ = try await result2 Issue.record("Should not allow concurrent transcriptions") + } catch let error as TranscriptionService.TranscriptionError { + guard case .transcriptionFailed(let message) = error else { + Issue.record("Unexpected error: \(error)") + return + } + #expect(message.contains("already in progress")) } catch { + Issue.record("Unexpected error: \(error)") } } // MARK: - Engine Switching Integration Tests @Test func engineSwitchCallsUnloadForDifferentProvider() async throws { - // Given: Service starts in unloaded state - let service = TranscriptionService() + let whisperEngine = MockDiarizationTranscriptionEngine() + let parakeetEngine = MockDiarizationTranscriptionEngine() + let service = TranscriptionService( + engineFactory: { provider in + switch provider { + case .whisperKit: + return whisperEngine + case .parakeet: + return parakeetEngine + default: + throw TranscriptionService.TranscriptionError.modelLoadFailed("unsupported") + } + } + ) #expect(service.state == .unloaded) - - // When: Attempt to load a model (fails in test env but exercises switching path) - do { - try await service.loadModel(modelPath: "/test/whisperkit/model") - } catch { - // Expected: model path doesn't exist - } - - let stateAfterFirstLoad = service.state - - // When: Switch provider - do { - try await service.loadModel(modelPath: "/test/parakeet/model") - } catch { - // Expected - } - - // Then: Both load attempts should have been made (state changed from unloaded) - #expect(stateAfterFirstLoad == .error, "First load should result in error for invalid path") - #expect(service.state == .error, "Second load should also result in error") + + try await service.loadModel(modelName: "tiny", provider: .whisperKit) + try await service.loadModel(modelName: "parakeet-tdt-0.6b-v3", provider: .parakeet) + + #expect(whisperEngine.unloadCallCount == 1) + #expect(parakeetEngine.loadModelNameCalls == ["parakeet-tdt-0.6b-v3"]) + #expect(service.state == .ready) } @Test func engineSwitchPreservesUnloadedStateOnCleanup() async throws { - let service = TranscriptionService() - - // Given: Attempt failed loads (exercises switching logic) - do { - try await service.loadModel(modelPath: "/test/path1") - } catch {} - - do { - try await service.loadModel(modelPath: "/test/path2") - } catch {} - - // When: Unload after switching attempts + let mockEngine = MockDiarizationTranscriptionEngine() + let service = TranscriptionService(engineFactory: { _ in mockEngine }) + + try await service.loadModel(modelName: "tiny") await service.unloadModel() - - // Then: Should be back to clean unloaded state + #expect(service.state == .unloaded) #expect(service.error == nil) } @@ -215,61 +193,54 @@ struct TranscriptionServiceTests { } @Test func unloadModelReleasesEngineReference() async throws { - let service = TranscriptionService() - - // Given: Attempt to load an engine - do { - try await service.loadModel(modelName: "tiny", provider: .whisperKit) - } catch { - // Expected in test environment - } - - // When: Unload the model + let mockEngine = MockDiarizationTranscriptionEngine() + let service = TranscriptionService(engineFactory: { _ in mockEngine }) + + try await service.loadModel(modelName: "tiny", provider: .whisperKit) await service.unloadModel() - - // Then: State should be unloaded and error cleared + #expect(service.state == .unloaded, "State should be unloaded after unloadModel") #expect(service.error == nil, "Error should be nil after unloadModel") + #expect(mockEngine.unloadCallCount == 1) } @Test func unloadModelAfterSwitchingEngines() async throws { - let service = TranscriptionService() - - // Given: Load and switch engines (both will fail in test env) - do { - try await service.loadModel(modelName: "tiny", provider: .whisperKit) - } catch {} - - do { - try await service.loadModel(modelName: "parakeet-tdt-0.6b-v3", provider: .parakeet) - } catch {} - - // When: Unload + let whisperEngine = MockDiarizationTranscriptionEngine() + let parakeetEngine = MockDiarizationTranscriptionEngine() + let service = TranscriptionService( + engineFactory: { provider in + switch provider { + case .whisperKit: + return whisperEngine + case .parakeet: + return parakeetEngine + default: + throw TranscriptionService.TranscriptionError.modelLoadFailed("unsupported") + } + } + ) + + try await service.loadModel(modelName: "tiny", provider: .whisperKit) + try await service.loadModel(modelName: "parakeet-tdt-0.6b-v3", provider: .parakeet) await service.unloadModel() - - // Then: Should cleanly return to unloaded state + #expect(service.state == .unloaded, "State should be unloaded") #expect(service.error == nil, "Error should be cleared") + #expect(parakeetEngine.unloadCallCount == 1) } @Test func reloadSameEngineAfterUnload() async throws { - let service = TranscriptionService() - - // Given: Load then unload WhisperKit - do { - try await service.loadModel(modelName: "tiny", provider: .whisperKit) - } catch {} - + let mockEngine = MockDiarizationTranscriptionEngine() + let service = TranscriptionService(engineFactory: { _ in mockEngine }) + + try await service.loadModel(modelName: "tiny", provider: .whisperKit) await service.unloadModel() #expect(service.state == .unloaded) - - // When: Load same engine again - do { - try await service.loadModel(modelName: "tiny", provider: .whisperKit) - } catch {} - - // Then: Should attempt to load (state transitions from unloaded) - #expect(service.state != .unloaded, "State should change when reloading engine") + + try await service.loadModel(modelName: "tiny", provider: .whisperKit) + + #expect(service.state == .ready, "State should be ready after reloading engine") + #expect(mockEngine.loadModelNameCalls == ["tiny", "tiny"]) } // MARK: - Speaker Diarization Tests @@ -759,6 +730,10 @@ private final class MockDiarizationTranscriptionEngine: TranscriptionEngine { private(set) var state: TranscriptionEngineState = .unloaded var transcribeResponses: [String] = [] var transcribeError: Error? + var transcribeDelayNanoseconds: UInt64 = 0 + var loadDelayNanoseconds: UInt64 = 0 + var loadModelPathError: Error? + var loadModelNameError: Error? private(set) var transcribeCallCount = 0 private(set) var receivedOptions: [TranscriptionOptions] = [] private(set) var loadModelNameCalls: [String] = [] @@ -767,11 +742,27 @@ private final class MockDiarizationTranscriptionEngine: TranscriptionEngine { func loadModel(path: String) async throws { loadModelPathCalls.append(path) + state = .loading + if loadDelayNanoseconds > 0 { + try await Task.sleep(nanoseconds: loadDelayNanoseconds) + } + if let loadModelPathError { + state = .error + throw loadModelPathError + } state = .ready } func loadModel(name: String, downloadBase: URL?) async throws { loadModelNameCalls.append(name) + state = .loading + if loadDelayNanoseconds > 0 { + try await Task.sleep(nanoseconds: loadDelayNanoseconds) + } + if let loadModelNameError { + state = .error + throw loadModelNameError + } state = .ready } @@ -780,6 +771,10 @@ private final class MockDiarizationTranscriptionEngine: TranscriptionEngine { throw transcribeError } + if transcribeDelayNanoseconds > 0 { + try await Task.sleep(nanoseconds: transcribeDelayNanoseconds) + } + receivedOptions.append(options) transcribeCallCount += 1 if transcribeResponses.isEmpty { diff --git a/PindropTests/WhisperKitEngineTests.swift b/PindropTests/WhisperKitEngineTests.swift index b7c93f4..1d16658 100644 --- a/PindropTests/WhisperKitEngineTests.swift +++ b/PindropTests/WhisperKitEngineTests.swift @@ -10,7 +10,7 @@ import Testing @testable import Pindrop @MainActor -@Suite +@Suite(.enabled(if: ProcessInfo.processInfo.environment["PINDROP_RUN_INTEGRATION_TESTS"] == "1", "WhisperKit engine integration tests are disabled by default. Run `just test-integration` to execute them.")) struct WhisperKitEngineTests { private func makeEngine() -> WhisperKitEngine { WhisperKitEngine() diff --git a/README.md b/README.md index 5599d80..9593d39 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Since this is an open-source project, you can also build it yourself. Don't worr - `assets/`: images and other repository assets The current shipping app is still the native macOS target in `Pindrop/`. The `shared/` workspace contains cross-platform logic that future native Windows and Linux clients can reuse while keeping their own platform-specific transcription adapters and UI layers. +Today, the shared workspace only produces the macOS frameworks used by the app plus JVM test artifacts. Linux and Windows are represented by explicit stub verification tasks that fail fast until real platform targets are implemented. ### Step 1: Clone the Repository diff --git a/scripts/build-shared-frameworks-if-needed.sh b/scripts/build-shared-frameworks-if-needed.sh new file mode 100755 index 0000000..201a269 --- /dev/null +++ b/scripts/build-shared-frameworks-if-needed.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +set -eu + +SRCROOT="${SRCROOT:-$(cd "$(dirname "$0")/.." && pwd)}" +SHARED_DIR="$SRCROOT/shared" +CORE_INFO_PLIST="$SHARED_DIR/core/build/XCFrameworks/release/PindropSharedCore.xcframework/Info.plist" +TRANSCRIPTION_INFO_PLIST="$SHARED_DIR/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/Info.plist" +BUILD_STAMP="$SHARED_DIR/build/xcode-shared-frameworks.stamp" + +needs_build=0 + +if [ ! -f "$CORE_INFO_PLIST" ] || [ ! -f "$TRANSCRIPTION_INFO_PLIST" ] || [ ! -f "$BUILD_STAMP" ]; then + needs_build=1 +fi + +if [ "$needs_build" -eq 0 ]; then + newest_shared_input=$( + find "$SHARED_DIR" \ + \( -type d -name .gradle -o -type d -name build \) -prune \ + -o \ + \( -name '*.kts' -o -name '*.kt' -o -name '*.properties' -o -name 'gradlew' -o -name '*.swift' \) \ + -type f -print0 | + xargs -0 stat -f '%m %N' | + sort -nr | + head -n 1 | + cut -d' ' -f1 + ) + + build_stamp_mtime=$(stat -f '%m' "$BUILD_STAMP") + + if [ "${newest_shared_input:-0}" -gt "$build_stamp_mtime" ]; then + needs_build=1 + fi +fi + +if [ "$needs_build" -eq 0 ]; then + echo "Shared Kotlin frameworks are up to date; skipping Gradle." + exit 0 +fi + +echo "Building shared Kotlin frameworks..." +cd "$SHARED_DIR" +"$SHARED_DIR/gradlew" --no-daemon --console=plain -p "$SHARED_DIR" \ + :core:assemblePindropSharedCoreXCFramework \ + :feature-transcription:assemblePindropSharedTranscriptionXCFramework +mkdir -p "$(dirname "$BUILD_STAMP")" +touch "$BUILD_STAMP" diff --git a/shared/README.md b/shared/README.md index 788c800..0947c1a 100644 --- a/shared/README.md +++ b/shared/README.md @@ -7,6 +7,11 @@ Layout: - `core/`: shared domain types and cross-platform ports - `feature-transcription/`: shared transcription policy and orchestration logic +Current target status: +- `macosArm64` / `macosX64`: actively built and embedded into the app +- `jvm`: used for shared unit tests +- `desktopLinuxStub` / `desktopWindowsStub`: explicit placeholder tasks that fail with a clear "not implemented yet" error until real Linux/Windows targets land + Common commands from the repo root: - `just shared-test` - `just shared-xcframework` diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index dbd7cb4..1b2ec8f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,3 +1,21 @@ +import org.gradle.api.GradleException + plugins { kotlin("multiplatform") version "2.3.10" apply false } + +tasks.register("desktopLinuxStub") { + group = "verification" + description = "Fails fast to document that Linux support has not been implemented yet." + doLast { + throw GradleException("linuxX64 is a stub only. Windows/Linux support is not implemented yet.") + } +} + +tasks.register("desktopWindowsStub") { + group = "verification" + description = "Fails fast to document that Windows support has not been implemented yet." + doLast { + throw GradleException("mingwX64 is a stub only. Windows/Linux support is not implemented yet.") + } +} diff --git a/shared/core/build.gradle.kts b/shared/core/build.gradle.kts index b2253ba..7ceba64 100644 --- a/shared/core/build.gradle.kts +++ b/shared/core/build.gradle.kts @@ -13,18 +13,12 @@ kotlin { } val macosArm64Target = macosArm64() val macosX64Target = macosX64() - val iosArm64Target = iosArm64() - val iosSimulatorArm64Target = iosSimulatorArm64() - val iosX64Target = iosX64() val xcframework = XCFramework("PindropSharedCore") listOf( macosArm64Target, macosX64Target, - iosArm64Target, - iosSimulatorArm64Target, - iosX64Target, ).forEach { target -> target.binaries.framework { baseName = "PindropSharedCore" diff --git a/shared/feature-transcription/build.gradle.kts b/shared/feature-transcription/build.gradle.kts index cf068da..a3fe79f 100644 --- a/shared/feature-transcription/build.gradle.kts +++ b/shared/feature-transcription/build.gradle.kts @@ -13,18 +13,12 @@ kotlin { } val macosArm64Target = macosArm64() val macosX64Target = macosX64() - val iosArm64Target = iosArm64() - val iosSimulatorArm64Target = iosSimulatorArm64() - val iosX64Target = iosX64() val xcframework = XCFramework("PindropSharedTranscription") listOf( macosArm64Target, macosX64Target, - iosArm64Target, - iosSimulatorArm64Target, - iosX64Target, ).forEach { target -> target.binaries.framework { baseName = "PindropSharedTranscription" diff --git a/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt index 4a0d932..3d9fcda 100644 --- a/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt +++ b/shared/feature-transcription/src/commonTest/kotlin/tech/watzon/pindrop/shared/feature/transcription/MediaTranscriptionJobStateMachineTest.kt @@ -110,4 +110,21 @@ class MediaTranscriptionJobStateMachineTest { ) assertNull(afterFolderDelete.selectedFolderId) } + + @Test + fun setupIssueAndLibraryMessagesCanBeMutatedWithoutSwiftFallbacks() { + val initial = MediaTranscriptionJobStateMachine.setSetupIssue( + snapshot = MediaTranscriptionFeatureSnapshot(), + message = "ffmpeg missing", + ) + + assertEquals("ffmpeg missing", initial.setupIssue) + assertIs(initial.route) + + val messaged = MediaTranscriptionJobStateMachine.setLibraryMessage( + snapshot = initial, + message = "Ready", + ) + assertEquals("Ready", messaged.libraryMessage) + } } From 872bb92ad2085fa534606b2ac4812c5b26eaa103 Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 17:06:15 -0600 Subject: [PATCH 7/9] Ignore derived data variants --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5f36944..9f99187 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ xcuserdata/ # Build products build/ DerivedData/ +DerivedData*/ .gradle/ .kotlin/ *.ipa From 1aa4bc2d6c29f98b44fb4b40adf5c699962cfa56 Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 19:58:31 -0600 Subject: [PATCH 8/9] Integrate shared Kotlin UI presenters - Wire app and Xcode project to new shared UI KMP modules - Move dashboard, history, and dictionary state into shared presenters - Update related services, tests, and build scripts for the rewrite --- Pindrop.xcodeproj/project.pbxproj | 56 ++ Pindrop/AppCoordinator.swift | 11 +- Pindrop/Models/MediaTranscriptionTypes.swift | 20 + Pindrop/Services/AudioRecorder.swift | 2 +- Pindrop/Services/UpdateService.swift | 14 +- .../Services/WorkspaceFileIndexService.swift | 7 +- Pindrop/UI/FloatingIndicatorShared.swift | 2 +- Pindrop/UI/Main/DashboardView.swift | 54 +- Pindrop/UI/Main/DictionaryView.swift | 110 +++- Pindrop/UI/Main/HistoryView.swift | 79 ++- Pindrop/UI/Main/MainWindow.swift | 95 ++- Pindrop/UI/Main/NotesView.swift | 99 ++- Pindrop/UI/Main/TranscribeView.swift | 141 +++-- .../UI/Onboarding/AIEnhancementStepView.swift | 91 +++ .../Settings/AIEnhancementSettingsView.swift | 197 +++++- Pindrop/UI/Settings/ModelsSettingsView.swift | 152 +++-- .../UI/Settings/PresetManagementSheet.swift | 62 +- Pindrop/UI/Settings/SettingsWindow.swift | 126 ++-- Pindrop/UI/Theme/Theme.swift | 313 +++++----- Pindrop/UI/Theme/ThemeModels.swift | 388 ++++++------ PindropTests/LaunchAtLoginManagerTests.swift | 3 +- PindropTests/ModelManagerTests.swift | 2 +- PindropTests/SettingsStoreTests.swift | 335 ++++++++++ PindropTests/TranscriptionEngineTests.swift | 4 +- justfile | 4 +- scripts/build-shared-frameworks-if-needed.sh | 32 +- shared/settings.gradle.kts | 4 + shared/ui-settings/build.gradle.kts | 33 + .../uisettings/AISettingsPresentation.kt | 363 +++++++++++ .../uisettings/AISettingsPresentationTest.kt | 92 +++ shared/ui-shell/build.gradle.kts | 33 + .../pindrop/shared/uishell/ShellState.kt | 172 +++++ .../pindrop/shared/uishell/ShellStateTest.kt | 47 ++ shared/ui-theme/build.gradle.kts | 36 ++ .../pindrop/shared/uitheme/ThemeEngine.kt | 590 ++++++++++++++++++ .../pindrop/shared/uitheme/ThemeEngineTest.kt | 85 +++ shared/ui-workspace/build.gradle.kts | 33 + .../uiworkspace/WorkspacePresentation.kt | 567 +++++++++++++++++ .../uiworkspace/WorkspacePresentationTest.kt | 246 ++++++++ 39 files changed, 4044 insertions(+), 656 deletions(-) create mode 100644 shared/ui-settings/build.gradle.kts create mode 100644 shared/ui-settings/src/commonMain/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentation.kt create mode 100644 shared/ui-settings/src/commonTest/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentationTest.kt create mode 100644 shared/ui-shell/build.gradle.kts create mode 100644 shared/ui-shell/src/commonMain/kotlin/tech/watzon/pindrop/shared/uishell/ShellState.kt create mode 100644 shared/ui-shell/src/commonTest/kotlin/tech/watzon/pindrop/shared/uishell/ShellStateTest.kt create mode 100644 shared/ui-theme/build.gradle.kts create mode 100644 shared/ui-theme/src/commonMain/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngine.kt create mode 100644 shared/ui-theme/src/commonTest/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngineTest.kt create mode 100644 shared/ui-workspace/build.gradle.kts create mode 100644 shared/ui-workspace/src/commonMain/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentation.kt create mode 100644 shared/ui-workspace/src/commonTest/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentationTest.kt diff --git a/Pindrop.xcodeproj/project.pbxproj b/Pindrop.xcodeproj/project.pbxproj index eb72b24..7de7664 100644 --- a/Pindrop.xcodeproj/project.pbxproj +++ b/Pindrop.xcodeproj/project.pbxproj @@ -108,6 +108,14 @@ KMPSHAREDCOREEMBED01A2B3C4D5E6F /* PindropSharedCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDCOREFILE01A2B3C4D5E6F /* PindropSharedCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; KMPSHAREDTRANSBUILD01A2B3C4D5E6F /* PindropSharedTranscription.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */; }; KMPSHAREDTRANSEMBED01A2B3C4D5E6F /* PindropSharedTranscription.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + KMPSHAREDUITHEMEBUILD01A2B3C4D5E6F /* PindropSharedUITheme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUITHEMEFILE01A2B3C4D5E6F /* PindropSharedUITheme.framework */; }; + KMPSHAREDUITHEMEEMBED01A2B3C4D5E6F /* PindropSharedUITheme.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUITHEMEFILE01A2B3C4D5E6F /* PindropSharedUITheme.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + KMPSHAREDUISHELLBUILD01A2B3C4D5E6F /* PindropSharedNavigation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUISHELLFILE01A2B3C4D5E6F /* PindropSharedNavigation.framework */; }; + KMPSHAREDUISHELLEMBED01A2B3C4D5E6F /* PindropSharedNavigation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUISHELLFILE01A2B3C4D5E6F /* PindropSharedNavigation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + KMPSHAREDUISETTINGSBUILD01A2B3C4D5 /* PindropSharedSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUISETTINGSFILE01A2B3C4D5 /* PindropSharedSettings.framework */; }; + KMPSHAREDUISETTINGSEMBED01A2B3C4D5 /* PindropSharedSettings.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUISETTINGSFILE01A2B3C4D5 /* PindropSharedSettings.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + KMPSHAREDUIWORKSPACEBUILD01A2B3C4D5 /* PindropSharedUIWorkspace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUIWORKSPACEFILE01A2B3C4D5 /* PindropSharedUIWorkspace.framework */; }; + KMPSHAREDUIWORKSPACEEMBED01A2B3C4D5 /* PindropSharedUIWorkspace.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = KMPSHAREDUIWORKSPACEFILE01A2B3C4D5 /* PindropSharedUIWorkspace.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; MNTFMT01A2B3C4D5E6F /* MentionFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = MNTFMT02A2B3C4D5E6F /* MentionFormatter.swift */; }; MNTFMTTEST01A2B3C4D5E6F /* MentionFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = MNTFMTTEST02A2B3C4D5E6F /* MentionFormatterTests.swift */; }; MNTRWRT02A2B3C4D5E6F /* MentionRewriteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = MNTRWRT01A2B3C4D5E6F /* MentionRewriteService.swift */; }; @@ -187,6 +195,10 @@ files = ( KMPSHAREDCOREEMBED01A2B3C4D5E6F /* PindropSharedCore.framework in Embed Frameworks */, KMPSHAREDTRANSEMBED01A2B3C4D5E6F /* PindropSharedTranscription.framework in Embed Frameworks */, + KMPSHAREDUITHEMEEMBED01A2B3C4D5E6F /* PindropSharedUITheme.framework in Embed Frameworks */, + KMPSHAREDUISHELLEMBED01A2B3C4D5E6F /* PindropSharedNavigation.framework in Embed Frameworks */, + KMPSHAREDUISETTINGSEMBED01A2B3C4D5 /* PindropSharedSettings.framework in Embed Frameworks */, + KMPSHAREDUIWORKSPACEEMBED01A2B3C4D5 /* PindropSharedUIWorkspace.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -293,6 +305,10 @@ KMPPORTSFILE01A2B3C4D5E6F /* TranscriptionPorts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptionPorts.swift; sourceTree = ""; }; KMPSHAREDCOREFILE01A2B3C4D5E6F /* PindropSharedCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedCore.framework; path = shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64/PindropSharedCore.framework; sourceTree = SOURCE_ROOT; }; KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedTranscription.framework; path = shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64/PindropSharedTranscription.framework; sourceTree = SOURCE_ROOT; }; + KMPSHAREDUITHEMEFILE01A2B3C4D5E6F /* PindropSharedUITheme.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedUITheme.framework; path = shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/macos-arm64_x86_64/PindropSharedUITheme.framework; sourceTree = SOURCE_ROOT; }; + KMPSHAREDUISHELLFILE01A2B3C4D5E6F /* PindropSharedNavigation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedNavigation.framework; path = shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/macos-arm64_x86_64/PindropSharedNavigation.framework; sourceTree = SOURCE_ROOT; }; + KMPSHAREDUISETTINGSFILE01A2B3C4D5 /* PindropSharedSettings.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedSettings.framework; path = shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/macos-arm64_x86_64/PindropSharedSettings.framework; sourceTree = SOURCE_ROOT; }; + KMPSHAREDUIWORKSPACEFILE01A2B3C4D5 /* PindropSharedUIWorkspace.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PindropSharedUIWorkspace.framework; path = shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/macos-arm64_x86_64/PindropSharedUIWorkspace.framework; sourceTree = SOURCE_ROOT; }; MNTFMT02A2B3C4D5E6F /* MentionFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionFormatter.swift; sourceTree = ""; }; MNTFMTTEST02A2B3C4D5E6F /* MentionFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionFormatterTests.swift; sourceTree = ""; }; MNTRWRT01A2B3C4D5E6F /* MentionRewriteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionRewriteService.swift; sourceTree = ""; }; @@ -354,6 +370,10 @@ files = ( KMPSHAREDCOREBUILD01A2B3C4D5E6F /* PindropSharedCore.framework in Frameworks */, KMPSHAREDTRANSBUILD01A2B3C4D5E6F /* PindropSharedTranscription.framework in Frameworks */, + KMPSHAREDUITHEMEBUILD01A2B3C4D5E6F /* PindropSharedUITheme.framework in Frameworks */, + KMPSHAREDUISHELLBUILD01A2B3C4D5E6F /* PindropSharedNavigation.framework in Frameworks */, + KMPSHAREDUISETTINGSBUILD01A2B3C4D5 /* PindropSharedSettings.framework in Frameworks */, + KMPSHAREDUIWORKSPACEBUILD01A2B3C4D5 /* PindropSharedUIWorkspace.framework in Frameworks */, E30EF5B5D83B494BB7B98568 /* WhisperKit in Frameworks */, FLUIDAUDIOBUILD01A2B3C4D5E6F /* FluidAudio in Frameworks */, SPARKLEBUILD01A2B3C4D5E6F /* Sparkle in Frameworks */, @@ -502,6 +522,10 @@ 49CB4FF0EA78C24EFBB3F08A /* Cocoa.framework */, KMPSHAREDCOREFILE01A2B3C4D5E6F /* PindropSharedCore.framework */, KMPSHAREDTRANSFILE01A2B3C4D5E6F /* PindropSharedTranscription.framework */, + KMPSHAREDUITHEMEFILE01A2B3C4D5E6F /* PindropSharedUITheme.framework */, + KMPSHAREDUISHELLFILE01A2B3C4D5E6F /* PindropSharedNavigation.framework */, + KMPSHAREDUISETTINGSFILE01A2B3C4D5 /* PindropSharedSettings.framework */, + KMPSHAREDUIWORKSPACEFILE01A2B3C4D5 /* PindropSharedUIWorkspace.framework */, ); name = "OS X"; sourceTree = ""; @@ -870,6 +894,10 @@ "$(SRCROOT)/shared/gradle.properties", "$(SRCROOT)/shared/core/build.gradle.kts", "$(SRCROOT)/shared/feature-transcription/build.gradle.kts", + "$(SRCROOT)/shared/ui-shell/build.gradle.kts", + "$(SRCROOT)/shared/ui-settings/build.gradle.kts", + "$(SRCROOT)/shared/ui-theme/build.gradle.kts", + "$(SRCROOT)/shared/ui-workspace/build.gradle.kts", "$(SRCROOT)/shared/settings.gradle.kts", "$(SRCROOT)/shared/gradlew", ); @@ -880,6 +908,10 @@ "$(SRCROOT)/shared/build/xcode-shared-frameworks.stamp", "$(SRCROOT)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/Info.plist", "$(SRCROOT)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/Info.plist", + "$(SRCROOT)/shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/Info.plist", + "$(SRCROOT)/shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/Info.plist", + "$(SRCROOT)/shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/Info.plist", + "$(SRCROOT)/shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/Info.plist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1074,6 +1106,10 @@ "$(inherited)", "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/macos-arm64_x86_64", ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; @@ -1108,6 +1144,10 @@ "$(inherited)", "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/macos-arm64_x86_64", ); GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Pindrop/Info.plist; @@ -1194,6 +1234,10 @@ "$(inherited)", "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/macos-arm64_x86_64", ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; @@ -1293,6 +1337,10 @@ "$(inherited)", "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/macos-arm64_x86_64", ); GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Pindrop/Info.plist; @@ -1322,6 +1370,10 @@ "$(inherited)", "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/macos-arm64_x86_64", ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; @@ -1347,6 +1399,10 @@ "$(inherited)", "$(PROJECT_DIR)/shared/core/build/XCFrameworks/release/PindropSharedCore.xcframework/macos-arm64_x86_64", "$(PROJECT_DIR)/shared/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/macos-arm64_x86_64", + "$(PROJECT_DIR)/shared/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/macos-arm64_x86_64", ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; diff --git a/Pindrop/AppCoordinator.swift b/Pindrop/AppCoordinator.swift index ac18fe2..3f2c28a 100644 --- a/Pindrop/AppCoordinator.swift +++ b/Pindrop/AppCoordinator.swift @@ -1817,16 +1817,12 @@ final class AppCoordinator { mediaPauseService.endRecordingSession() suspendLiveContextSessionUpdates() isProcessing = true - var didResetProcessingState = false - statusBarController.setProcessingState() transitionRecordingIndicatorToProcessing() defer { - if !didResetProcessingState { - resetProcessingState() - } + resetProcessingState() } let audioData: Data @@ -2323,16 +2319,13 @@ final class AppCoordinator { mediaPauseService.endRecordingSession() suspendLiveContextSessionUpdates() isProcessing = true - var didResetProcessingState = false statusBarController.setProcessingState() transitionRecordingIndicatorToProcessing() defer { - if !didResetProcessingState { - resetProcessingState() - } + resetProcessingState() } do { diff --git a/Pindrop/Models/MediaTranscriptionTypes.swift b/Pindrop/Models/MediaTranscriptionTypes.swift index f1ab69c..1f81cd2 100644 --- a/Pindrop/Models/MediaTranscriptionTypes.swift +++ b/Pindrop/Models/MediaTranscriptionTypes.swift @@ -11,6 +11,9 @@ import Observation #if canImport(PindropSharedTranscription) import PindropSharedTranscription #endif +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif enum MediaLibrarySortMode: String, CaseIterable, Equatable, Sendable { case newest @@ -36,6 +39,23 @@ enum MediaLibrarySortMode: String, CaseIterable, Equatable, Sendable { } } +#if canImport(PindropSharedUIWorkspace) +extension MediaLibrarySortMode { + var coreValue: MediaLibrarySortModeCore { + switch self { + case .newest: + .newest + case .oldest: + .oldest + case .nameAscending: + .nameAscending + case .nameDescending: + .nameDescending + } + } +} +#endif + enum MediaTranscriptionStage: String, CaseIterable, Equatable, Sendable { case preflight case importing diff --git a/Pindrop/Services/AudioRecorder.swift b/Pindrop/Services/AudioRecorder.swift index 2055567..97915c7 100644 --- a/Pindrop/Services/AudioRecorder.swift +++ b/Pindrop/Services/AudioRecorder.swift @@ -351,7 +351,7 @@ final class AudioRecorder { var onAudioLevel: ((Float) -> Void)? var onAudioBuffer: ((AVAudioPCMBuffer) -> Void)? - nonisolated init( + init( permissionManager: some PermissionProviding, captureBackend: AudioCaptureBackend? = nil ) throws { diff --git a/Pindrop/Services/UpdateService.swift b/Pindrop/Services/UpdateService.swift index f17b4a7..eb67f1f 100644 --- a/Pindrop/Services/UpdateService.swift +++ b/Pindrop/Services/UpdateService.swift @@ -65,10 +65,10 @@ final class GentleReminderUserDriverDelegate: NSObject, SPUStandardUserDriverDel nonisolated func updater(_ updater: SPUUpdater, willScheduleUpdateCheckAfterDelay delay: TimeInterval) { Task { @MainActor in - UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) { granted, error in - if let error { - Log.app.warning("Failed to request notification authorization for updates: \(error.localizedDescription)") - } + do { + _ = try await UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) + } catch { + Log.app.warning("Failed to request notification authorization for updates: \(error.localizedDescription)") } } } @@ -86,7 +86,11 @@ final class GentleReminderUserDriverDelegate: NSObject, SPUStandardUserDriverDel content.sound = .default let request = UNNotificationRequest(identifier: self.notificationIdentifier, content: content, trigger: nil) - UNUserNotificationCenter.current().add(request) + do { + try await UNUserNotificationCenter.current().add(request) + } catch { + Log.app.warning("Failed to schedule update notification: \(error.localizedDescription)") + } } } } diff --git a/Pindrop/Services/WorkspaceFileIndexService.swift b/Pindrop/Services/WorkspaceFileIndexService.swift index 8c5cd3e..6fab7be 100644 --- a/Pindrop/Services/WorkspaceFileIndexService.swift +++ b/Pindrop/Services/WorkspaceFileIndexService.swift @@ -337,10 +337,13 @@ final class WorkspaceFileIndexService { ) } + let buildTimeout = Self.buildTimeout + let timeoutSeconds = Int(buildTimeout.components.seconds) + group.addTask { - try await Task.sleep(for: Self.buildTimeout) + try await Task.sleep(for: buildTimeout) throw WorkspaceFileIndexError.enumerationFailed( - "Indexing exceeded \(Int(Self.buildTimeout.components.seconds)) seconds" + "Indexing exceeded \(timeoutSeconds) seconds" ) } diff --git a/Pindrop/UI/FloatingIndicatorShared.swift b/Pindrop/UI/FloatingIndicatorShared.swift index f85c985..d641a55 100644 --- a/Pindrop/UI/FloatingIndicatorShared.swift +++ b/Pindrop/UI/FloatingIndicatorShared.swift @@ -73,7 +73,7 @@ extension Timer { /// Repeating timer on the main run loop in `.common` modes so it still fires during event tracking /// (window drag, resize, menus, scroll tracking) when `.default`-only timers are paused. @MainActor - static func pindrop_scheduleRepeating(interval: TimeInterval, block: @escaping (Timer) -> Void) -> Timer { + static func pindrop_scheduleRepeating(interval: TimeInterval, block: @escaping @Sendable (Timer) -> Void) -> Timer { let timer = Timer(timeInterval: interval, repeats: true, block: block) RunLoop.main.add(timer, forMode: .common) return timer diff --git a/Pindrop/UI/Main/DashboardView.swift b/Pindrop/UI/Main/DashboardView.swift index 0233322..33f445a 100644 --- a/Pindrop/UI/Main/DashboardView.swift +++ b/Pindrop/UI/Main/DashboardView.swift @@ -7,6 +7,9 @@ import SwiftUI import SwiftData +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif struct DashboardView: View { @Environment(\.modelContext) private var modelContext @@ -29,21 +32,52 @@ struct DashboardView: View { } private var totalSessions: Int { - transcriptions.count + Int(dashboardState.totalSessions) } private var totalWords: Int { - transcriptions.reduce(0) { $0 + $1.text.split(separator: " ").count } + Int(dashboardState.totalWords) } private var totalDuration: TimeInterval { - transcriptions.reduce(0) { $0 + $1.duration } + dashboardState.totalDurationSeconds } private var averageWPM: Double { - guard totalDuration > 0 else { return 0 } + dashboardState.averageWordsPerMinute + } + + private var dashboardState: DashboardViewState { + #if canImport(PindropSharedUIWorkspace) + return DashboardPresenter.shared.present( + records: transcriptions.map { + DashboardRecordSnapshot(text: $0.text, durationSeconds: $0.duration) + }, + currentHour: Int32(Calendar.current.component(.hour, from: Date())), + hasDismissedHotkeyReminder: hasDismissedHotkeyReminder + ) + #else + let totalWords = transcriptions.reduce(0) { $0 + $1.text.split(separator: " ").count } + let totalDuration = transcriptions.reduce(0) { $0 + $1.duration } let minutes = totalDuration / 60 - return Double(totalWords) / max(minutes, 1) + let averageWPM = totalDuration > 0 ? Double(totalWords) / max(minutes, 1) : 0 + let hour = Calendar.current.component(.hour, from: Date()) + let greetingKey: String + switch hour { + case 5..<12: greetingKey = "Good morning" + case 12..<17: greetingKey = "Good afternoon" + case 17..<22: greetingKey = "Good evening" + default: greetingKey = "Good night" + } + return DashboardViewState( + greetingKey: greetingKey, + totalSessions: Int32(transcriptions.count), + totalWords: Int32(totalWords), + totalDurationSeconds: totalDuration, + averageWordsPerMinute: averageWPM, + shouldShowHotkeyReminder: !hasDismissedHotkeyReminder + ) + #endif } var body: some View { @@ -70,7 +104,7 @@ struct DashboardView: View { VStack(alignment: .leading, spacing: AppTheme.Spacing.xxl) { welcomeHeader - if !hasDismissedHotkeyReminder { + if dashboardState.shouldShowHotkeyReminder { hotkeyReminderCard } @@ -104,13 +138,7 @@ struct DashboardView: View { } private var greetingText: String { - let hour = Calendar.current.component(.hour, from: Date()) - switch hour { - case 5..<12: return localized("Good morning", locale: locale) - case 12..<17: return localized("Good afternoon", locale: locale) - case 17..<22: return localized("Good evening", locale: locale) - default: return localized("Good night", locale: locale) - } + localized(dashboardState.greetingKey, locale: locale) } private func statBadge(icon: String, value: String, label: String) -> some View { diff --git a/Pindrop/UI/Main/DictionaryView.swift b/Pindrop/UI/Main/DictionaryView.swift index 4b4a6e3..7420bf7 100644 --- a/Pindrop/UI/Main/DictionaryView.swift +++ b/Pindrop/UI/Main/DictionaryView.swift @@ -8,6 +8,9 @@ import SwiftUI import SwiftData import Foundation +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif enum DictionarySection: String, CaseIterable { case replacements = "Word Replacements" @@ -90,9 +93,63 @@ struct DictionaryView: View { // Hover state @State private var hoveredRowID: UUID? - - private var totalItemCount: Int { - replacements.count + vocabularyWords.count + + private var dictionaryViewState: DictionaryViewState { + #if canImport(PindropSharedUIWorkspace) + return DictionaryPresenter.shared.present( + selectedSection: selectedSection.coreValue, + replacements: replacements.map { + ReplacementEntrySnapshot( + id: $0.id.uuidString, + originals: $0.originals, + replacement: $0.replacement, + sortOrder: Int32($0.sortOrder) + ) + }, + vocabularyWords: vocabularyWords.map { + VocabularyWordSnapshot( + id: $0.id.uuidString, + word: $0.word + ) + }, + primaryInput: primaryInput, + secondaryInput: secondaryInput, + errorMessage: errorMessage + ) + #else + return DictionaryViewState( + selectedSection: selectedSection.coreValue, + totalItemCount: Int32(replacements.count + vocabularyWords.count), + visibleReplacementIds: replacements.sorted(by: { $0.sortOrder < $1.sortOrder }).map { $0.id.uuidString }, + visibleVocabularyIds: vocabularyWords.sorted(by: { $0.word.localizedCaseInsensitiveCompare($1.word) == .orderedAscending }).map { $0.id.uuidString }, + canAdd: { + if selectedSection == .replacements { + return !primaryInput.trimmingCharacters(in: .whitespaces).isEmpty && + !secondaryInput.trimmingCharacters(in: .whitespaces).isEmpty + } + return !primaryInput.trimmingCharacters(in: .whitespaces).isEmpty + }(), + contentStateKind: { + if errorMessage != nil { return .error } + if selectedSection == .replacements ? replacements.isEmpty : vocabularyWords.isEmpty { return .empty } + return .populated + }() + ) + #endif + } + + private var visibleReplacements: [WordReplacement] { + let visibleIDs = Set(dictionaryViewState.visibleReplacementIds) + return replacements + .sorted(by: { $0.sortOrder < $1.sortOrder }) + .filter { visibleIDs.contains($0.id.uuidString) } + } + + private var visibleVocabularyWords: [VocabularyWord] { + let visibleIDs = Set(dictionaryViewState.visibleVocabularyIds) + return vocabularyWords + .sorted(by: { $0.word.localizedCaseInsensitiveCompare($1.word) == .orderedAscending }) + .filter { visibleIDs.contains($0.id.uuidString) } } var body: some View { @@ -139,7 +196,7 @@ struct DictionaryView: View { .font(AppTypography.largeTitle) .foregroundStyle(AppColors.textPrimary) - Text("\(totalItemCount) \(localized("items", locale: locale))") + Text("\(dictionaryViewState.totalItemCount) \(localized("items", locale: locale))") .font(AppTypography.body) .foregroundStyle(AppColors.textSecondary) } @@ -294,19 +351,10 @@ struct DictionaryView: View { .buttonStyle(.plain) .background( RoundedRectangle(cornerRadius: AppTheme.Radius.md) - .fill(canAdd ? AppColors.accent : AppColors.textTertiary.opacity(0.3)) + .fill(dictionaryViewState.canAdd ? AppColors.accent : AppColors.textTertiary.opacity(0.3)) ) .foregroundStyle(.white) - .disabled(!canAdd) - } - } - - private var canAdd: Bool { - if selectedSection == .replacements { - return !primaryInput.trimmingCharacters(in: .whitespaces).isEmpty && - !secondaryInput.trimmingCharacters(in: .whitespaces).isEmpty - } else { - return !primaryInput.trimmingCharacters(in: .whitespaces).isEmpty + .disabled(!dictionaryViewState.canAdd) } } @@ -316,7 +364,7 @@ struct DictionaryView: View { private var contentArea: some View { if let errorMessage = errorMessage { errorView(errorMessage) - } else if isContentEmpty { + } else if dictionaryViewState.contentStateKind == .empty { emptyStateView } else { contentTable @@ -324,15 +372,6 @@ struct DictionaryView: View { } } - private var isContentEmpty: Bool { - switch selectedSection { - case .replacements: - return replacements.isEmpty - case .vocabulary: - return vocabularyWords.isEmpty - } - } - private func errorView(_ message: String) -> some View { VStack(spacing: AppTheme.Spacing.lg) { Image(systemName: "exclamationmark.triangle.fill") @@ -428,7 +467,7 @@ struct DictionaryView: View { ScrollView { LazyVStack(spacing: 0) { - ForEach(replacements.sorted(by: { $0.sortOrder < $1.sortOrder })) { replacement in + ForEach(visibleReplacements) { replacement in ReplacementRow( replacement: replacement, isEditing: editingReplacement?.id == replacement.id, @@ -448,7 +487,7 @@ struct DictionaryView: View { } } - if replacement.id != replacements.last?.id { + if replacement.id != visibleReplacements.last?.id { Divider() .padding(.horizontal, AppTheme.Spacing.md) .background(AppColors.divider) @@ -493,7 +532,7 @@ struct DictionaryView: View { // Rows ScrollView { LazyVStack(spacing: 0) { - ForEach(vocabularyWords.sorted(by: { $0.word < $1.word })) { word in + ForEach(visibleVocabularyWords) { word in VocabularyRow( word: word, isEditing: editingVocabulary?.id == word.id, @@ -512,7 +551,7 @@ struct DictionaryView: View { } } - if word.id != vocabularyWords.last?.id { + if word.id != visibleVocabularyWords.last?.id { Divider() .padding(.horizontal, AppTheme.Spacing.md) .background(AppColors.divider) @@ -995,6 +1034,19 @@ struct VocabularyRow: View { } } +#if canImport(PindropSharedUIWorkspace) +private extension DictionarySection { + var coreValue: DictionarySectionCore { + switch self { + case .replacements: + return .replacements + case .vocabulary: + return .vocabulary + } + } +} +#endif + // MARK: - Flow Layout struct FlowLayout: Layout { diff --git a/Pindrop/UI/Main/HistoryView.swift b/Pindrop/UI/Main/HistoryView.swift index d25404d..d2ed187 100644 --- a/Pindrop/UI/Main/HistoryView.swift +++ b/Pindrop/UI/Main/HistoryView.swift @@ -9,6 +9,9 @@ import SwiftUI import SwiftData import Foundation +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif struct HistoryView: View { private static let topListPadding: CGFloat = 12 @@ -40,9 +43,55 @@ struct HistoryView: View { private var historyStore: HistoryStore { HistoryStore(modelContext: modelContext) } + + private var historyViewState: HistoryViewState { + #if canImport(PindropSharedUIWorkspace) + return HistoryPresenter.shared.present( + records: visibleTranscriptions.map { + HistoryRecordSnapshot( + id: $0.id.uuidString, + timestampEpochMillis: Int64($0.timestamp.timeIntervalSince1970 * 1000) + ) + }, + totalTranscriptionsCount: Int32(totalTranscriptionsCount), + searchText: searchText, + selectedRecordId: selectedRecord?.id.uuidString, + hasLoadedInitialPage: hasLoadedInitialPage, + isLoadingPage: isLoadingPage, + errorMessage: errorMessage, + nowEpochMillis: Int64(Date().timeIntervalSince1970 * 1000), + timeZoneOffsetMinutes: Int32(TimeZone.current.secondsFromGMT() / 60) + ) + #else + return HistoryViewState( + trimmedSearchText: trimmedSearchText, + totalTranscriptionsCount: Int32(totalTranscriptionsCount), + selectedRecordId: selectedRecord?.id.uuidString, + contentStateKind: { + if errorMessage != nil { return .error } + if !hasLoadedInitialPage { return .loading } + if totalTranscriptionsCount == 0 && !trimmedSearchText.isEmpty { return .emptySearch } + if totalTranscriptionsCount == 0 { return .emptyLibrary } + return .populated + }(), + canExport: totalTranscriptionsCount > 0, + shouldShowLoadingMoreIndicator: isLoadingPage && hasLoadedInitialPage && !visibleTranscriptions.isEmpty, + sections: [] + ) + #endif + } /// Group transcriptions by date private var groupedTranscriptions: [(String, [TranscriptionRecord])] { + #if canImport(PindropSharedUIWorkspace) + return historyViewState.sections.compactMap { section in + let records = section.recordIds.compactMap { recordID in + visibleTranscriptions.first { $0.id.uuidString == recordID } + } + guard !records.isEmpty else { return nil } + return (title(for: section), records) + } + #else let calendar = Calendar.current let grouped = Dictionary(grouping: visibleTranscriptions) { record -> String in if calendar.isDateInToday(record.timestamp) { @@ -54,7 +103,6 @@ struct HistoryView: View { } } - // Sort by most recent date first return grouped.sorted { first, second in if first.key == "Today" { return true } if second.key == "Today" { return false } @@ -62,6 +110,7 @@ struct HistoryView: View { if second.key == "Yesterday" { return false } return first.value.first?.timestamp ?? Date() > second.value.first?.timestamp ?? Date() } + #endif } var body: some View { @@ -93,7 +142,7 @@ struct HistoryView: View { .font(AppTypography.largeTitle) .foregroundStyle(AppColors.textPrimary) - Text("\(totalTranscriptionsCount) \(localized("transcriptions", locale: locale))") + Text("\(historyViewState.totalTranscriptionsCount) \(localized("transcriptions", locale: locale))") .font(AppTypography.body) .foregroundStyle(AppColors.textSecondary) } @@ -171,7 +220,7 @@ struct HistoryView: View { .padding(.vertical, AppTheme.Spacing.sm) } .menuStyle(.borderlessButton) - .disabled(totalTranscriptionsCount == 0) + .disabled(!historyViewState.canExport) } // MARK: - Content @@ -180,9 +229,9 @@ struct HistoryView: View { private var contentArea: some View { if let errorMessage = errorMessage { errorView(errorMessage) - } else if !hasLoadedInitialPage { + } else if historyViewState.contentStateKind == .loading { loadingStateView - } else if totalTranscriptionsCount == 0 { + } else if historyViewState.contentStateKind == .emptyLibrary || historyViewState.contentStateKind == .emptySearch { emptyStateView } else { transcriptionsList @@ -226,17 +275,17 @@ struct HistoryView: View { private var emptyStateView: some View { VStack(spacing: AppTheme.Spacing.lg) { - Image(systemName: searchText.isEmpty ? "waveform.badge.mic" : "magnifyingglass") + Image(systemName: historyViewState.contentStateKind == .emptyLibrary ? "waveform.badge.mic" : "magnifyingglass") .font(.system(size: 48)) .foregroundStyle(AppColors.textTertiary) - Text(searchText.isEmpty + Text(historyViewState.contentStateKind == .emptyLibrary ? localized("No transcriptions yet", locale: locale) : localized("No results found", locale: locale)) .font(AppTypography.headline) .foregroundStyle(AppColors.textPrimary) - Text(searchText.isEmpty + Text(historyViewState.contentStateKind == .emptyLibrary ? localized("Start recording to see your transcriptions here", locale: locale) : localized("Try a different search term", locale: locale)) .font(AppTypography.body) @@ -281,7 +330,7 @@ struct HistoryView: View { } } - if isLoadingPage && hasLoadedInitialPage && !visibleTranscriptions.isEmpty { + if historyViewState.shouldShowLoadingMoreIndicator { HStack { Spacer() ProgressView() @@ -338,6 +387,18 @@ struct HistoryView: View { .padding(.vertical, AppTheme.Spacing.sm) .background(AppColors.contentBackground) } + + private func title(for section: HistorySectionState) -> String { + switch section.kind { + case .today: + return localized("Today", locale: locale) + case .yesterday: + return localized("Yesterday", locale: locale) + default: + let date = Date(timeIntervalSince1970: TimeInterval(section.representativeTimestampEpochMillis) / 1000) + return Self.dayFormatter.string(from: date) + } + } // MARK: - Actions diff --git a/Pindrop/UI/Main/MainWindow.swift b/Pindrop/UI/Main/MainWindow.swift index 0b3245d..2ece218 100644 --- a/Pindrop/UI/Main/MainWindow.swift +++ b/Pindrop/UI/Main/MainWindow.swift @@ -8,6 +8,9 @@ import SwiftUI import SwiftData import AppKit +#if canImport(PindropSharedNavigation) +import PindropSharedNavigation +#endif // MARK: - Navigation @@ -33,18 +36,18 @@ enum MainNavItem: String, Identifiable { var id: String { rawValue } func title(locale: Locale) -> String { - localized(rawValue, locale: locale) + localized(titleKey, locale: locale) } var icon: String { switch self { - case .home: return "house.fill" - case .history: return "clock.fill" - case .transcribe: return "waveform" - case .models: return "cpu" - case .notes: return "note.text" - case .dictionary: return "text.book.closed" - case .settings: return "gearshape" + case .home: "house.fill" + case .history: "clock.fill" + case .transcribe: "waveform" + case .models: "cpu" + case .notes: "note.text" + case .dictionary: "text.book.closed" + case .settings: "gearshape" } } @@ -58,6 +61,43 @@ enum MainNavItem: String, Identifiable { } var isComingSoon: Bool { false } + + private var titleKey: String { + rawValue + } + + #if canImport(PindropSharedNavigation) + var coreValue: MainNavigationItem { + switch self { + case .home: .home + case .history: .history + case .transcribe: .transcribe + case .models: .models + case .notes: .notes + case .dictionary: .dictionary + case .settings: .settings + } + } + + init(coreValue: MainNavigationItem) { + switch coreValue { + case .home: + self = .home + case .history: + self = .history + case .transcribe: + self = .transcribe + case .models: + self = .models + case .notes: + self = .notes + case .dictionary: + self = .dictionary + default: + self = .settings + } + } + #endif } // MARK: - Navigation Notification @@ -111,8 +151,12 @@ final class TitlebarlessHostingController: NSHostingController Void)? @@ -123,13 +167,36 @@ struct MainWindow: View { if item == .transcribe { mediaTranscriptionState?.showLibrary() } - + #if canImport(PindropSharedNavigation) + workspaceState = MainWorkspaceNavigator.shared.navigateTo(currentState: workspaceState, item: item.coreValue) + #else selectedNav = item + #endif } private func navigateToSettings(_ tab: SettingsTab) { + #if canImport(PindropSharedNavigation) + workspaceState = MainWorkspaceNavigator.shared.navigateToSettings(currentState: workspaceState, section: tab.coreValue) + #else selectedSettingsTab = tab selectedNav = .settings + #endif + } + + private var currentSelectedNav: MainNavItem { + #if canImport(PindropSharedNavigation) + MainNavItem(coreValue: workspaceState.selectedNavigationItem) + #else + selectedNav + #endif + } + + private var currentSelectedSettingsTab: SettingsTab { + #if canImport(PindropSharedNavigation) + SettingsTab(coreValue: workspaceState.selectedSettingsSection) + #else + selectedSettingsTab + #endif } var body: some View { @@ -165,7 +232,7 @@ struct MainWindow: View { private var sidebarPanel: some View { return MainSidebar( - selectedNav: selectedNav, + selectedNav: currentSelectedNav, onSelect: navigateTo ) .frame(width: AppTheme.Window.sidebarWidth) @@ -200,7 +267,7 @@ struct MainWindow: View { @ViewBuilder private var detailContent: some View { - switch selectedNav { + switch currentSelectedNav { case .home: DashboardView( onOpenHotkeys: { navigateToSettings(.hotkeys) }, @@ -224,20 +291,20 @@ struct MainWindow: View { onOpenModels: { navigateTo(.models) } ) } else { - comingSoonView(for: selectedNav) + comingSoonView(for: currentSelectedNav) } case .models: if let modelManager { ModelsSettingsView(settings: settingsStore, modelManager: modelManager) } else { - comingSoonView(for: selectedNav) + comingSoonView(for: currentSelectedNav) } case .notes: NotesView() case .dictionary: DictionaryView() case .settings: - SettingsContainerView(settings: settingsStore, initialTab: selectedSettingsTab) + SettingsContainerView(settings: settingsStore, initialTab: currentSelectedSettingsTab) } } diff --git a/Pindrop/UI/Main/NotesView.swift b/Pindrop/UI/Main/NotesView.swift index 781c52f..bc7191d 100644 --- a/Pindrop/UI/Main/NotesView.swift +++ b/Pindrop/UI/Main/NotesView.swift @@ -8,6 +8,9 @@ import SwiftUI import SwiftData import Foundation +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif struct NotesView: View { @Environment(\.modelContext) private var modelContext @@ -23,27 +26,60 @@ struct NotesView: View { private var notesStore: NotesStore { NotesStore(modelContext: modelContext) } - + + private var notesViewState: NotesViewState { + #if canImport(PindropSharedUIWorkspace) + return NotesPresenter.shared.present( + notes: notes.map { + NoteSnapshot( + id: $0.id.uuidString, + title: $0.title, + content: $0.content, + tags: $0.tags, + updatedAtEpochMillis: Int64($0.updatedAt.timeIntervalSince1970 * 1000) + ) + }, + searchText: searchText, + sortOrder: sortOrder.coreValue, + selectedNoteId: selectedNote?.id.uuidString, + errorMessage: errorMessage + ) + #else + let filtered = filteredNotesFallback + return NotesViewState( + trimmedSearchText: searchText.trimmingCharacters(in: .whitespacesAndNewlines), + sortOrder: sortOrder.coreValue, + selectedNoteId: selectedNote?.id.uuidString, + visibleNoteIds: filtered.map { $0.id.uuidString }, + totalVisibleCount: Int32(filtered.count), + contentStateKind: { + if errorMessage != nil { return .error } + if filtered.isEmpty && !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return .emptySearch } + if filtered.isEmpty { return .emptyLibrary } + return .populated + }() + ) + #endif + } + private var filteredNotes: [NoteSchema.Note] { - let sorted = sortNotes(notes) - - if searchText.isEmpty { - return sorted - } else { - return sorted.filter { note in - note.title.localizedStandardContains(searchText) || - note.content.localizedStandardContains(searchText) || - note.tags.contains { $0.localizedStandardContains(searchText) } - } - } + let visibleIDs = Set(notesViewState.visibleNoteIds) + let sorted = filteredNotesFallback + return sorted.filter { visibleIDs.contains($0.id.uuidString) } } - - private func sortNotes(_ notes: [NoteSchema.Note]) -> [NoteSchema.Note] { - switch sortOrder { - case .ascending: - return notes.sorted { $0.updatedAt < $1.updatedAt } - case .descending: - return notes.sorted { $0.updatedAt > $1.updatedAt } + + private var filteredNotesFallback: [NoteSchema.Note] { + let sorted = switch sortOrder { + case .ascending: notes.sorted { $0.updatedAt < $1.updatedAt } + case .descending: notes.sorted { $0.updatedAt > $1.updatedAt } + } + + let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedSearchText.isEmpty else { return sorted } + return sorted.filter { note in + note.title.localizedStandardContains(trimmedSearchText) || + note.content.localizedStandardContains(trimmedSearchText) || + note.tags.contains { $0.localizedStandardContains(trimmedSearchText) } } } @@ -73,7 +109,7 @@ struct NotesView: View { .font(AppTypography.largeTitle) .foregroundStyle(AppColors.textPrimary) - Text("\(filteredNotes.count) \(localized("notes", locale: locale))") + Text("\(notesViewState.totalVisibleCount) \(localized("notes", locale: locale))") .font(AppTypography.body) .foregroundStyle(AppColors.textSecondary) } @@ -153,7 +189,7 @@ struct NotesView: View { private var contentArea: some View { if let errorMessage = errorMessage { errorView(errorMessage) - } else if filteredNotes.isEmpty { + } else if notesViewState.contentStateKind == .emptyLibrary || notesViewState.contentStateKind == .emptySearch { emptyStateView } else { notesGrid @@ -185,23 +221,23 @@ struct NotesView: View { private var emptyStateView: some View { VStack(spacing: AppTheme.Spacing.lg) { - Image(systemName: searchText.isEmpty ? "note.text" : "magnifyingglass") + Image(systemName: notesViewState.contentStateKind == .emptyLibrary ? "note.text" : "magnifyingglass") .font(.system(size: 48)) .foregroundStyle(AppColors.textTertiary) - Text(searchText.isEmpty + Text(notesViewState.contentStateKind == .emptyLibrary ? localized("No notes yet", locale: locale) : localized("No results found", locale: locale)) .font(AppTypography.headline) .foregroundStyle(AppColors.textPrimary) - Text(searchText.isEmpty + Text(notesViewState.contentStateKind == .emptyLibrary ? localized("Create your first note to get started", locale: locale) : localized("Try a different search term", locale: locale)) .font(AppTypography.body) .foregroundStyle(AppColors.textSecondary) - if searchText.isEmpty { + if notesViewState.contentStateKind == .emptyLibrary { Button(localized("Create New Note", locale: locale)) { createNewNote() } @@ -285,6 +321,19 @@ enum SortOrder { case descending } +#if canImport(PindropSharedUIWorkspace) +private extension SortOrder { + var coreValue: NotesSortOrderCore { + switch self { + case .ascending: + return .ascending + case .descending: + return .descending + } + } +} +#endif + // MARK: - Preview #Preview("Notes View - With Data") { diff --git a/Pindrop/UI/Main/TranscribeView.swift b/Pindrop/UI/Main/TranscribeView.swift index e88e060..e355efb 100644 --- a/Pindrop/UI/Main/TranscribeView.swift +++ b/Pindrop/UI/Main/TranscribeView.swift @@ -13,6 +13,9 @@ import Observation import SwiftData import SwiftUI import UniformTypeIdentifiers +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif struct TranscribeView: View { @Environment(\.displayScale) private var displayScale @@ -57,12 +60,21 @@ struct TranscribeView: View { } private var visibleFolders: [MediaFolder] { + #if canImport(PindropSharedUIWorkspace) + let visibleIDs = Set(libraryBrowseState.visibleFolderIds) + return folders.filter { visibleIDs.contains($0.id.uuidString) } + #else guard selectedFolder == nil else { return [] } guard !trimmedSearchText.isEmpty else { return folders } return folders.filter { $0.name.localizedStandardContains(trimmedSearchText) } + #endif } private var visibleMediaRecords: [TranscriptionRecord] { + #if canImport(PindropSharedUIWorkspace) + let visibleIDs = Set(libraryBrowseState.visibleRecordIds) + return mediaRecords.filter { visibleIDs.contains($0.id.uuidString) } + #else let filteredRecords = mediaRecords .filter { record in guard let selectedFolder else { return record.folder == nil } @@ -73,21 +85,89 @@ struct TranscribeView: View { } return sort(records: filteredRecords) + #endif + } + + private var libraryBrowseState: MediaLibraryBrowseState { + #if canImport(PindropSharedUIWorkspace) + return MediaLibraryPresenter.shared.browse( + folders: folders.map { folder in + MediaFolderSnapshot( + id: folder.id.uuidString, + name: folder.name, + itemCount: Int32(mediaRecords.filter { $0.folder?.id == folder.id }.count) + ) + }, + records: mediaRecords.map { record in + MediaRecordSnapshot( + id: record.id.uuidString, + folderId: record.folder?.id.uuidString, + timestampEpochMillis: Int64(record.timestamp.timeIntervalSince1970 * 1000), + searchText: [record.text, record.originalText, record.sourceDisplayName, record.originalSourceURL] + .compactMap { $0 } + .joined(separator: "\n"), + sortName: record.mediaLibrarySortName + ) + }, + selectedFolderId: featureState.selectedFolderID?.uuidString, + searchText: featureState.librarySearchText, + sortMode: featureState.librarySortMode.coreValue + ) + #else + return MediaLibraryBrowseState( + trimmedSearchText: trimmedSearchText, + selectedFolderId: selectedFolder?.id.uuidString, + visibleFolderIds: visibleFolders.map(\.id.uuidString), + visibleRecordIds: visibleMediaRecords.map(\.id.uuidString), + filteredFolderCount: Int32(visibleFolders.count), + filteredRecordCount: Int32(visibleMediaRecords.count), + totalRecordCountForSelectedFolder: Int32(mediaRecords.filter { $0.folder?.id == selectedFolder?.id }.count), + emptyStateKind: visibleFolders.isEmpty && visibleMediaRecords.isEmpty ? .libraryEmpty : .none + ) + #endif + } + + private var transcribeLibraryViewState: TranscribeLibraryViewState { + #if canImport(PindropSharedUIWorkspace) + return TranscribeLibraryPresenter.shared.present( + selectedFolderId: selectedFolder?.id.uuidString, + selectedFolderName: selectedFolder?.name, + draftLink: featureState.draftLink, + librarySearchText: featureState.librarySearchText, + browseState: libraryBrowseState + ) + #else + return TranscribeLibraryViewState( + selectedFolderId: selectedFolder?.id.uuidString, + selectedFolderName: selectedFolder?.name, + trimmedSearchText: trimmedSearchText, + filteredFolderCount: libraryBrowseState.filteredFolderCount, + filteredRecordCount: libraryBrowseState.filteredRecordCount, + totalRecordCountForSelectedFolder: libraryBrowseState.totalRecordCountForSelectedFolder, + shouldShowBackButton: selectedFolder != nil, + canSubmitDraftLink: !featureState.draftLink.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, + shouldShowDraftLinkClearButton: !featureState.draftLink.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, + shouldShowLibraryEmptyState: libraryBrowseState.emptyStateKind != .none, + emptyStateTitleKey: "No results found", + emptyStateMessageKey: "Try a different search term.", + emptyStateIconName: trimmedSearchText.isEmpty ? "folder.badge.questionmark" : "magnifyingglass" + ) + #endif } private var totalLibraryCountText: String { if let selectedFolder { return localized("%d items in %@", locale: locale) - .replacingOccurrences(of: "%d", with: "\(visibleMediaRecords.count)") + .replacingOccurrences(of: "%d", with: "\(libraryBrowseState.filteredRecordCount)") .replacingOccurrences(of: "%@", with: selectedFolder.name) } - let folderCountLabel = visibleFolders.count == 1 + let folderCountLabel = libraryBrowseState.filteredFolderCount == 1 ? localized("1 folder", locale: locale) - : localized("%d folders", locale: locale).replacingOccurrences(of: "%d", with: "\(visibleFolders.count)") - let transcriptCountLabel = visibleMediaRecords.count == 1 + : localized("%d folders", locale: locale).replacingOccurrences(of: "%d", with: "\(libraryBrowseState.filteredFolderCount)") + let transcriptCountLabel = libraryBrowseState.filteredRecordCount == 1 ? localized("1 transcription", locale: locale) - : localized("%d transcriptions", locale: locale).replacingOccurrences(of: "%d", with: "\(visibleMediaRecords.count)") + : localized("%d transcriptions", locale: locale).replacingOccurrences(of: "%d", with: "\(libraryBrowseState.filteredRecordCount)") return "\(folderCountLabel) • \(transcriptCountLabel)" } @@ -335,7 +415,7 @@ struct TranscribeView: View { submitCurrentLink() } .buttonStyle(.borderless) - .disabled(featureState.draftLink.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .disabled(!transcribeLibraryViewState.canSubmitDraftLink) } .padding(.horizontal, AppTheme.Spacing.md) .padding(.vertical, AppTheme.Spacing.md) @@ -371,7 +451,7 @@ struct TranscribeView: View { HStack { VStack(alignment: .leading, spacing: AppTheme.Spacing.xs) { HStack(spacing: AppTheme.Spacing.sm) { - if selectedFolder != nil { + if transcribeLibraryViewState.shouldShowBackButton { Button { featureState.clearSelectedFolder() } label: { @@ -395,9 +475,9 @@ struct TranscribeView: View { libraryControls - if visibleFolders.isEmpty && visibleMediaRecords.isEmpty { + if transcribeLibraryViewState.shouldShowLibraryEmptyState { VStack(spacing: AppTheme.Spacing.md) { - Image(systemName: trimmedSearchText.isEmpty ? "folder.badge.questionmark" : "magnifyingglass") + Image(systemName: transcribeLibraryViewState.emptyStateIconName) .font(.system(size: 36)) .foregroundStyle(AppColors.textTertiary) Text(emptyLibraryTitle) @@ -709,48 +789,15 @@ struct TranscribeView: View { } private var emptyLibraryTitle: String { - if let selectedFolder { - return trimmedSearchText.isEmpty - ? localized("No items in %@", locale: locale).replacingOccurrences(of: "%@", with: selectedFolder.name) - : localized("No results found", locale: locale) + let key = transcribeLibraryViewState.emptyStateTitleKey + if key.contains("%@") { + return localized(key, locale: locale).replacingOccurrences(of: "%@", with: transcribeLibraryViewState.selectedFolderName ?? "") } - - if trimmedSearchText.isEmpty { - return localized("No media transcriptions yet", locale: locale) - } - - return localized("No results found", locale: locale) + return localized(key, locale: locale) } private var emptyLibraryMessage: String { - if selectedFolder != nil { - return trimmedSearchText.isEmpty - ? localized("Import or transcribe media while this folder is selected to save items here.", locale: locale) - : localized("Try a different search term in this folder.", locale: locale) - } - - if trimmedSearchText.isEmpty { - return localized("Imported files and web links will appear here once processing completes.", locale: locale) - } - - return localized("Try a different search term.", locale: locale) - } - - private func sort(records: [TranscriptionRecord]) -> [TranscriptionRecord] { - switch featureState.librarySortMode { - case .newest: - return records.sorted { $0.timestamp > $1.timestamp } - case .oldest: - return records.sorted { $0.timestamp < $1.timestamp } - case .nameAscending: - return records.sorted { - $0.mediaLibrarySortName.localizedStandardCompare($1.mediaLibrarySortName) == .orderedAscending - } - case .nameDescending: - return records.sorted { - $0.mediaLibrarySortName.localizedStandardCompare($1.mediaLibrarySortName) == .orderedDescending - } - } + localized(transcribeLibraryViewState.emptyStateMessageKey, locale: locale) } private func saveFolder(mode: FolderSheetMode, name: String) throws { diff --git a/Pindrop/UI/Onboarding/AIEnhancementStepView.swift b/Pindrop/UI/Onboarding/AIEnhancementStepView.swift index df77f28..d3ce81f 100644 --- a/Pindrop/UI/Onboarding/AIEnhancementStepView.swift +++ b/Pindrop/UI/Onboarding/AIEnhancementStepView.swift @@ -7,6 +7,9 @@ import Foundation import SwiftUI +#if canImport(PindropSharedSettings) +import PindropSharedSettings +#endif enum AIProvider: String, CaseIterable, Identifiable { case openai = "OpenAI" @@ -18,12 +21,16 @@ enum AIProvider: String, CaseIterable, Identifiable { var id: String { rawValue } var displayName: String { + #if canImport(PindropSharedSettings) + return AISettingsCatalog.shared.provider(id: coreValue).displayName + #else switch self { case .custom: return "Custom/Local" default: return rawValue } + #endif } var icon: Icon { @@ -37,6 +44,9 @@ enum AIProvider: String, CaseIterable, Identifiable { } var defaultEndpoint: String { + #if canImport(PindropSharedSettings) + return AISettingsCatalog.shared.provider(id: coreValue).defaultEndpoint + #else switch self { case .openai: return "https://api.openai.com/v1/chat/completions" case .google: return "https://generativelanguage.googleapis.com/v1beta" @@ -44,9 +54,13 @@ enum AIProvider: String, CaseIterable, Identifiable { case .openrouter: return "https://openrouter.ai/api/v1/chat/completions" case .custom: return "" } + #endif } var apiKeyPlaceholder: String { + #if canImport(PindropSharedSettings) + return AISettingsCatalog.shared.provider(id: coreValue).apiKeyPlaceholder + #else switch self { case .openai: return "sk-..." case .google: return "AIza..." @@ -54,14 +68,41 @@ enum AIProvider: String, CaseIterable, Identifiable { case .openrouter: return "sk-or-..." case .custom: return "Enter API key" } + #endif } var isImplemented: Bool { + #if canImport(PindropSharedSettings) + return AISettingsCatalog.shared.provider(id: coreValue).isImplemented + #else switch self { case .openai, .openrouter, .custom, .anthropic: return true default: return false } + #endif + } + + #if canImport(PindropSharedSettings) + var coreValue: AIProviderCore { + switch self { + case .openai: .openai + case .google: .google + case .anthropic: .anthropic + case .openrouter: .openrouter + case .custom: .custom + } + } + + init(coreValue: AIProviderCore) { + switch coreValue { + case .openai: self = .openai + case .google: self = .google + case .anthropic: self = .anthropic + case .openrouter: self = .openrouter + default: self = .custom + } } + #endif } enum CustomProviderType: String, CaseIterable, Identifiable { @@ -81,6 +122,9 @@ enum CustomProviderType: String, CaseIterable, Identifiable { } var storageKey: String { + #if canImport(PindropSharedSettings) + return AISettingsCatalog.shared.customProvider(id: coreValue).storageKey + #else switch self { case .custom: return "custom" @@ -89,17 +133,29 @@ enum CustomProviderType: String, CaseIterable, Identifiable { case .lmStudio: return "lm-studio" } + #endif } var requiresAPIKey: Bool { + #if canImport(PindropSharedSettings) + AISettingsCatalog.shared.customProvider(id: coreValue).requiresApiKey + #else self == .custom + #endif } var supportsModelListing: Bool { + #if canImport(PindropSharedSettings) + AISettingsCatalog.shared.customProvider(id: coreValue).supportsModelListing + #else self != .custom + #endif } var defaultEndpoint: String { + #if canImport(PindropSharedSettings) + AISettingsCatalog.shared.customProvider(id: coreValue).defaultEndpoint + #else switch self { case .custom: return "" @@ -108,9 +164,13 @@ enum CustomProviderType: String, CaseIterable, Identifiable { case .lmStudio: return "http://localhost:1234/v1/chat/completions" } + #endif } var defaultModelsEndpoint: String? { + #if canImport(PindropSharedSettings) + AISettingsCatalog.shared.customProvider(id: coreValue).defaultModelsEndpoint + #else switch self { case .custom: return nil @@ -119,9 +179,13 @@ enum CustomProviderType: String, CaseIterable, Identifiable { case .lmStudio: return "http://localhost:1234/v1/models" } + #endif } var apiKeyPlaceholder: String { + #if canImport(PindropSharedSettings) + AISettingsCatalog.shared.customProvider(id: coreValue).apiKeyPlaceholder + #else switch self { case .custom: return "Enter API key" @@ -130,9 +194,13 @@ enum CustomProviderType: String, CaseIterable, Identifiable { case .lmStudio: return "Optional unless auth is enabled" } + #endif } var endpointPlaceholder: String { + #if canImport(PindropSharedSettings) + AISettingsCatalog.shared.customProvider(id: coreValue).endpointPlaceholder + #else switch self { case .custom: return "https://your-api.com/v1/chat/completions" @@ -141,9 +209,13 @@ enum CustomProviderType: String, CaseIterable, Identifiable { case .lmStudio: return defaultEndpoint } + #endif } var modelPlaceholder: String { + #if canImport(PindropSharedSettings) + AISettingsCatalog.shared.customProvider(id: coreValue).modelPlaceholder + #else switch self { case .custom: return "e.g., gpt-4o" @@ -152,7 +224,26 @@ enum CustomProviderType: String, CaseIterable, Identifiable { case .lmStudio: return "e.g., local-model" } + #endif + } + + #if canImport(PindropSharedSettings) + var coreValue: CustomProviderTypeCore { + switch self { + case .custom: .custom + case .ollama: .ollama + case .lmStudio: .lmStudio + } + } + + init(coreValue: CustomProviderTypeCore) { + switch coreValue { + case .custom: self = .custom + case .ollama: self = .ollama + default: self = .lmStudio + } } + #endif } struct AIEnhancementStepView: View { diff --git a/Pindrop/UI/Settings/AIEnhancementSettingsView.swift b/Pindrop/UI/Settings/AIEnhancementSettingsView.swift index 43f737a..56c5f13 100644 --- a/Pindrop/UI/Settings/AIEnhancementSettingsView.swift +++ b/Pindrop/UI/Settings/AIEnhancementSettingsView.swift @@ -7,6 +7,9 @@ import SwiftData import SwiftUI +#if canImport(PindropSharedSettings) +import PindropSharedSettings +#endif struct AIEnhancementSettingsView: View { @ObservedObject var settings: SettingsStore @@ -45,6 +48,93 @@ struct AIEnhancementSettingsView: View { PromptPresetStore(modelContext: modelContext) } + #if canImport(PindropSharedSettings) + private var aiEnhancementViewState: AIEnhancementViewState { + AIEnhancementPresenter.shared.present( + draft: AIEnhancementDraft( + selectedProvider: selectedProvider.coreValue, + selectedCustomProvider: selectedCustomProvider.coreValue, + apiKey: apiKey, + selectedModel: selectedModel, + customModel: customModel, + enhancementPrompt: enhancementPrompt, + noteEnhancementPrompt: noteEnhancementPrompt, + selectedPromptType: selectedPromptType.coreValue, + selectedPresetId: settings.selectedPresetId, + customEndpointText: currentCustomEndpointText, + availableModels: availableModels.map { + AIModelSnapshot(id: $0.id, name: $0.name, summary: $0.description) + }, + modelErrorMessage: modelError, + isLoadingModels: isLoadingModels, + aiEnhancementEnabled: settings.aiEnhancementEnabled + ), + presets: presets.map { + PromptPresetSnapshot( + id: $0.id.uuidString, + name: $0.name, + prompt: $0.prompt, + isBuiltIn: $0.isBuiltIn, + sortOrder: Int32($0.sortOrder) + ) + } + ) + } + #endif + + private var validatedSelectedPresetId: String? { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.validatedPresetId + #else + guard let presetId = settings.selectedPresetId, + presets.contains(where: { $0.id.uuidString == presetId }) + else { + return nil + } + return presetId + #endif + } + + private var shouldShowCustomProviderPicker: Bool { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.shouldShowCustomProviderPicker + #else + selectedProvider == .custom + #endif + } + + private var shouldShowPrimaryModelPicker: Bool { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.shouldShowModelPicker && selectedProvider != .custom + #else + selectedProvider == .openrouter || selectedProvider == .openai || selectedProvider == .anthropic + #endif + } + + private var shouldShowCustomModelPicker: Bool { + #if canImport(PindropSharedSettings) + shouldShowCustomProviderPicker && aiEnhancementViewState.shouldShowModelPicker + #else + selectedProvider == .custom && selectedCustomProvider.supportsModelListing + #endif + } + + private var shouldShowCustomModelField: Bool { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.shouldShowCustomModelField + #else + selectedProvider == .custom && !selectedCustomProvider.supportsModelListing + #endif + } + + private var shouldShowCustomEndpointField: Bool { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.shouldShowCustomEndpointField + #else + selectedProvider == .custom + #endif + } + enum PromptType: String, CaseIterable, Identifiable { case transcription = "Transcription" case notes = "Notes" @@ -195,14 +285,7 @@ struct AIEnhancementSettingsView: View { private var validatedPresetSelection: Binding { Binding( - get: { - guard let presetId = settings.selectedPresetId, - presets.contains(where: { $0.id.uuidString == presetId }) - else { - return nil - } - return presetId - }, + get: { validatedSelectedPresetId }, set: { settings.selectedPresetId = $0 } ) } @@ -396,11 +479,15 @@ struct AIEnhancementSettingsView: View { private var promptContent: some View { let currentPrompt = selectedPromptType == .transcription ? $enhancementPrompt : $noteEnhancementPrompt + #if canImport(PindropSharedSettings) + let charCount = aiEnhancementViewState.selectedPromptCharacterCount + let isReadOnly = aiEnhancementViewState.isSelectedPromptReadOnly + #else let charCount = selectedPromptType == .transcription ? enhancementPrompt.count : noteEnhancementPrompt.count - let isReadOnly = selectedPromptType == .transcription && isBuiltInPresetSelected + #endif VStack(alignment: .leading, spacing: 12) { TextEditor(text: currentPrompt) @@ -457,10 +544,14 @@ struct AIEnhancementSettingsView: View { } private var isBuiltInPresetSelected: Bool { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.isBuiltInPresetSelected + #else guard let id = settings.selectedPresetId, let preset = presets.first(where: { $0.id.uuidString == id }) else { return false } return preset.isBuiltIn + #endif } private func resetCurrentPrompt() { @@ -523,23 +614,26 @@ struct AIEnhancementSettingsView: View { comingSoonView } else { VStack(spacing: 16) { - if selectedProvider == .custom { + if shouldShowCustomProviderPicker { customProviderPicker } apiKeyField - if selectedProvider == .openrouter || selectedProvider == .openai || selectedProvider == .anthropic { + if shouldShowPrimaryModelPicker { modelPicker } - if selectedProvider == .custom { - if selectedCustomProvider.supportsModelListing { + if shouldShowCustomProviderPicker { + if shouldShowCustomModelPicker { modelPicker - } else { + } + if shouldShowCustomModelField { customModelField } - customEndpointField + if shouldShowCustomEndpointField { + customEndpointField + } } saveButton @@ -730,21 +824,25 @@ struct AIEnhancementSettingsView: View { private var emptyModelsMessage: String { + #if canImport(PindropSharedSettings) + return localized(aiEnhancementViewState.emptyModelsMessageKey, locale: locale) + #else if isLoadingModels { return localized("Loading models...", locale: locale) } - if selectedProvider == .openai, - apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - { - return localized("Enter an OpenAI API key to load models.", locale: locale) - } - if modelError != nil { - return localized("Unable to load models. Try refresh.", locale: locale) - } - if selectedProvider == .custom && selectedCustomProvider.supportsModelListing { - return localized("No models available. Try Refresh or enter a model ID manually.", locale: locale) - } - return localized("No models available.", locale: locale) + if selectedProvider == .openai, + apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + { + return localized("Enter an OpenAI API key to load models.", locale: locale) + } + if modelError != nil { + return localized("Unable to load models. Try refresh.", locale: locale) + } + if selectedProvider == .custom && selectedCustomProvider.supportsModelListing { + return localized("No models available. Try Refresh or enter a model ID manually.", locale: locale) + } + return localized("No models available.", locale: locale) + #endif } private var saveButton: some View { @@ -796,14 +894,25 @@ struct AIEnhancementSettingsView: View { } private var isAPIKeyOptional: Bool { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.isApiKeyOptional + #else selectedProvider == .custom && !selectedCustomProvider.requiresAPIKey + #endif } private var currentAPIKeyPlaceholder: String { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.currentApiKeyPlaceholder + #else selectedProvider == .custom ? selectedCustomProvider.apiKeyPlaceholder : selectedProvider.apiKeyPlaceholder + #endif } private var apiKeyHelpText: String? { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.apiKeyHelpText + #else guard selectedProvider == .custom else { return nil } switch selectedCustomProvider { @@ -814,6 +923,7 @@ struct AIEnhancementSettingsView: View { case .lmStudio: return "LM Studio only needs a token if local server authentication is enabled." } + #endif } private func applyCustomEndpointDefault(forceReset: Bool = false) { @@ -1014,6 +1124,9 @@ struct AIEnhancementSettingsView: View { // MARK: - Logic private var canSave: Bool { + #if canImport(PindropSharedSettings) + aiEnhancementViewState.canSave + #else guard selectedProvider.isImplemented else { return false } if settings.requiresAPIKey(for: selectedProvider, customLocalProvider: selectedCustomProvider) && apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty @@ -1031,6 +1144,7 @@ struct AIEnhancementSettingsView: View { return false } return true + #endif } private func loadCredentialsAndPrompt() { @@ -1040,13 +1154,22 @@ struct AIEnhancementSettingsView: View { loadCustomEndpointDrafts() + #if canImport(PindropSharedSettings) + let inferredSelection = AISettingsCatalog.shared.inferProviderSelection( + endpoint: settings.apiEndpoint, + fallbackCustomProvider: settings.currentCustomLocalProvider.coreValue, + currentProviderIsCustom: settings.currentAIProvider == .custom + ) + selectedProvider = AIProvider(coreValue: inferredSelection.provider) + selectedCustomProvider = CustomProviderType(coreValue: inferredSelection.customProvider) + #else if let endpoint = settings.apiEndpoint { if endpoint.contains("openai.com") { selectedProvider = .openai - } else if endpoint.contains("anthropic.com") { - selectedProvider = .anthropic - } else if endpoint.contains("googleapis.com") { - selectedProvider = .google + } else if endpoint.contains("anthropic.com") { + selectedProvider = .anthropic + } else if endpoint.contains("googleapis.com") { + selectedProvider = .google } else if endpoint.contains("openrouter.ai") { selectedProvider = .openrouter } else if !endpoint.isEmpty { @@ -1056,6 +1179,7 @@ struct AIEnhancementSettingsView: View { } else if settings.currentAIProvider == .custom { selectedProvider = .custom } + #endif customModel = loadedModel apiKey = settings.loadAPIKey( for: selectedProvider, @@ -1277,3 +1401,14 @@ extension AIModelService.AIModel: SearchableDropdownItem { [name, id, description].compactMap { $0 } } } + +#if canImport(PindropSharedSettings) +private extension AIEnhancementSettingsView.PromptType { + var coreValue: PromptTypeCore { + switch self { + case .transcription: .transcription + case .notes: .notes + } + } +} +#endif diff --git a/Pindrop/UI/Settings/ModelsSettingsView.swift b/Pindrop/UI/Settings/ModelsSettingsView.swift index 63cc0ae..fc6d2ac 100644 --- a/Pindrop/UI/Settings/ModelsSettingsView.swift +++ b/Pindrop/UI/Settings/ModelsSettingsView.swift @@ -6,6 +6,9 @@ // import SwiftUI +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif private let modelListItemInset: CGFloat = 6 @@ -19,8 +22,6 @@ struct ModelsSettingsView: View { @State private var errorMessage: String? @State private var selectedFilter: ModelFilter = .recommended @State private var searchText = "" - @State private var visibleModels: [ModelManager.WhisperModel] = [] - @State private var searchTask: Task? enum ModelFilter: String, CaseIterable { case recommended @@ -55,15 +56,6 @@ struct ModelsSettingsView: View { } } - private var filteredModels: [ModelManager.WhisperModel] { - switch effectiveFilter { - case .recommended: - return recommendedModels - case .all, .local, .cloud, .comingSoon: - return modelManager.availableModels.filter { effectiveFilter.matches($0) } - } - } - private var recommendedModels: [ModelManager.WhisperModel] { modelManager.recommendedModels(for: settings.selectedAppLanguage) } @@ -72,12 +64,62 @@ struct ModelsSettingsView: View { Set(recommendedModels.map(\.name)) } + private var browseState: ModelsBrowseState { + #if canImport(PindropSharedUIWorkspace) + return ModelsPresenter.shared.browse( + models: modelManager.availableModels.map { model in + ModelCatalogEntrySnapshot( + id: model.id, + name: model.name, + displayName: model.displayName, + description: model.description, + providerName: model.provider.rawValue, + isLocal: model.provider.isLocal, + isRecommended: recommendedModelNameSet.contains(model.name), + availability: model.availability.coreValue + ) + }, + selectedFilter: selectedFilter.coreValue, + searchText: searchText + ) + #else + return ModelsBrowseState( + trimmedSearchText: searchText.trimmingCharacters(in: .whitespacesAndNewlines), + selectedFilter: selectedFilter.coreValue, + effectiveFilter: searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? selectedFilter.coreValue : .all, + visibleModelIds: filteredModelsFallback.map(\.id), + contentStateKind: filteredModelsFallback.isEmpty + ? (searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .emptyLibrary : .emptySearch) + : .populated + ) + #endif + } + + private var filteredModelsFallback: [ModelManager.WhisperModel] { + let effectiveFilter = searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? selectedFilter : .all + let filteredModels: [ModelManager.WhisperModel] + switch effectiveFilter { + case .recommended: + filteredModels = recommendedModels + case .all, .local, .cloud, .comingSoon: + filteredModels = modelManager.availableModels.filter { effectiveFilter.matches($0) } + } + let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !query.isEmpty else { return filteredModels } + return filteredModels.filter { $0.matchesSearch(query) } + } + + private var visibleModels: [ModelManager.WhisperModel] { + let visibleIDs = Set(browseState.visibleModelIds) + return modelManager.availableModels.filter { visibleIDs.contains($0.id) } + } + private var effectiveFilter: ModelFilter { - trimmedSearchText.isEmpty ? selectedFilter : .all + ModelFilter(coreValue: browseState.effectiveFilter) } private var trimmedSearchText: String { - searchText.trimmingCharacters(in: .whitespacesAndNewlines) + browseState.trimmedSearchText } var body: some View { @@ -100,23 +142,12 @@ struct ModelsSettingsView: View { .task { await modelManager.refreshDownloadedModels() await modelManager.refreshDownloadedFeatureModels() - updateVisibleModels(immediately: true) } .onAppear { if activeModelName == nil { activeModelName = settings.selectedModel } NotificationCenter.default.post(name: .requestActiveModel, object: nil) - updateVisibleModels(immediately: true) - } - .onChange(of: selectedFilter) { _, _ in - updateVisibleModels(immediately: trimmedSearchText.isEmpty) - } - .onChange(of: searchText) { _, _ in - updateVisibleModels(immediately: trimmedSearchText.isEmpty) - } - .onChange(of: settings.selectedLanguage) { _, _ in - updateVisibleModels(immediately: true) } .onReceive(NotificationCenter.default.publisher(for: .modelActiveChanged)) { notification in if let modelName = notification.userInfo?["modelName"] as? String { @@ -124,9 +155,6 @@ struct ModelsSettingsView: View { switchingToModel = nil } } - .onDisappear { - searchTask?.cancel() - } } private var header: some View { @@ -387,25 +415,6 @@ struct ModelsSettingsView: View { } } - private func updateVisibleModels(immediately: Bool = false) { - searchTask?.cancel() - - let models = filteredModels - let query = trimmedSearchText - - searchTask = Task { - if !immediately && !query.isEmpty { - try? await Task.sleep(for: .milliseconds(120)) - } - - let filtered = await Task.detached(priority: .userInitiated) { - filterModels(models, matching: query) - }.value - - guard !Task.isCancelled else { return } - visibleModels = filtered - } - } } struct FilterButton: View { @@ -904,13 +913,52 @@ private extension ModelManager.WhisperModel { } } -private func filterModels( - _ models: [ModelManager.WhisperModel], - matching query: String -) -> [ModelManager.WhisperModel] { - guard !query.isEmpty else { return models } - return models.filter { $0.matchesSearch(query) } +#if canImport(PindropSharedUIWorkspace) +private extension ModelsSettingsView.ModelFilter { + var coreValue: ModelsFilterCore { + switch self { + case .recommended: + return .recommended + case .local: + return .local + case .cloud: + return .cloud + case .comingSoon: + return .comingSoon + case .all: + return .all + } + } + + init(coreValue: ModelsFilterCore) { + switch coreValue { + case .recommended: + self = .recommended + case .local: + self = .local + case .cloud: + self = .cloud + case .comingSoon: + self = .comingSoon + default: + self = .all + } + } +} + +private extension ModelManager.ModelAvailability { + var coreValue: String { + switch self { + case .available: + return "available" + case .comingSoon: + return "comingSoon" + case .requiresSetup: + return "requiresSetup" + } + } } +#endif #Preview { ModelsSettingsView(settings: SettingsStore(), modelManager: ModelManager()) diff --git a/Pindrop/UI/Settings/PresetManagementSheet.swift b/Pindrop/UI/Settings/PresetManagementSheet.swift index 2d09d87..f8f9ee3 100644 --- a/Pindrop/UI/Settings/PresetManagementSheet.swift +++ b/Pindrop/UI/Settings/PresetManagementSheet.swift @@ -7,6 +7,9 @@ import SwiftUI import SwiftData +#if canImport(PindropSharedSettings) +import PindropSharedSettings +#endif struct PresetManagementSheet: View { @Environment(\.modelContext) private var modelContext @@ -33,13 +36,48 @@ struct PresetManagementSheet: View { @State private var hoveredRowID: UUID? private var builtInPresets: [PromptPreset] { + #if canImport(PindropSharedSettings) + let visibleIDs = Set(presetManagementState.builtInPresetIds) + return presets + .sorted(by: { $0.sortOrder < $1.sortOrder }) + .filter { visibleIDs.contains($0.id.uuidString) } + #else presets.filter { $0.isBuiltIn }.sorted(by: { $0.sortOrder < $1.sortOrder }) + #endif } private var customPresets: [PromptPreset] { + #if canImport(PindropSharedSettings) + let visibleIDs = Set(presetManagementState.customPresetIds) + return presets + .sorted(by: { $0.sortOrder < $1.sortOrder }) + .filter { visibleIDs.contains($0.id.uuidString) } + #else presets.filter { !$0.isBuiltIn }.sorted(by: { $0.sortOrder < $1.sortOrder }) + #endif } + #if canImport(PindropSharedSettings) + private var presetManagementState: PromptPresetManagementState { + PromptPresetPresenter.shared.present( + presets: presets.map { + PromptPresetSnapshot( + id: $0.id.uuidString, + name: $0.name, + prompt: $0.prompt, + isBuiltIn: $0.isBuiltIn, + sortOrder: Int32($0.sortOrder) + ) + }, + newName: newName, + newPrompt: newPrompt, + editingPresetId: editingPresetID?.uuidString, + editName: editName, + editPrompt: editPrompt + ) + } + #endif + var body: some View { VStack(spacing: 0) { header @@ -239,8 +277,8 @@ struct PresetManagementSheet: View { .background(AppColors.accent) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.Radius.md)) - .disabled(newName.isEmpty || newPrompt.isEmpty) - .opacity(newName.isEmpty || newPrompt.isEmpty ? 0.5 : 1) + .disabled(!canCreatePreset) + .opacity(canCreatePreset ? 1 : 0.5) } } .padding(AppTheme.Spacing.lg) @@ -392,6 +430,7 @@ struct PresetManagementSheet: View { private func saveNewPreset() { guard let store = store else { return } + guard canCreatePreset else { return } let preset = PromptPreset( name: newName, @@ -419,6 +458,7 @@ struct PresetManagementSheet: View { private func saveEdit(_ preset: PromptPreset) { guard let store = store else { return } + guard canSaveEditingPreset else { return } preset.name = editName preset.prompt = editPrompt @@ -514,6 +554,24 @@ struct PresetManagementSheet: View { } } +private extension PresetManagementSheet { + var canCreatePreset: Bool { + #if canImport(PindropSharedSettings) + presetManagementState.canCreatePreset + #else + !newName.isEmpty && !newPrompt.isEmpty + #endif + } + + var canSaveEditingPreset: Bool { + #if canImport(PindropSharedSettings) + presetManagementState.canSaveEditingPreset + #else + editingPresetID != nil && !editName.isEmpty && !editPrompt.isEmpty + #endif + } +} + // MARK: - Preset Row struct PresetRow: View { diff --git a/Pindrop/UI/Settings/SettingsWindow.swift b/Pindrop/UI/Settings/SettingsWindow.swift index 3a6c83b..efad30a 100644 --- a/Pindrop/UI/Settings/SettingsWindow.swift +++ b/Pindrop/UI/Settings/SettingsWindow.swift @@ -6,6 +6,9 @@ // import SwiftUI +#if canImport(PindropSharedNavigation) +import PindropSharedNavigation +#endif enum SettingsTab: String, CaseIterable, Identifiable { case general = "General" @@ -18,25 +21,11 @@ enum SettingsTab: String, CaseIterable, Identifiable { var id: String { rawValue } func title(locale: Locale) -> String { - switch self { - case .general: return localized("General", locale: locale) - case .theme: return localized("Theme", locale: locale) - case .hotkeys: return localized("Hotkeys", locale: locale) - case .ai: return localized("AI Enhancement", locale: locale) - case .update: return localized("Update", locale: locale) - case .about: return localized("About", locale: locale) - } + localized(definition.titleKey, locale: locale) } var systemIcon: String { - switch self { - case .general: return "gear" - case .theme: return "paintbrush" - case .hotkeys: return "keyboard" - case .ai: return "sparkles" - case .update: return "arrow.triangle.2.circlepath" - case .about: return "info.circle" - } + definition.systemIcon } var subtitle: String { @@ -44,61 +33,58 @@ enum SettingsTab: String, CaseIterable, Identifiable { } func subtitle(locale: Locale) -> String { + localized(definition.subtitleKey, locale: locale) + } + + func matches(_ searchText: String) -> Bool { + SettingsTab.browseState(for: searchText, selectedTab: self, initialTab: self).filteredSections.contains(coreValue) + } + + #if canImport(PindropSharedNavigation) + var coreValue: SettingsSection { switch self { - case .general: - return localized("Output, audio, interface, and everyday behavior", locale: locale) - case .theme: - return localized("Light, dark, and curated palette presets", locale: locale) - case .hotkeys: - return localized("Configure keyboard shortcuts for recording and note capture", locale: locale) - case .ai: - return localized("Providers, prompts, and vibe mode controls", locale: locale) - case .update: - return localized("Automatic updates and manual update checks", locale: locale) - case .about: - return localized("App info, acknowledgments, support, and logs", locale: locale) + case .general: .general + case .theme: .theme + case .hotkeys: .hotkeys + case .ai: .ai + case .update: .update + case .about: .about } } - private var searchKeywords: [String] { - switch self { + init(coreValue: SettingsSection) { + switch coreValue { case .general: - return [ - "output", "clipboard", "direct insert", "space", "microphone", "audio", - "input", "floating indicator", "dictionary", "launch at login", "dock", - "mute", "pause media", "reset", "language", "locale", "transcription language", - "interface language" - ] + self = .general case .theme: - return [ - "appearance", "theme", "light", "dark", "system", "preset", "palette" - ] + self = .theme case .hotkeys: - return [ - "shortcut", "toggle recording", "push to talk", "copy last transcript", - "note capture", "keyboard" - ] + self = .hotkeys case .ai: - return [ - "provider", "api key", "endpoint", "prompt", "preset", "vibe mode", - "clipboard context", "ui context", "model", "enhancement" - ] + self = .ai case .update: - return ["updates", "automatic updates", "check now", "version"] - case .about: - return ["support", "logs", "github", "license", "system info", "version"] + self = .update + default: + self = .about } } - func matches(_ searchText: String) -> Bool { - let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard !query.isEmpty else { return true } + fileprivate var definition: SettingsSectionDefinition { + SettingsShell.shared.section(id: coreValue) + } - let searchableText = ([rawValue, subtitle] + searchKeywords) - .joined(separator: " ") - .lowercased() - return searchableText.contains(query) + fileprivate static func browseState( + for query: String, + selectedTab: SettingsTab?, + initialTab: SettingsTab + ) -> SettingsBrowseState { + SettingsShell.shared.browse( + query: query, + selectedSection: selectedTab?.coreValue, + initialSection: initialTab.coreValue + ) } + #endif } struct SettingsWindow: View { @@ -146,7 +132,15 @@ struct SettingsContainerView: View { } private var filteredTabs: [SettingsTab] { - SettingsTab.allCases.filter { $0.matches(searchText) } + browseState.filteredSections.map(SettingsTab.init(coreValue:)) + } + + private var browseState: SettingsBrowseState { + SettingsTab.browseState(for: searchText, selectedTab: selectedTab, initialTab: initialTab) + } + + private var activeTab: SettingsTab { + SettingsTab(coreValue: browseState.selectedSection) } var body: some View { @@ -162,10 +156,7 @@ struct SettingsContainerView: View { selectedTab = newValue } .onChange(of: searchText) { _, _ in - guard let firstVisibleTab = filteredTabs.first else { return } - if !filteredTabs.contains(selectedTab) { - selectedTab = firstVisibleTab - } + selectedTab = activeTab } .onAppear { if AppTestMode.isRunningUITests { @@ -187,11 +178,11 @@ struct SettingsContainerView: View { } else { VStack(alignment: .leading, spacing: AppTheme.Spacing.xl) { VStack(alignment: .leading, spacing: AppTheme.Spacing.xxs) { - Text(selectedTab.title(locale: locale)) + Text(activeTab.title(locale: locale)) .font(AppTypography.headline) .foregroundStyle(AppColors.textPrimary) - Text(selectedTab.subtitle(locale: locale)) + Text(activeTab.subtitle(locale: locale)) .font(AppTypography.body) .foregroundStyle(AppColors.textSecondary) } @@ -200,7 +191,7 @@ struct SettingsContainerView: View { .background(AppColors.divider) Group { - switch selectedTab { + switch activeTab { case .general: GeneralSettingsView(settings: settings) case .theme: @@ -358,10 +349,7 @@ private extension SettingsContainerView { private extension SettingsTab { var accessibilityIdentifier: String { - let slug = rawValue - .lowercased() - .replacingOccurrences(of: " ", with: "-") - return "settings.tab.\(slug)" + definition.accessibilityIdentifier } } diff --git a/Pindrop/UI/Theme/Theme.swift b/Pindrop/UI/Theme/Theme.swift index e6a1f62..a8d40ce 100644 --- a/Pindrop/UI/Theme/Theme.swift +++ b/Pindrop/UI/Theme/Theme.swift @@ -8,6 +8,10 @@ import AppKit import SwiftUI +#if canImport(PindropSharedUITheme) +import PindropSharedUITheme +#endif + @MainActor final class PindropThemeController: ObservableObject { static let shared = PindropThemeController() @@ -19,13 +23,16 @@ final class PindropThemeController: ObservableObject { } func refresh() { + #if canImport(PindropSharedUITheme) + PindropThemeBridge.invalidateCache() + #endif applyAppAppearance() revision &+= 1 } func apply(to window: NSWindow?) { window?.appearance = currentMode.appKitAppearanceName.flatMap(NSAppearance.init(named:)) - window?.backgroundColor = NSColor(AppColors.windowBackground) + window?.backgroundColor = AppColors.windowBackgroundColor } private func applyAppAppearance() { @@ -49,29 +56,50 @@ private struct ThemeRefreshModifier: ViewModifier { enum AppTheme { enum Spacing { - static let xxs: CGFloat = 4 - static let xs: CGFloat = 6 - static let sm: CGFloat = 10 - static let md: CGFloat = 14 - static let lg: CGFloat = 18 - static let xl: CGFloat = 24 - static let xxl: CGFloat = 32 - static let xxxl: CGFloat = 40 - static let huge: CGFloat = 56 + static var xxs: CGFloat { CGFloat(themeSpacing.xxs) } + static var xs: CGFloat { CGFloat(themeSpacing.xs) } + static var sm: CGFloat { CGFloat(themeSpacing.sm) } + static var md: CGFloat { CGFloat(themeSpacing.md) } + static var lg: CGFloat { CGFloat(themeSpacing.lg) } + static var xl: CGFloat { CGFloat(themeSpacing.xl) } + static var xxl: CGFloat { CGFloat(themeSpacing.xxl) } + static var xxxl: CGFloat { CGFloat(themeSpacing.xxxl) } + static var huge: CGFloat { CGFloat(themeSpacing.huge) } + + #if canImport(PindropSharedUITheme) + private static var themeSpacing: SpacingScale { PindropThemeBridge.spacingScale } + #endif } enum Radius { - static let sm: CGFloat = 8 - static let md: CGFloat = 12 - static let lg: CGFloat = 18 - static let xl: CGFloat = 24 - static let full: CGFloat = 9999 + static var sm: CGFloat { CGFloat(themeRadius.sm) } + static var md: CGFloat { CGFloat(themeRadius.md) } + static var lg: CGFloat { CGFloat(themeRadius.lg) } + static var xl: CGFloat { CGFloat(themeRadius.xl) } + static var full: CGFloat { CGFloat(themeRadius.full) } + + #if canImport(PindropSharedUITheme) + private static var themeRadius: RadiusScale { PindropThemeBridge.radiusScale } + #endif } enum Shadow { - static let sm = ShadowStyle(color: AppColors.shadowColor.opacity(0.08), radius: 6, x: 0, y: 2) - static let md = ShadowStyle(color: AppColors.shadowColor.opacity(0.14), radius: 16, x: 0, y: 8) - static let lg = ShadowStyle(color: AppColors.shadowColor.opacity(0.2), radius: 30, x: 0, y: 18) + static var sm: ShadowStyle { shadowStyle(from: themeShadow.sm) } + static var md: ShadowStyle { shadowStyle(from: themeShadow.md) } + static var lg: ShadowStyle { shadowStyle(from: themeShadow.lg) } + + #if canImport(PindropSharedUITheme) + private static var themeShadow: ShadowScale { PindropThemeBridge.shadowScale } + + private static func shadowStyle(from token: ShadowTokenValue) -> ShadowStyle { + ShadowStyle( + color: Color(nsColor: NSColor(token.color)), + radius: CGFloat(token.radius), + x: CGFloat(token.x), + y: CGFloat(token.y) + ) + } + #endif } enum Window { @@ -158,16 +186,17 @@ enum AppColors { private static func dynamicNSColor(_ keyPath: KeyPath) -> NSColor { NSColor(name: nil) { appearance in - resolvedPalette(for: isDark(appearance))[keyPath: keyPath] + resolvedPalette(for: appearance)[keyPath: keyPath] } } - private static func resolvedPalette(for isDark: Bool) -> ResolvedPalette { - let variant: PindropThemeVariant = isDark ? .dark : .light - let storageKey = isDark ? PindropThemeStorageKeys.darkThemePresetID : PindropThemeStorageKeys.lightThemePresetID - let presetID = UserDefaults.standard.string(forKey: storageKey) - let profile = PindropThemePresetCatalog.profile(for: presetID, variant: variant) - return ResolvedPalette(profile: profile, isDark: isDark) + private static func resolvedPalette(for appearance: NSAppearance) -> ResolvedPalette { + #if canImport(PindropSharedUITheme) + let variant: PindropThemeVariant = isDark(appearance) ? .dark : .light + return ResolvedPalette(theme: PindropThemeBridge.resolveTheme(systemVariant: variant)) + #else + fatalError("PindropSharedUITheme is required") + #endif } private static func isDark(_ appearance: NSAppearance) -> Bool { @@ -176,18 +205,52 @@ enum AppColors { } enum AppTypography { - static let largeTitle = Font.system(size: 30, weight: .semibold, design: .rounded) - static let title = Font.system(size: 21, weight: .semibold, design: .rounded) - static let headline = Font.system(size: 16, weight: .semibold, design: .rounded) - static let subheadline = Font.system(size: 14, weight: .semibold, design: .rounded) - static let body = Font.system(size: 14, weight: .regular, design: .rounded) - static let bodySmall = Font.system(size: 13, weight: .regular, design: .rounded) - static let caption = Font.system(size: 12, weight: .medium, design: .rounded) - static let tiny = Font.system(size: 11, weight: .medium, design: .rounded) - static let mono = Font.system(size: 13, weight: .medium, design: .monospaced) - static let monoSmall = Font.system(size: 11, weight: .medium, design: .monospaced) - static let statLarge = Font.system(size: 32, weight: .bold, design: .rounded) - static let statMedium = Font.system(size: 24, weight: .semibold, design: .rounded) + static var largeTitle: Font { font(from: scale.largeTitle) } + static var title: Font { font(from: scale.title) } + static var headline: Font { font(from: scale.headline) } + static var subheadline: Font { font(from: scale.subheadline) } + static var body: Font { font(from: scale.body) } + static var bodySmall: Font { font(from: scale.bodySmall) } + static var caption: Font { font(from: scale.caption) } + static var tiny: Font { font(from: scale.tiny) } + static var mono: Font { font(from: scale.mono) } + static var monoSmall: Font { font(from: scale.monoSmall) } + static var statLarge: Font { font(from: scale.statLarge) } + static var statMedium: Font { font(from: scale.statMedium) } + + #if canImport(PindropSharedUITheme) + private static var scale: TypographyScale { PindropThemeBridge.typographyScale } + + private static func font(from token: TypographyTokenValue) -> Font { + Font.system( + size: token.size, + weight: weight(from: Int(token.weight)), + design: design(from: token.design) + ) + } + + private static func weight(from value: Int) -> Font.Weight { + switch value { + case 700...: + .bold + case 600...: + .semibold + case 500...: + .medium + default: + .regular + } + } + + private static func design(from design: TypographyDesign) -> Font.Design { + switch design { + case .monospaced: + .monospaced + default: + .rounded + } + } + #endif } private struct ResolvedPalette { @@ -229,96 +292,48 @@ private struct ResolvedPalette { let overlayTooltipAccent: NSColor let shadow: NSColor - init(profile: PindropThemeProfile, isDark: Bool) { - let background = NSColor(pindropHex: profile.backgroundHex) - ?? (isDark ? NSColor(red: 0.08, green: 0.08, blue: 0.09, alpha: 1) : NSColor(red: 0.97, green: 0.96, blue: 0.94, alpha: 1)) - let foreground = NSColor(pindropHex: profile.foregroundHex) - ?? (isDark ? NSColor.white : NSColor.black) - let accentBase = NSColor(pindropHex: profile.accentHex) ?? .systemOrange - let successBase = NSColor(pindropHex: profile.successHex) ?? .systemGreen - let warningBase = NSColor(pindropHex: profile.warningHex) ?? .systemOrange - let dangerBase = NSColor(pindropHex: profile.dangerHex) ?? .systemRed - let processingBase = NSColor(pindropHex: profile.processingHex) ?? .systemBlue - let contrast = min(max(profile.contrast, 20), 80) / 100 - - if isDark { - windowBackground = background - sidebarBackground = background.lighter(by: 0.035) - contentBackground = background.lighter(by: 0.015) - surfaceBackground = background.lighter(by: 0.055 + contrast * 0.035) - elevatedSurface = background.lighter(by: 0.09 + contrast * 0.045) - mutedSurface = foreground.withAlphaComponent(0.06 + contrast * 0.02) - inputBackground = background.lighter(by: 0.075 + contrast * 0.03) - inputBorder = foreground.withAlphaComponent(0.14 + contrast * 0.06) - inputBorderFocused = accentBase.withAlphaComponent(0.78) - accent = accentBase - accentSecondary = accentBase.mixed(with: foreground, ratio: 0.22) - accentBackground = accentBase.mixed(with: background, ratio: 0.86) - textPrimary = foreground - textSecondary = foreground.withAlphaComponent(0.72) - textTertiary = foreground.withAlphaComponent(0.48) - border = foreground.withAlphaComponent(0.11 + contrast * 0.05) - divider = foreground.withAlphaComponent(0.08 + contrast * 0.04) - success = successBase - successBackground = successBase.mixed(with: background, ratio: 0.88) - warning = warningBase - warningBackground = warningBase.mixed(with: background, ratio: 0.88) - error = dangerBase - errorBackground = dangerBase.mixed(with: background, ratio: 0.89) - recording = dangerBase - processing = processingBase - sidebarItemHover = foreground.withAlphaComponent(0.065) - sidebarItemActive = accentBase.mixed(with: background, ratio: 0.82) - overlaySurface = background.darker(by: 0.24) - overlaySurfaceStrong = background.darker(by: 0.32) - overlayLine = foreground.withAlphaComponent(0.18) - overlayTextPrimary = NSColor.white.withAlphaComponent(0.96) - overlayTextSecondary = NSColor.white.withAlphaComponent(0.74) - overlayWaveform = accentBase.mixed(with: NSColor.white, ratio: 0.24) - overlayRecording = dangerBase.mixed(with: NSColor.white, ratio: 0.12) - overlayWarning = warningBase - overlayTooltipAccent = accentBase.mixed(with: NSColor.white, ratio: 0.3) - shadow = NSColor.black - } else { - windowBackground = background - sidebarBackground = background.darker(by: 0.018) - contentBackground = background.lighter(by: 0.005) - surfaceBackground = background.lighter(by: 0.025) - elevatedSurface = background.darker(by: 0.02 + contrast * 0.01) - mutedSurface = foreground.withAlphaComponent(0.045 + contrast * 0.02) - inputBackground = background.lighter(by: 0.015) - inputBorder = foreground.withAlphaComponent(0.14 + contrast * 0.04) - inputBorderFocused = accentBase.withAlphaComponent(0.72) - accent = accentBase - accentSecondary = accentBase.mixed(with: foreground, ratio: 0.18) - accentBackground = accentBase.mixed(with: background, ratio: 0.92) - textPrimary = foreground - textSecondary = foreground.withAlphaComponent(0.7) - textTertiary = foreground.withAlphaComponent(0.48) - border = foreground.withAlphaComponent(0.1 + contrast * 0.04) - divider = foreground.withAlphaComponent(0.07 + contrast * 0.03) - success = successBase - successBackground = successBase.mixed(with: background, ratio: 0.93) - warning = warningBase - warningBackground = warningBase.mixed(with: background, ratio: 0.93) - error = dangerBase - errorBackground = dangerBase.mixed(with: background, ratio: 0.94) - recording = dangerBase - processing = processingBase - sidebarItemHover = foreground.withAlphaComponent(0.05) - sidebarItemActive = accentBase.mixed(with: background, ratio: 0.87) - overlaySurface = background.darker(by: 0.82) - overlaySurfaceStrong = background.darker(by: 0.9) - overlayLine = NSColor.white.withAlphaComponent(0.14) - overlayTextPrimary = NSColor.white.withAlphaComponent(0.96) - overlayTextSecondary = NSColor.white.withAlphaComponent(0.74) - overlayWaveform = accentBase.mixed(with: NSColor.white, ratio: 0.42) - overlayRecording = dangerBase.mixed(with: NSColor.white, ratio: 0.18) - overlayWarning = warningBase.mixed(with: NSColor.white, ratio: 0.18) - overlayTooltipAccent = accentBase.mixed(with: NSColor.white, ratio: 0.42) - shadow = foreground - } - } + #if canImport(PindropSharedUITheme) + init(theme: ResolvedTheme) { + let tokens = theme.tokens + windowBackground = NSColor(tokens.windowBackground) + sidebarBackground = NSColor(tokens.sidebarBackground) + contentBackground = NSColor(tokens.contentBackground) + surfaceBackground = NSColor(tokens.surfaceBackground) + elevatedSurface = NSColor(tokens.elevatedSurface) + mutedSurface = NSColor(tokens.mutedSurface) + inputBackground = NSColor(tokens.inputBackground) + inputBorder = NSColor(tokens.inputBorder) + inputBorderFocused = NSColor(tokens.inputBorderFocused) + accent = NSColor(tokens.accent) + accentSecondary = NSColor(tokens.accentSecondary) + accentBackground = NSColor(tokens.accentBackground) + textPrimary = NSColor(tokens.textPrimary) + textSecondary = NSColor(tokens.textSecondary) + textTertiary = NSColor(tokens.textTertiary) + border = NSColor(tokens.border) + divider = NSColor(tokens.divider) + success = NSColor(tokens.success) + successBackground = NSColor(tokens.successBackground) + warning = NSColor(tokens.warning) + warningBackground = NSColor(tokens.warningBackground) + error = NSColor(tokens.error) + errorBackground = NSColor(tokens.errorBackground) + recording = NSColor(tokens.recording) + processing = NSColor(tokens.processing) + sidebarItemHover = NSColor(tokens.sidebarItemHover) + sidebarItemActive = NSColor(tokens.sidebarItemActive) + overlaySurface = NSColor(tokens.overlaySurface) + overlaySurfaceStrong = NSColor(tokens.overlaySurfaceStrong) + overlayLine = NSColor(tokens.overlayLine) + overlayTextPrimary = NSColor(tokens.overlayTextPrimary) + overlayTextSecondary = NSColor(tokens.overlayTextSecondary) + overlayWaveform = NSColor(tokens.overlayWaveform) + overlayRecording = NSColor(tokens.overlayRecording) + overlayWarning = NSColor(tokens.overlayWarning) + overlayTooltipAccent = NSColor(tokens.overlayTooltipAccent) + shadow = NSColor(tokens.shadow) + } + #endif } private struct HairlineBorderModifier: ViewModifier { @@ -449,27 +464,16 @@ extension NSColor { self.init(red: red, green: green, blue: blue, alpha: 1) } - func mixed(with color: NSColor, ratio: CGFloat) -> NSColor { - let resolvedSelf = usingColorSpace(.deviceRGB) ?? self - let resolvedOther = color.usingColorSpace(.deviceRGB) ?? color - let clampedRatio = min(max(ratio, 0), 1) - let inverse = 1 - clampedRatio - - return NSColor( - red: (resolvedSelf.redComponent * inverse) + (resolvedOther.redComponent * clampedRatio), - green: (resolvedSelf.greenComponent * inverse) + (resolvedOther.greenComponent * clampedRatio), - blue: (resolvedSelf.blueComponent * inverse) + (resolvedOther.blueComponent * clampedRatio), - alpha: (resolvedSelf.alphaComponent * inverse) + (resolvedOther.alphaComponent * clampedRatio) + #if canImport(PindropSharedUITheme) + convenience init(_ token: ColorTokenValue) { + self.init( + red: CGFloat(token.red) / 255, + green: CGFloat(token.green) / 255, + blue: CGFloat(token.blue) / 255, + alpha: CGFloat(token.alpha) / 255 ) } - - func lighter(by amount: CGFloat) -> NSColor { - mixed(with: .white, ratio: amount) - } - - func darker(by amount: CGFloat) -> NSColor { - mixed(with: .black, ratio: amount) - } + #endif } #Preview("Theme Colors - Light") { @@ -519,22 +523,17 @@ private struct ThemePreviewView: View { } .padding(AppTheme.Spacing.xxl) .background(AppColors.windowBackground) - .themeRefresh() } - private func colorSwatch(_ name: String, _ color: Color) -> some View { + private func colorSwatch(_ title: String, _ color: Color) -> some View { VStack(spacing: AppTheme.Spacing.xs) { - RoundedRectangle(cornerRadius: AppTheme.Radius.sm) + RoundedRectangle(cornerRadius: AppTheme.Radius.md, style: .continuous) .fill(color) - .frame(width: 60, height: 40) - .hairlineBorder( - RoundedRectangle(cornerRadius: AppTheme.Radius.sm), - style: AppColors.border - ) - - Text(name) - .font(AppTypography.tiny) + .frame(height: 72) + Text(title) + .font(AppTypography.caption) .foregroundStyle(AppColors.textSecondary) } + .frame(maxWidth: .infinity) } } diff --git a/Pindrop/UI/Theme/ThemeModels.swift b/Pindrop/UI/Theme/ThemeModels.swift index 61af5c7..c4c2a7e 100644 --- a/Pindrop/UI/Theme/ThemeModels.swift +++ b/Pindrop/UI/Theme/ThemeModels.swift @@ -8,6 +8,10 @@ import AppKit import Foundation +#if canImport(PindropSharedUITheme) +import PindropSharedUITheme +#endif + enum PindropThemeStorageKeys { static let themeMode = "themeMode" static let lightThemePresetID = "lightThemePresetID" @@ -19,6 +23,24 @@ enum PindropThemeVariant: String, CaseIterable, Identifiable { case dark var id: String { rawValue } + + #if canImport(PindropSharedUITheme) + var coreValue: ThemeVariant { + switch self { + case .light: .light + case .dark: .dark + } + } + + init(coreValue: ThemeVariant) { + switch coreValue { + case .light: + self = .light + default: + self = .dark + } + } + #endif } enum PindropThemeMode: String, CaseIterable, Identifiable { @@ -64,6 +86,27 @@ enum PindropThemeMode: String, CaseIterable, Identifiable { return .darkAqua } } + + #if canImport(PindropSharedUITheme) + var coreValue: ThemeMode { + switch self { + case .system: .system + case .light: .light + case .dark: .dark + } + } + + init(coreValue: ThemeMode) { + switch coreValue { + case .system: + self = .system + case .light: + self = .light + default: + self = .dark + } + } + #endif } struct PindropThemeProfile: Hashable { @@ -75,6 +118,19 @@ struct PindropThemeProfile: Hashable { let warningHex: String let dangerHex: String let processingHex: String + + #if canImport(PindropSharedUITheme) + init(coreProfile: ThemeProfile) { + accentHex = coreProfile.accentHex + backgroundHex = coreProfile.backgroundHex + foregroundHex = coreProfile.foregroundHex + contrast = coreProfile.contrast + successHex = coreProfile.successHex + warningHex = coreProfile.warningHex + dangerHex = coreProfile.dangerHex + processingHex = coreProfile.processingHex + } + #endif } struct PindropThemePreset: Hashable, Identifiable { @@ -84,202 +140,176 @@ struct PindropThemePreset: Hashable, Identifiable { let badgeText: String let badgeBackgroundHex: String let badgeForegroundHex: String - let lightTheme: PindropThemeProfile - let darkTheme: PindropThemeProfile + + private let lightTheme: PindropThemeProfile + private let darkTheme: PindropThemeProfile func profile(for variant: PindropThemeVariant) -> PindropThemeProfile { switch variant { case .light: - return lightTheme + lightTheme case .dark: - return darkTheme + darkTheme } } + + #if canImport(PindropSharedUITheme) + init(corePreset: ThemePreset) { + id = corePreset.id + title = corePreset.title + summary = corePreset.summary + badgeText = corePreset.badgeText + badgeBackgroundHex = corePreset.badgeBackgroundHex + badgeForegroundHex = corePreset.badgeForegroundHex + lightTheme = PindropThemeProfile(coreProfile: corePreset.lightTheme) + darkTheme = PindropThemeProfile(coreProfile: corePreset.darkTheme) + } + #endif } enum PindropThemePresetCatalog { - static let defaultPresetID = "pindrop" - - static let presets: [PindropThemePreset] = [ - PindropThemePreset( - id: "pindrop", - title: "Pindrop", - summary: "Warm editorial surfaces with a copper signal accent.", - badgeText: "Pd", - badgeBackgroundHex: "#F7F1E8", - badgeForegroundHex: "#C56E42", - lightTheme: PindropThemeProfile( - accentHex: "#C56E42", - backgroundHex: "#F7F1E8", - foregroundHex: "#221A14", - contrast: 50, - successHex: "#2E8B67", - warningHex: "#A9692D", - dangerHex: "#C95452", - processingHex: "#4D78D6" - ), - darkTheme: PindropThemeProfile( - accentHex: "#E19260", - backgroundHex: "#15120F", - foregroundHex: "#F2E5D8", - contrast: 66, - successHex: "#53B48A", - warningHex: "#D09049", - dangerHex: "#E5726E", - processingHex: "#74A2FF" - ) - ), - PindropThemePreset( - id: "paper", - title: "Paper", - summary: "Quiet parchment tones with ink-forward contrast.", - badgeText: "Aa", - badgeBackgroundHex: "#FBF7EF", - badgeForegroundHex: "#2E4E73", - lightTheme: PindropThemeProfile( - accentHex: "#2E4E73", - backgroundHex: "#FBF7EF", - foregroundHex: "#1A1712", - contrast: 46, - successHex: "#2D7D5A", - warningHex: "#9C6B24", - dangerHex: "#BD514A", - processingHex: "#3A67C3" - ), - darkTheme: PindropThemeProfile( - accentHex: "#89A9D4", - backgroundHex: "#1A1816", - foregroundHex: "#F4EEE5", - contrast: 62, - successHex: "#58B48B", - warningHex: "#D09B53", - dangerHex: "#E87C74", - processingHex: "#7FA7FF" - ) - ), - PindropThemePreset( - id: "harbor", - title: "Harbor", - summary: "Cool blue-gray chrome with a crisp marine accent.", - badgeText: "Hb", - badgeBackgroundHex: "#EFF5F7", - badgeForegroundHex: "#14708A", - lightTheme: PindropThemeProfile( - accentHex: "#14708A", - backgroundHex: "#EFF5F7", - foregroundHex: "#14232B", - contrast: 48, - successHex: "#2F8663", - warningHex: "#B0702D", - dangerHex: "#C85652", - processingHex: "#2F78D0" + static var defaultPresetID: String { + #if canImport(PindropSharedUITheme) + ThemeCatalog.shared.defaultPresetId + #else + "pindrop" + #endif + } + + static var presets: [PindropThemePreset] { + #if canImport(PindropSharedUITheme) + ThemeCatalog.shared.presets().map(PindropThemePreset.init(corePreset:)) + #else + [] + #endif + } + + static func preset(withID id: String?) -> PindropThemePreset { + #if canImport(PindropSharedUITheme) + PindropThemePreset(corePreset: ThemeCatalog.shared.preset(id: id)) + #else + fatalError("PindropSharedUITheme is required") + #endif + } + + static func profile(for id: String?, variant: PindropThemeVariant) -> PindropThemeProfile { + preset(withID: id).profile(for: variant) + } +} + +#if canImport(PindropSharedUITheme) +enum PindropThemeBridge { + static let capabilities = ThemeCapabilities( + supportsTranslucentSidebar: true, + supportsWindowMaterial: true, + supportsOverlayBlur: true, + supportsNativeVibrancy: true, + supportsUnifiedTitlebar: true + ) + + private struct CacheKey: Equatable { + let mode: String + let lightPresetID: String + let darkPresetID: String + let variant: PindropThemeVariant + } + + private static var cachedKey: CacheKey? + private static var cachedTheme: ResolvedTheme? + + static func resolveTheme(systemVariant: PindropThemeVariant) -> ResolvedTheme { + let selection = ThemeSelection( + mode: currentMode().coreValue, + lightPresetId: currentLightPresetID(), + darkPresetId: currentDarkPresetID() + ) + let key = CacheKey( + mode: currentMode().rawValue, + lightPresetID: selection.lightPresetId, + darkPresetID: selection.darkPresetId, + variant: systemVariant + ) + + if let cachedTheme, cachedKey == key { + return cachedTheme + } + + let resolved = ThemeEngine.shared.resolveTheme( + selection: selection, + systemVariant: systemVariant.coreValue, + capabilities: capabilities + ) + cachedKey = key + cachedTheme = resolved + return resolved + } + + static func invalidateCache() { + cachedKey = nil + cachedTheme = nil + } + + static var spacingScale: SpacingScale { + ThemeEngine.shared.resolveTheme( + selection: ThemeSelection( + mode: .system, + lightPresetId: ThemeCatalog.shared.defaultPresetId, + darkPresetId: ThemeCatalog.shared.defaultPresetId ), - darkTheme: PindropThemeProfile( - accentHex: "#5AB4D4", - backgroundHex: "#0F171C", - foregroundHex: "#E3F0F5", - contrast: 67, - successHex: "#5FB98C", - warningHex: "#D59A4F", - dangerHex: "#E3716D", - processingHex: "#69A8FF" - ) - ), - PindropThemePreset( - id: "evergreen", - title: "Evergreen", - summary: "Forest-tinted utility palette with a calm studio feel.", - badgeText: "Eg", - badgeBackgroundHex: "#F3F5EE", - badgeForegroundHex: "#4D7A4A", - lightTheme: PindropThemeProfile( - accentHex: "#4D7A4A", - backgroundHex: "#F3F5EE", - foregroundHex: "#1C2019", - contrast: 47, - successHex: "#3A8B5B", - warningHex: "#AA6D26", - dangerHex: "#B84F49", - processingHex: "#4A74C9" + systemVariant: .light, + capabilities: capabilities + ).tokens.spacing + } + + static var radiusScale: RadiusScale { + ThemeEngine.shared.resolveTheme( + selection: ThemeSelection( + mode: .system, + lightPresetId: ThemeCatalog.shared.defaultPresetId, + darkPresetId: ThemeCatalog.shared.defaultPresetId ), - darkTheme: PindropThemeProfile( - accentHex: "#87B57D", - backgroundHex: "#101411", - foregroundHex: "#E6EEE1", - contrast: 65, - successHex: "#64BC85", - warningHex: "#D29648", - dangerHex: "#DF6F68", - processingHex: "#7EA7FF" - ) - ), - PindropThemePreset( - id: "graphite", - title: "Graphite", - summary: "Neutral monochrome with a high-signal cobalt edge.", - badgeText: "Gr", - badgeBackgroundHex: "#F4F5F7", - badgeForegroundHex: "#4B65D6", - lightTheme: PindropThemeProfile( - accentHex: "#4B65D6", - backgroundHex: "#F4F5F7", - foregroundHex: "#16181D", - contrast: 49, - successHex: "#2C8A67", - warningHex: "#A66821", - dangerHex: "#C34C50", - processingHex: "#507BFF" + systemVariant: .light, + capabilities: capabilities + ).tokens.radius + } + + static var typographyScale: TypographyScale { + ThemeEngine.shared.resolveTheme( + selection: ThemeSelection( + mode: .system, + lightPresetId: ThemeCatalog.shared.defaultPresetId, + darkPresetId: ThemeCatalog.shared.defaultPresetId ), - darkTheme: PindropThemeProfile( - accentHex: "#7D93FF", - backgroundHex: "#101114", - foregroundHex: "#ECEFF4", - contrast: 70, - successHex: "#5DBD93", - warningHex: "#D69D55", - dangerHex: "#E77A80", - processingHex: "#87A7FF" - ) - ), - PindropThemePreset( - id: "signal", - title: "Signal", - summary: "Dark broadcast palette with a vivid red-orange pulse.", - badgeText: "Sg", - badgeBackgroundHex: "#181211", - badgeForegroundHex: "#F06D4F", - lightTheme: PindropThemeProfile( - accentHex: "#D95E45", - backgroundHex: "#FBF4F1", - foregroundHex: "#251816", - contrast: 51, - successHex: "#2C8863", - warningHex: "#AF6A21", - dangerHex: "#C94E4B", - processingHex: "#466AD4" + systemVariant: .light, + capabilities: capabilities + ).tokens.typography + } + + static var shadowScale: ShadowScale { + ThemeEngine.shared.resolveTheme( + selection: ThemeSelection( + mode: .system, + lightPresetId: ThemeCatalog.shared.defaultPresetId, + darkPresetId: ThemeCatalog.shared.defaultPresetId ), - darkTheme: PindropThemeProfile( - accentHex: "#F06D4F", - backgroundHex: "#181211", - foregroundHex: "#F5E7E2", - contrast: 72, - successHex: "#53B98A", - warningHex: "#DD9745", - dangerHex: "#F5847A", - processingHex: "#7EA4FF" - ) - ), - ] + systemVariant: .light, + capabilities: capabilities + ).tokens.shadowScale + } - static func preset(withID id: String?) -> PindropThemePreset { - guard let id, let preset = presets.first(where: { $0.id == id }) else { - return presets.first(where: { $0.id == defaultPresetID }) ?? presets[0] - } + private static func currentMode() -> PindropThemeMode { + let rawValue = UserDefaults.standard.string(forKey: PindropThemeStorageKeys.themeMode) ?? "" + return PindropThemeMode(rawValue: rawValue) ?? .system + } - return preset + private static func currentLightPresetID() -> String { + UserDefaults.standard.string(forKey: PindropThemeStorageKeys.lightThemePresetID) + ?? ThemeCatalog.shared.defaultPresetId } - static func profile(for id: String?, variant: PindropThemeVariant) -> PindropThemeProfile { - preset(withID: id).profile(for: variant) + private static func currentDarkPresetID() -> String { + UserDefaults.standard.string(forKey: PindropThemeStorageKeys.darkThemePresetID) + ?? ThemeCatalog.shared.defaultPresetId } } +#endif diff --git a/PindropTests/LaunchAtLoginManagerTests.swift b/PindropTests/LaunchAtLoginManagerTests.swift index 2c06487..53fc466 100644 --- a/PindropTests/LaunchAtLoginManagerTests.swift +++ b/PindropTests/LaunchAtLoginManagerTests.swift @@ -47,7 +47,7 @@ struct LaunchAtLoginManagerTests { @Test func initialization() { let sut = makeSUT().sut - #expect(sut != nil) + #expect(sut.isEnabled == false) } @Test func isEnabledReflectsServiceStatus() { @@ -137,7 +137,6 @@ struct LaunchAtLoginManagerTests { @Test func errorConformsToLocalizedError() { let error = LaunchAtLoginManager.LaunchAtLoginError.registrationFailed(NSError(domain: "test", code: 1)) - #expect(error is LocalizedError) #expect((error as LocalizedError).errorDescription != nil) } diff --git a/PindropTests/ModelManagerTests.swift b/PindropTests/ModelManagerTests.swift index 5de34ab..096043f 100644 --- a/PindropTests/ModelManagerTests.swift +++ b/PindropTests/ModelManagerTests.swift @@ -61,7 +61,7 @@ struct ModelManagerTests { @Test func checkDownloadedModels() async { let downloadedModels = await modelManager.getDownloadedModels() - #expect(downloadedModels != nil) + _ = downloadedModels } @Test func isModelDownloaded() { diff --git a/PindropTests/SettingsStoreTests.swift b/PindropTests/SettingsStoreTests.swift index f318ec8..74f5bc0 100644 --- a/PindropTests/SettingsStoreTests.swift +++ b/PindropTests/SettingsStoreTests.swift @@ -8,6 +8,18 @@ import AppKit import Testing @testable import Pindrop +#if canImport(PindropSharedUITheme) +import PindropSharedUITheme +#endif +#if canImport(PindropSharedNavigation) +import PindropSharedNavigation +#endif +#if canImport(PindropSharedSettings) +import PindropSharedSettings +#endif +#if canImport(PindropSharedUIWorkspace) +import PindropSharedUIWorkspace +#endif @MainActor @Suite @@ -249,6 +261,329 @@ struct SettingsStoreTests { #expect(PindropThemeMode.dark.appKitAppearanceName == .darkAqua) } + @Test func testThemeCatalogBridgesSharedPresetDefinitions() { + let presetIDs = Set(PindropThemePresetCatalog.presets.map(\.id)) + + #expect(presetIDs.contains(PindropThemePresetCatalog.defaultPresetID)) + #expect(presetIDs.contains("paper")) + #expect(presetIDs.contains("signal")) + } + + @Test func testThemeBridgeResolvesSharedThemeAndCapabilities() { + let settingsStore = makeSettingsStore() + defer { cleanup(settingsStore) } + + UserDefaults.standard.set(PindropThemeMode.system.rawValue, forKey: PindropThemeStorageKeys.themeMode) + UserDefaults.standard.set("paper", forKey: PindropThemeStorageKeys.lightThemePresetID) + UserDefaults.standard.set("signal", forKey: PindropThemeStorageKeys.darkThemePresetID) + + #if canImport(PindropSharedUITheme) + PindropThemeBridge.invalidateCache() + let lightTheme = PindropThemeBridge.resolveTheme(systemVariant: .light) + let darkTheme = PindropThemeBridge.resolveTheme(systemVariant: .dark) + + #expect(lightTheme.selectedPreset.id == "paper") + #expect(darkTheme.selectedPreset.id == "signal") + #expect(lightTheme.adaptedSidebarTreatment == .translucent) + #expect(darkTheme.adaptedOverlayTreatment == .blurred) + #endif + } + + @Test func testSettingsTabSearchUsesSharedShellDefinitions() { + #expect(SettingsTab.theme.matches("palette")) + #expect(!SettingsTab.about.matches("palette")) + } + + @Test func testAISettingsPresenterSharesValidationAndPresetRules() { + #if canImport(PindropSharedSettings) + let state = AIEnhancementPresenter.shared.present( + draft: AIEnhancementDraft( + selectedProvider: .custom, + selectedCustomProvider: .ollama, + apiKey: "", + selectedModel: "", + customModel: "", + enhancementPrompt: "Prompt", + noteEnhancementPrompt: "Notes", + selectedPromptType: .transcription, + selectedPresetId: "builtin", + customEndpointText: "http://localhost:11434/v1/chat/completions", + availableModels: [], + modelErrorMessage: nil, + isLoadingModels: false, + aiEnhancementEnabled: true + ), + presets: [ + PromptPresetSnapshot( + id: "builtin", + name: "Built In", + prompt: "Prompt", + isBuiltIn: true, + sortOrder: 0 + ) + ] + ) + + #expect(state.isApiKeyOptional) + #expect(!state.canSave) + #expect(state.selectedPresetId == "builtin") + #expect(state.isBuiltInPresetSelected) + #expect(state.isSelectedPromptReadOnly) + #endif + } + + @Test func testPromptPresetPresenterSharesGroupingAndValidation() { + #if canImport(PindropSharedSettings) + let state = PromptPresetPresenter.shared.present( + presets: [ + PromptPresetSnapshot(id: "builtin", name: "Built In", prompt: "One", isBuiltIn: true, sortOrder: 0), + PromptPresetSnapshot(id: "custom", name: "Custom", prompt: "Two", isBuiltIn: false, sortOrder: 1), + ], + newName: "New", + newPrompt: "Prompt", + editingPresetId: "custom", + editName: "Edited", + editPrompt: "Updated" + ) + + #expect(state.builtInPresetIds == ["builtin"]) + #expect(state.customPresetIds == ["custom"]) + #expect(state.canCreatePreset) + #expect(state.canSaveEditingPreset) + #endif + } + + @Test func testSharedShellBrowseSelectsFirstVisibleTab() { + #if canImport(PindropSharedNavigation) + let browseState = SettingsShell.shared.browse( + query: "palette", + selectedSection: SettingsSection.general, + initialSection: SettingsSection.general + ) + + #expect(browseState.selectedSection == .theme) + #expect(browseState.matchCount == 1) + #endif + } + + @Test func testMainWorkspaceNavigatorRoutesSettingsSelection() { + #if canImport(PindropSharedNavigation) + let state = MainWorkspaceNavigator.shared.navigateToSettings( + currentState: MainWorkspaceNavigator.shared.initialState(), + section: .hotkeys + ) + + #expect(state.selectedNavigationItem == .settings) + #expect(state.selectedSettingsSection == .hotkeys) + #endif + } + + @Test func testDashboardPresenterSharesGreetingAndStats() { + #if canImport(PindropSharedUIWorkspace) + let state = DashboardPresenter.shared.present( + records: [ + DashboardRecordSnapshot(text: "one two three", durationSeconds: 30), + DashboardRecordSnapshot(text: "four five", durationSeconds: 30), + ], + currentHour: 9, + hasDismissedHotkeyReminder: false + ) + + #expect(state.greetingKey == "Good morning") + #expect(state.totalSessions == 2) + #expect(state.totalWords == 5) + #expect(state.shouldShowHotkeyReminder) + #endif + } + + @Test func testMediaLibraryPresenterFiltersAndSortsRecords() { + #if canImport(PindropSharedUIWorkspace) + let state = MediaLibraryPresenter.shared.browse( + folders: [ + MediaFolderSnapshot(id: "folder-a", name: "Calls", itemCount: 1), + MediaFolderSnapshot(id: "folder-b", name: "Meetings", itemCount: 1), + ], + records: [ + MediaRecordSnapshot( + id: "record-older", + folderId: nil, + timestampEpochMillis: 1, + searchText: "planning session", + sortName: "Planning Session" + ), + MediaRecordSnapshot( + id: "record-newer", + folderId: nil, + timestampEpochMillis: 2, + searchText: "planning follow up", + sortName: "Planning Follow Up" + ), + ], + selectedFolderId: nil, + searchText: "planning", + sortMode: .newest + ) + + #expect(state.visibleRecordIds == ["record-newer", "record-older"]) + #expect(state.emptyStateKind == .none) + #endif + } + + @Test func testHistoryPresenterBuildsSectionsAndLoadingState() { + #if canImport(PindropSharedUIWorkspace) + let now: Int64 = 1_700_000_000_000 + let state = HistoryPresenter.shared.present( + records: [ + HistoryRecordSnapshot(id: "today", timestampEpochMillis: now), + HistoryRecordSnapshot(id: "yesterday", timestampEpochMillis: now - 86_400_000), + HistoryRecordSnapshot(id: "older", timestampEpochMillis: now - 172_800_000), + ], + totalTranscriptionsCount: 3, + searchText: "", + selectedRecordId: "yesterday", + hasLoadedInitialPage: true, + isLoadingPage: false, + errorMessage: nil, + nowEpochMillis: now, + timeZoneOffsetMinutes: 0 + ) + + #expect(state.contentStateKind == .populated) + #expect(state.selectedRecordId == "yesterday") + #expect(state.sections.count == 3) + #expect(state.sections[0].kind == .today) + #expect(state.sections[1].kind == .yesterday) + #expect(state.sections[2].kind == .date) + #endif + } + + @Test func testDictionaryPresenterSharesOrderingAndFormValidation() { + #if canImport(PindropSharedUIWorkspace) + let state = DictionaryPresenter.shared.present( + selectedSection: .replacements, + replacements: [ + ReplacementEntrySnapshot(id: "second", originals: ["beta"], replacement: "B", sortOrder: 2), + ReplacementEntrySnapshot(id: "first", originals: ["alpha"], replacement: "A", sortOrder: 1), + ], + vocabularyWords: [ + VocabularyWordSnapshot(id: "vocabulary", word: "Zebra"), + ], + primaryInput: "source", + secondaryInput: "target", + errorMessage: nil + ) + + #expect(state.totalItemCount == 3) + #expect(state.visibleReplacementIds == ["first", "second"]) + #expect(state.canAdd) + #expect(state.contentStateKind == .populated) + #endif + } + + @Test func testNotesPresenterSharesFilteringAndEmptyState() { + #if canImport(PindropSharedUIWorkspace) + let state = NotesPresenter.shared.present( + notes: [ + NoteSnapshot( + id: "note-1", + title: "Meeting Notes", + content: "Quarterly planning session", + tags: ["planning"], + updatedAtEpochMillis: 20 + ), + NoteSnapshot( + id: "note-2", + title: "Ideas", + content: "Ship desktop rewrite", + tags: ["product"], + updatedAtEpochMillis: 10 + ), + ], + searchText: "quarterly", + sortOrder: .descending, + selectedNoteId: "note-2", + errorMessage: nil + ) + + #expect(state.visibleNoteIds == ["note-1"]) + #expect(state.selectedNoteId == nil) + #expect(state.contentStateKind == .populated) + + let emptyState = NotesPresenter.shared.present( + notes: [], + searchText: "missing", + sortOrder: .ascending, + selectedNoteId: nil, + errorMessage: nil + ) + + #expect(emptyState.contentStateKind == .emptySearch) + #endif + } + + @Test func testModelsPresenterSharesBrowseState() { + #if canImport(PindropSharedUIWorkspace) + let state = ModelsPresenter.shared.browse( + models: [ + ModelCatalogEntrySnapshot( + id: "recommended", + name: "recommended", + displayName: "Recommended Local", + description: "fast local model", + providerName: "WhisperKit", + isLocal: true, + isRecommended: true, + availability: "available" + ), + ModelCatalogEntrySnapshot( + id: "cloud", + name: "cloud", + displayName: "Cloud Model", + description: "remote model", + providerName: "OpenAI", + isLocal: false, + isRecommended: false, + availability: "available" + ), + ], + selectedFilter: .recommended, + searchText: "" + ) + + #expect(state.effectiveFilter == .recommended) + #expect(state.visibleModelIds == ["recommended"]) + #expect(state.contentStateKind == .populated) + #endif + } + + @Test func testTranscribeLibraryPresenterSharesEmptyStateAndActions() { + #if canImport(PindropSharedUIWorkspace) + let browseState = MediaLibraryBrowseState( + trimmedSearchText: "", + selectedFolderId: "folder-1", + visibleFolderIds: [], + visibleRecordIds: [], + filteredFolderCount: 0, + filteredRecordCount: 0, + totalRecordCountForSelectedFolder: 0, + emptyStateKind: .folderEmpty + ) + let state = TranscribeLibraryPresenter.shared.present( + selectedFolderId: "folder-1", + selectedFolderName: "Calls", + draftLink: " https://example.com/video ", + librarySearchText: "", + browseState: browseState + ) + + #expect(state.shouldShowBackButton) + #expect(state.canSubmitDraftLink) + #expect(state.shouldShowLibraryEmptyState) + #expect(state.emptyStateTitleKey == "No items in %@") + #expect(state.emptyStateMessageKey == "Import or transcribe media while this folder is selected to save items here.") + #endif + } + @Test func testResetAllSettingsResetsThemeSettings() { let settingsStore = makeSettingsStore() defer { cleanup(settingsStore) } diff --git a/PindropTests/TranscriptionEngineTests.swift b/PindropTests/TranscriptionEngineTests.swift index 6684402..f45adbb 100644 --- a/PindropTests/TranscriptionEngineTests.swift +++ b/PindropTests/TranscriptionEngineTests.swift @@ -29,8 +29,8 @@ struct TranscriptionEngineTests { } @Test func mockEngineConformsToProtocol() { - let engine = MockTranscriptionEngine() - #expect(engine is TranscriptionEngine) + let engine: any TranscriptionEngine = MockTranscriptionEngine() + #expect(engine.state == .unloaded) } @Test func mockEngineInitialState() { diff --git a/justfile b/justfile index 510bec8..7b7b56f 100644 --- a/justfile +++ b/justfile @@ -106,13 +106,13 @@ test: # Run Kotlin Multiplatform shared-module tests shared-test: @echo "🧪 Running shared Kotlin tests..." - ./shared/gradlew --no-daemon --console=plain -p shared :core:jvmTest :feature-transcription:jvmTest + ./shared/gradlew --no-daemon --console=plain -p shared :core:jvmTest :feature-transcription:jvmTest :ui-theme:jvmTest :ui-shell:jvmTest :ui-settings:jvmTest :ui-workspace:jvmTest @echo "✅ Shared Kotlin tests complete" # Build Apple XCFrameworks for the shared Kotlin modules shared-xcframework: @echo "📦 Building shared XCFrameworks..." - ./shared/gradlew --no-daemon --console=plain -p shared :core:assemblePindropSharedCoreXCFramework :feature-transcription:assemblePindropSharedTranscriptionXCFramework + FORCE_SHARED_FRAMEWORK_BUILD=1 ./scripts/build-shared-frameworks-if-needed.sh @echo "✅ Shared XCFrameworks built" # Run integration tests only (opt-in) diff --git a/scripts/build-shared-frameworks-if-needed.sh b/scripts/build-shared-frameworks-if-needed.sh index 201a269..d566598 100755 --- a/scripts/build-shared-frameworks-if-needed.sh +++ b/scripts/build-shared-frameworks-if-needed.sh @@ -4,17 +4,36 @@ set -eu SRCROOT="${SRCROOT:-$(cd "$(dirname "$0")/.." && pwd)}" SHARED_DIR="$SRCROOT/shared" +FORCE_SHARED_FRAMEWORK_BUILD="${FORCE_SHARED_FRAMEWORK_BUILD:-0}" CORE_INFO_PLIST="$SHARED_DIR/core/build/XCFrameworks/release/PindropSharedCore.xcframework/Info.plist" TRANSCRIPTION_INFO_PLIST="$SHARED_DIR/feature-transcription/build/XCFrameworks/release/PindropSharedTranscription.xcframework/Info.plist" +UI_THEME_INFO_PLIST="$SHARED_DIR/ui-theme/build/XCFrameworks/release/PindropSharedUITheme.xcframework/Info.plist" +UI_SHELL_INFO_PLIST="$SHARED_DIR/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework/Info.plist" +UI_SETTINGS_INFO_PLIST="$SHARED_DIR/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework/Info.plist" +UI_WORKSPACE_INFO_PLIST="$SHARED_DIR/ui-workspace/build/XCFrameworks/release/PindropSharedUIWorkspace.xcframework/Info.plist" BUILD_STAMP="$SHARED_DIR/build/xcode-shared-frameworks.stamp" needs_build=0 -if [ ! -f "$CORE_INFO_PLIST" ] || [ ! -f "$TRANSCRIPTION_INFO_PLIST" ] || [ ! -f "$BUILD_STAMP" ]; then +cleanup_xcframework_outputs() { + rm -rf \ + "$SHARED_DIR/ui-shell/build/XCFrameworks/debug/PindropSharedShell.xcframework" \ + "$SHARED_DIR/ui-shell/build/XCFrameworks/release/PindropSharedShell.xcframework" \ + "$SHARED_DIR/ui-shell/build/XCFrameworks/debug/PindropSharedNavigation.xcframework" \ + "$SHARED_DIR/ui-shell/build/XCFrameworks/release/PindropSharedNavigation.xcframework" \ + "$SHARED_DIR/ui-shell/build/XCFrameworks/debug/PindropSharedUIShell.xcframework" \ + "$SHARED_DIR/ui-shell/build/XCFrameworks/release/PindropSharedUIShell.xcframework" \ + "$SHARED_DIR/ui-settings/build/XCFrameworks/debug/PindropSharedSettings.xcframework" \ + "$SHARED_DIR/ui-settings/build/XCFrameworks/release/PindropSharedSettings.xcframework" \ + "$SHARED_DIR/ui-settings/build/XCFrameworks/debug/PindropSharedUISettings.xcframework" \ + "$SHARED_DIR/ui-settings/build/XCFrameworks/release/PindropSharedUISettings.xcframework" +} + +if [ ! -f "$CORE_INFO_PLIST" ] || [ ! -f "$TRANSCRIPTION_INFO_PLIST" ] || [ ! -f "$UI_THEME_INFO_PLIST" ] || [ ! -f "$UI_SHELL_INFO_PLIST" ] || [ ! -f "$UI_SETTINGS_INFO_PLIST" ] || [ ! -f "$UI_WORKSPACE_INFO_PLIST" ] || [ ! -f "$BUILD_STAMP" ]; then needs_build=1 fi -if [ "$needs_build" -eq 0 ]; then +if [ "$FORCE_SHARED_FRAMEWORK_BUILD" -eq 0 ] && [ "$needs_build" -eq 0 ]; then newest_shared_input=$( find "$SHARED_DIR" \ \( -type d -name .gradle -o -type d -name build \) -prune \ @@ -34,15 +53,20 @@ if [ "$needs_build" -eq 0 ]; then fi fi -if [ "$needs_build" -eq 0 ]; then +if [ "$FORCE_SHARED_FRAMEWORK_BUILD" -eq 0 ] && [ "$needs_build" -eq 0 ]; then echo "Shared Kotlin frameworks are up to date; skipping Gradle." exit 0 fi echo "Building shared Kotlin frameworks..." +cleanup_xcframework_outputs cd "$SHARED_DIR" "$SHARED_DIR/gradlew" --no-daemon --console=plain -p "$SHARED_DIR" \ :core:assemblePindropSharedCoreXCFramework \ - :feature-transcription:assemblePindropSharedTranscriptionXCFramework + :feature-transcription:assemblePindropSharedTranscriptionXCFramework \ + :ui-theme:assemblePindropSharedUIThemeXCFramework \ + :ui-shell:assemblePindropSharedNavigationXCFramework \ + :ui-settings:assemblePindropSharedSettingsXCFramework \ + :ui-workspace:assemblePindropSharedUIWorkspaceXCFramework mkdir -p "$(dirname "$BUILD_STAMP")" touch "$BUILD_STAMP" diff --git a/shared/settings.gradle.kts b/shared/settings.gradle.kts index 392419f..e4d0f4b 100644 --- a/shared/settings.gradle.kts +++ b/shared/settings.gradle.kts @@ -17,3 +17,7 @@ rootProject.name = "pindrop-shared" include(":core") include(":feature-transcription") +include(":ui-shell") +include(":ui-settings") +include(":ui-theme") +include(":ui-workspace") diff --git a/shared/ui-settings/build.gradle.kts b/shared/ui-settings/build.gradle.kts new file mode 100644 index 0000000..63e9553 --- /dev/null +++ b/shared/ui-settings/build.gradle.kts @@ -0,0 +1,33 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + val macosArm64Target = macosArm64() + val macosX64Target = macosX64() + + val xcframework = XCFramework("PindropSharedSettings") + + listOf(macosArm64Target, macosX64Target).forEach { target -> + target.binaries.framework { + baseName = "PindropSharedSettings" + xcframework.add(this) + } + } + + sourceSets { + commonMain.dependencies { + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/shared/ui-settings/src/commonMain/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentation.kt b/shared/ui-settings/src/commonMain/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentation.kt new file mode 100644 index 0000000..a46b577 --- /dev/null +++ b/shared/ui-settings/src/commonMain/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentation.kt @@ -0,0 +1,363 @@ +package tech.watzon.pindrop.shared.uisettings + +enum class AIProviderCore { + OPENAI, + GOOGLE, + ANTHROPIC, + OPENROUTER, + CUSTOM, +} + +enum class CustomProviderTypeCore { + CUSTOM, + OLLAMA, + LM_STUDIO, +} + +enum class PromptTypeCore { + TRANSCRIPTION, + NOTES, +} + +data class AIProviderDefinition( + val id: AIProviderCore, + val displayName: String, + val iconKey: String, + val defaultEndpoint: String, + val apiKeyPlaceholder: String, + val isImplemented: Boolean, +) + +data class CustomProviderDefinition( + val id: CustomProviderTypeCore, + val displayName: String, + val iconKey: String, + val storageKey: String, + val requiresApiKey: Boolean, + val supportsModelListing: Boolean, + val defaultEndpoint: String, + val defaultModelsEndpoint: String?, + val apiKeyPlaceholder: String, + val endpointPlaceholder: String, + val modelPlaceholder: String, +) + +data class AIModelSnapshot( + val id: String, + val name: String, + val summary: String?, +) + +data class PromptPresetSnapshot( + val id: String, + val name: String, + val prompt: String, + val isBuiltIn: Boolean, + val sortOrder: Int, +) + +data class AIProviderSelection( + val provider: AIProviderCore, + val customProvider: CustomProviderTypeCore, +) + +data class AIEnhancementDraft( + val selectedProvider: AIProviderCore, + val selectedCustomProvider: CustomProviderTypeCore, + val apiKey: String, + val selectedModel: String, + val customModel: String, + val enhancementPrompt: String, + val noteEnhancementPrompt: String, + val selectedPromptType: PromptTypeCore, + val selectedPresetId: String?, + val customEndpointText: String, + val availableModels: List, + val modelErrorMessage: String?, + val isLoadingModels: Boolean, + val aiEnhancementEnabled: Boolean, +) + +data class AIEnhancementViewState( + val canSave: Boolean, + val isApiKeyOptional: Boolean, + val currentApiKeyPlaceholder: String, + val apiKeyHelpText: String?, + val shouldShowCustomProviderPicker: Boolean, + val shouldShowModelPicker: Boolean, + val shouldShowCustomModelField: Boolean, + val shouldShowCustomEndpointField: Boolean, + val emptyModelsMessageKey: String, + val selectedPresetId: String?, + val validatedPresetId: String?, + val isBuiltInPresetSelected: Boolean, + val isSelectedPromptReadOnly: Boolean, + val selectedPromptCharacterCount: Int, +) + +data class PromptPresetManagementState( + val builtInPresetIds: List, + val customPresetIds: List, + val canCreatePreset: Boolean, + val canSaveEditingPreset: Boolean, +) + +object AISettingsCatalog { + private val providerDefinitions = listOf( + AIProviderDefinition( + id = AIProviderCore.OPENAI, + displayName = "OpenAI", + iconKey = "openai", + defaultEndpoint = "https://api.openai.com/v1/chat/completions", + apiKeyPlaceholder = "sk-...", + isImplemented = true, + ), + AIProviderDefinition( + id = AIProviderCore.GOOGLE, + displayName = "Google", + iconKey = "google", + defaultEndpoint = "https://generativelanguage.googleapis.com/v1beta", + apiKeyPlaceholder = "AIza...", + isImplemented = false, + ), + AIProviderDefinition( + id = AIProviderCore.ANTHROPIC, + displayName = "Anthropic", + iconKey = "anthropic", + defaultEndpoint = "https://api.anthropic.com/v1/messages", + apiKeyPlaceholder = "sk-ant-...", + isImplemented = true, + ), + AIProviderDefinition( + id = AIProviderCore.OPENROUTER, + displayName = "OpenRouter", + iconKey = "openrouter", + defaultEndpoint = "https://openrouter.ai/api/v1/chat/completions", + apiKeyPlaceholder = "sk-or-...", + isImplemented = true, + ), + AIProviderDefinition( + id = AIProviderCore.CUSTOM, + displayName = "Custom/Local", + iconKey = "server", + defaultEndpoint = "", + apiKeyPlaceholder = "Enter API key", + isImplemented = true, + ), + ) + + private val customProviderDefinitions = listOf( + CustomProviderDefinition( + id = CustomProviderTypeCore.CUSTOM, + displayName = "Custom", + iconKey = "server", + storageKey = "custom", + requiresApiKey = true, + supportsModelListing = false, + defaultEndpoint = "", + defaultModelsEndpoint = null, + apiKeyPlaceholder = "Enter API key", + endpointPlaceholder = "https://your-api.com/v1/chat/completions", + modelPlaceholder = "e.g., gpt-4o", + ), + CustomProviderDefinition( + id = CustomProviderTypeCore.OLLAMA, + displayName = "Ollama", + iconKey = "hardDrive", + storageKey = "ollama", + requiresApiKey = false, + supportsModelListing = true, + defaultEndpoint = "http://localhost:11434/v1/chat/completions", + defaultModelsEndpoint = "http://localhost:11434/v1/models", + apiKeyPlaceholder = "Optional (usually not needed)", + endpointPlaceholder = "http://localhost:11434/v1/chat/completions", + modelPlaceholder = "e.g., llama3.2", + ), + CustomProviderDefinition( + id = CustomProviderTypeCore.LM_STUDIO, + displayName = "LM Studio", + iconKey = "hardDrive", + storageKey = "lm-studio", + requiresApiKey = false, + supportsModelListing = true, + defaultEndpoint = "http://localhost:1234/v1/chat/completions", + defaultModelsEndpoint = "http://localhost:1234/v1/models", + apiKeyPlaceholder = "Optional unless auth is enabled", + endpointPlaceholder = "http://localhost:1234/v1/chat/completions", + modelPlaceholder = "e.g., local-model", + ), + ) + + fun providers(): List = providerDefinitions + + fun provider(id: AIProviderCore): AIProviderDefinition { + return providerDefinitions.first { it.id == id } + } + + fun customProviders(): List = customProviderDefinitions + + fun customProvider(id: CustomProviderTypeCore): CustomProviderDefinition { + return customProviderDefinitions.first { it.id == id } + } + + fun defaultModelIdentifier(provider: AIProviderCore): String = when (provider) { + AIProviderCore.OPENROUTER -> "openai/gpt-4o-mini" + AIProviderCore.OPENAI -> "gpt-4o-mini" + AIProviderCore.ANTHROPIC -> "claude-haiku-4-5" + else -> "gpt-4o-mini" + } + + fun inferProviderSelection( + endpoint: String?, + fallbackCustomProvider: CustomProviderTypeCore, + currentProviderIsCustom: Boolean, + ): AIProviderSelection { + val resolvedEndpoint = endpoint?.trim().orEmpty() + return when { + resolvedEndpoint.contains("openai.com", ignoreCase = true) -> + AIProviderSelection(AIProviderCore.OPENAI, fallbackCustomProvider) + resolvedEndpoint.contains("anthropic.com", ignoreCase = true) -> + AIProviderSelection(AIProviderCore.ANTHROPIC, fallbackCustomProvider) + resolvedEndpoint.contains("googleapis.com", ignoreCase = true) -> + AIProviderSelection(AIProviderCore.GOOGLE, fallbackCustomProvider) + resolvedEndpoint.contains("openrouter.ai", ignoreCase = true) -> + AIProviderSelection(AIProviderCore.OPENROUTER, fallbackCustomProvider) + resolvedEndpoint.isNotEmpty() -> + AIProviderSelection(AIProviderCore.CUSTOM, inferCustomProvider(endpoint = resolvedEndpoint, fallback = fallbackCustomProvider)) + currentProviderIsCustom -> + AIProviderSelection(AIProviderCore.CUSTOM, fallbackCustomProvider) + else -> + AIProviderSelection(AIProviderCore.OPENAI, fallbackCustomProvider) + } + } + + fun inferCustomProvider( + endpoint: String?, + fallback: CustomProviderTypeCore, + ): CustomProviderTypeCore { + val resolvedEndpoint = endpoint?.trim().orEmpty() + return when { + resolvedEndpoint.contains("localhost:11434", ignoreCase = true) -> CustomProviderTypeCore.OLLAMA + resolvedEndpoint.contains("localhost:1234", ignoreCase = true) -> CustomProviderTypeCore.LM_STUDIO + else -> fallback + } + } +} + +object AIEnhancementPresenter { + fun present( + draft: AIEnhancementDraft, + presets: List, + ): AIEnhancementViewState { + val provider = AISettingsCatalog.provider(draft.selectedProvider) + val customProvider = AISettingsCatalog.customProvider(draft.selectedCustomProvider) + val validatedPresetId = draft.selectedPresetId?.takeIf { selectedId -> + presets.any { it.id == selectedId } + } + val selectedPreset = validatedPresetId?.let { selectedId -> + presets.firstOrNull { it.id == selectedId } + } + val selectedPromptCharacterCount = when (draft.selectedPromptType) { + PromptTypeCore.TRANSCRIPTION -> draft.enhancementPrompt.length + PromptTypeCore.NOTES -> draft.noteEnhancementPrompt.length + } + val isBuiltInPresetSelected = selectedPreset?.isBuiltIn == true + val isSelectedPromptReadOnly = + draft.selectedPromptType == PromptTypeCore.TRANSCRIPTION && isBuiltInPresetSelected + val canSave = when { + !provider.isImplemented -> false + requiresApiKey(draft.selectedProvider, draft.selectedCustomProvider) && + draft.apiKey.isBlank() -> false + draft.selectedProvider == AIProviderCore.CUSTOM && + draft.customEndpointText.isBlank() -> false + draft.selectedProvider == AIProviderCore.CUSTOM && + selectedModelText(draft).isBlank() -> false + draft.selectedProvider in setOf(AIProviderCore.OPENROUTER, AIProviderCore.OPENAI, AIProviderCore.ANTHROPIC) && + draft.selectedModel.isBlank() -> false + else -> true + } + + return AIEnhancementViewState( + canSave = canSave, + isApiKeyOptional = draft.selectedProvider == AIProviderCore.CUSTOM && !customProvider.requiresApiKey, + currentApiKeyPlaceholder = if (draft.selectedProvider == AIProviderCore.CUSTOM) { + customProvider.apiKeyPlaceholder + } else { + provider.apiKeyPlaceholder + }, + apiKeyHelpText = when { + draft.selectedProvider != AIProviderCore.CUSTOM -> null + draft.selectedCustomProvider == CustomProviderTypeCore.OLLAMA -> + "Ollama usually does not require authentication for local requests." + draft.selectedCustomProvider == CustomProviderTypeCore.LM_STUDIO -> + "LM Studio only needs a token if local server authentication is enabled." + else -> null + }, + shouldShowCustomProviderPicker = draft.selectedProvider == AIProviderCore.CUSTOM, + shouldShowModelPicker = shouldShowModelPicker(draft), + shouldShowCustomModelField = draft.selectedProvider == AIProviderCore.CUSTOM && !customProvider.supportsModelListing, + shouldShowCustomEndpointField = draft.selectedProvider == AIProviderCore.CUSTOM, + emptyModelsMessageKey = emptyModelsMessageKey(draft), + selectedPresetId = draft.selectedPresetId, + validatedPresetId = validatedPresetId, + isBuiltInPresetSelected = isBuiltInPresetSelected, + isSelectedPromptReadOnly = isSelectedPromptReadOnly, + selectedPromptCharacterCount = selectedPromptCharacterCount, + ) + } + + private fun shouldShowModelPicker(draft: AIEnhancementDraft): Boolean { + val customProvider = AISettingsCatalog.customProvider(draft.selectedCustomProvider) + return draft.selectedProvider in setOf( + AIProviderCore.OPENROUTER, + AIProviderCore.OPENAI, + AIProviderCore.ANTHROPIC, + ) || (draft.selectedProvider == AIProviderCore.CUSTOM && customProvider.supportsModelListing) + } + + private fun emptyModelsMessageKey(draft: AIEnhancementDraft): String = when { + draft.isLoadingModels -> "Loading models..." + draft.selectedProvider == AIProviderCore.OPENAI && draft.apiKey.isBlank() -> + "Enter an OpenAI API key to load models." + !draft.modelErrorMessage.isNullOrBlank() -> + "Unable to load models. Try refresh." + draft.selectedProvider == AIProviderCore.CUSTOM && + AISettingsCatalog.customProvider(draft.selectedCustomProvider).supportsModelListing -> + "No models available. Try Refresh or enter a model ID manually." + else -> "No models available." + } + + private fun requiresApiKey( + provider: AIProviderCore, + customProvider: CustomProviderTypeCore, + ): Boolean { + return when (provider) { + AIProviderCore.CUSTOM -> AISettingsCatalog.customProvider(customProvider).requiresApiKey + AIProviderCore.OPENAI, AIProviderCore.OPENROUTER, AIProviderCore.ANTHROPIC, AIProviderCore.GOOGLE -> true + } + } + + private fun selectedModelText(draft: AIEnhancementDraft): String { + val customProvider = AISettingsCatalog.customProvider(draft.selectedCustomProvider) + return if (customProvider.supportsModelListing) draft.selectedModel else draft.customModel + } +} + +object PromptPresetPresenter { + fun present( + presets: List, + newName: String, + newPrompt: String, + editingPresetId: String?, + editName: String, + editPrompt: String, + ): PromptPresetManagementState { + val sortedPresets = presets.sortedBy(PromptPresetSnapshot::sortOrder) + return PromptPresetManagementState( + builtInPresetIds = sortedPresets.filter(PromptPresetSnapshot::isBuiltIn).map(PromptPresetSnapshot::id), + customPresetIds = sortedPresets.filterNot(PromptPresetSnapshot::isBuiltIn).map(PromptPresetSnapshot::id), + canCreatePreset = newName.isNotBlank() && newPrompt.isNotBlank(), + canSaveEditingPreset = editingPresetId != null && editName.isNotBlank() && editPrompt.isNotBlank(), + ) + } +} diff --git a/shared/ui-settings/src/commonTest/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentationTest.kt b/shared/ui-settings/src/commonTest/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentationTest.kt new file mode 100644 index 0000000..bc1d254 --- /dev/null +++ b/shared/ui-settings/src/commonTest/kotlin/tech/watzon/pindrop/shared/uisettings/AISettingsPresentationTest.kt @@ -0,0 +1,92 @@ +package tech.watzon.pindrop.shared.uisettings + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class AISettingsPresentationTest { + @Test + fun aiPresenterHandlesCustomProviderValidationAndFallbackMessages() { + val state = AIEnhancementPresenter.present( + draft = AIEnhancementDraft( + selectedProvider = AIProviderCore.CUSTOM, + selectedCustomProvider = CustomProviderTypeCore.OLLAMA, + apiKey = "", + selectedModel = "", + customModel = "", + enhancementPrompt = "Prompt", + noteEnhancementPrompt = "Note prompt", + selectedPromptType = PromptTypeCore.TRANSCRIPTION, + selectedPresetId = null, + customEndpointText = "http://localhost:11434/v1/chat/completions", + availableModels = emptyList(), + modelErrorMessage = null, + isLoadingModels = false, + aiEnhancementEnabled = true, + ), + presets = emptyList(), + ) + + assertFalse(state.canSave) + assertTrue(state.isApiKeyOptional) + assertTrue(state.shouldShowCustomProviderPicker) + assertTrue(state.shouldShowModelPicker) + assertEquals("No models available. Try Refresh or enter a model ID manually.", state.emptyModelsMessageKey) + } + + @Test + fun aiPresenterTreatsBuiltInTranscriptionPresetAsReadOnly() { + val state = AIEnhancementPresenter.present( + draft = AIEnhancementDraft( + selectedProvider = AIProviderCore.OPENAI, + selectedCustomProvider = CustomProviderTypeCore.CUSTOM, + apiKey = "sk-test", + selectedModel = "gpt-4o-mini", + customModel = "", + enhancementPrompt = "Prompt", + noteEnhancementPrompt = "Note prompt", + selectedPromptType = PromptTypeCore.TRANSCRIPTION, + selectedPresetId = "builtin", + customEndpointText = "", + availableModels = emptyList(), + modelErrorMessage = null, + isLoadingModels = false, + aiEnhancementEnabled = true, + ), + presets = listOf( + PromptPresetSnapshot( + id = "builtin", + name = "Built In", + prompt = "Prompt", + isBuiltIn = true, + sortOrder = 0, + ) + ), + ) + + assertTrue(state.isBuiltInPresetSelected) + assertTrue(state.isSelectedPromptReadOnly) + assertEquals("builtin", state.validatedPresetId) + } + + @Test + fun promptPresetPresenterBuildsSectionsAndValidation() { + val state = PromptPresetPresenter.present( + presets = listOf( + PromptPresetSnapshot("b", "Built In", "One", true, 0), + PromptPresetSnapshot("c", "Custom", "Two", false, 1), + ), + newName = "New Preset", + newPrompt = "Prompt", + editingPresetId = "c", + editName = "Updated", + editPrompt = "Updated prompt", + ) + + assertEquals(listOf("b"), state.builtInPresetIds) + assertEquals(listOf("c"), state.customPresetIds) + assertTrue(state.canCreatePreset) + assertTrue(state.canSaveEditingPreset) + } +} diff --git a/shared/ui-shell/build.gradle.kts b/shared/ui-shell/build.gradle.kts new file mode 100644 index 0000000..09f8c75 --- /dev/null +++ b/shared/ui-shell/build.gradle.kts @@ -0,0 +1,33 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + val macosArm64Target = macosArm64() + val macosX64Target = macosX64() + + val xcframework = XCFramework("PindropSharedNavigation") + + listOf(macosArm64Target, macosX64Target).forEach { target -> + target.binaries.framework { + baseName = "PindropSharedNavigation" + xcframework.add(this) + } + } + + sourceSets { + commonMain.dependencies { + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/shared/ui-shell/src/commonMain/kotlin/tech/watzon/pindrop/shared/uishell/ShellState.kt b/shared/ui-shell/src/commonMain/kotlin/tech/watzon/pindrop/shared/uishell/ShellState.kt new file mode 100644 index 0000000..b891fa7 --- /dev/null +++ b/shared/ui-shell/src/commonMain/kotlin/tech/watzon/pindrop/shared/uishell/ShellState.kt @@ -0,0 +1,172 @@ +package tech.watzon.pindrop.shared.uishell + +enum class MainNavigationItem { + HOME, + HISTORY, + TRANSCRIBE, + MODELS, + NOTES, + DICTIONARY, + SETTINGS, +} + +enum class SettingsSection { + GENERAL, + THEME, + HOTKEYS, + AI, + UPDATE, + ABOUT, +} + +data class SettingsSectionDefinition( + val id: SettingsSection, + val titleKey: String, + val subtitleKey: String, + val systemIcon: String, + val searchKeywords: List, + val accessibilityIdentifier: String, +) + +data class SettingsBrowseState( + val query: String, + val selectedSection: SettingsSection, + val filteredSections: List, + val matchCount: Int, +) + +data class MainWorkspaceState( + val selectedNavigationItem: MainNavigationItem, + val selectedSettingsSection: SettingsSection, +) + +object SettingsShell { + private val sectionDefinitions = listOf( + SettingsSectionDefinition( + id = SettingsSection.GENERAL, + titleKey = "General", + subtitleKey = "Output, audio, interface, and everyday behavior", + systemIcon = "gear", + searchKeywords = listOf( + "output", "clipboard", "direct insert", "space", "microphone", "audio", + "input", "floating indicator", "dictionary", "launch at login", "dock", + "mute", "pause media", "reset", "language", "locale", "transcription language", + "interface language" + ), + accessibilityIdentifier = "settings.tab.general", + ), + SettingsSectionDefinition( + id = SettingsSection.THEME, + titleKey = "Theme", + subtitleKey = "Light, dark, and curated palette presets", + systemIcon = "paintbrush", + searchKeywords = listOf("appearance", "theme", "light", "dark", "system", "preset", "palette"), + accessibilityIdentifier = "settings.tab.theme", + ), + SettingsSectionDefinition( + id = SettingsSection.HOTKEYS, + titleKey = "Hotkeys", + subtitleKey = "Configure keyboard shortcuts for recording and note capture", + systemIcon = "keyboard", + searchKeywords = listOf("shortcut", "toggle recording", "push to talk", "copy last transcript", "note capture", "keyboard"), + accessibilityIdentifier = "settings.tab.hotkeys", + ), + SettingsSectionDefinition( + id = SettingsSection.AI, + titleKey = "AI Enhancement", + subtitleKey = "Providers, prompts, and vibe mode controls", + systemIcon = "sparkles", + searchKeywords = listOf( + "provider", "api key", "endpoint", "prompt", "preset", "vibe mode", + "clipboard context", "ui context", "model", "enhancement" + ), + accessibilityIdentifier = "settings.tab.ai-enhancement", + ), + SettingsSectionDefinition( + id = SettingsSection.UPDATE, + titleKey = "Update", + subtitleKey = "Automatic updates and manual update checks", + systemIcon = "arrow.triangle.2.circlepath", + searchKeywords = listOf("updates", "automatic updates", "check now", "version"), + accessibilityIdentifier = "settings.tab.update", + ), + SettingsSectionDefinition( + id = SettingsSection.ABOUT, + titleKey = "About", + subtitleKey = "App info, acknowledgments, support, and logs", + systemIcon = "info.circle", + searchKeywords = listOf("support", "logs", "github", "license", "system info", "version"), + accessibilityIdentifier = "settings.tab.about", + ), + ) + + fun sections(): List = sectionDefinitions + + fun section(id: SettingsSection): SettingsSectionDefinition { + return sectionDefinitions.first { it.id == id } + } + + fun browse( + query: String, + selectedSection: SettingsSection?, + initialSection: SettingsSection, + ): SettingsBrowseState { + val normalizedQuery = query.trim() + val filtered = if (normalizedQuery.isEmpty()) { + sectionDefinitions.map { it.id } + } else { + val queryLower = normalizedQuery.lowercase() + sectionDefinitions + .filter { definition -> + val searchableText = buildString { + append(definition.titleKey) + append(' ') + append(definition.subtitleKey) + append(' ') + append(definition.searchKeywords.joinToString(" ")) + }.lowercase() + searchableText.contains(queryLower) + } + .map { it.id } + } + + val resolvedSelection = when { + filtered.isEmpty() -> selectedSection ?: initialSection + selectedSection != null && filtered.contains(selectedSection) -> selectedSection + else -> filtered.first() + } + + return SettingsBrowseState( + query = query, + selectedSection = resolvedSelection, + filteredSections = filtered, + matchCount = filtered.size, + ) + } +} + +object MainWorkspaceNavigator { + fun initialState(): MainWorkspaceState { + return MainWorkspaceState( + selectedNavigationItem = MainNavigationItem.HOME, + selectedSettingsSection = SettingsSection.GENERAL, + ) + } + + fun navigateTo( + currentState: MainWorkspaceState, + item: MainNavigationItem, + ): MainWorkspaceState { + return currentState.copy(selectedNavigationItem = item) + } + + fun navigateToSettings( + currentState: MainWorkspaceState, + section: SettingsSection, + ): MainWorkspaceState { + return currentState.copy( + selectedNavigationItem = MainNavigationItem.SETTINGS, + selectedSettingsSection = section, + ) + } +} diff --git a/shared/ui-shell/src/commonTest/kotlin/tech/watzon/pindrop/shared/uishell/ShellStateTest.kt b/shared/ui-shell/src/commonTest/kotlin/tech/watzon/pindrop/shared/uishell/ShellStateTest.kt new file mode 100644 index 0000000..036d9b9 --- /dev/null +++ b/shared/ui-shell/src/commonTest/kotlin/tech/watzon/pindrop/shared/uishell/ShellStateTest.kt @@ -0,0 +1,47 @@ +package tech.watzon.pindrop.shared.uishell + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ShellStateTest { + @Test + fun browseFallsBackToFirstVisibleSectionWhenSelectionFilteredOut() { + val state = SettingsShell.browse( + query = "palette", + selectedSection = SettingsSection.GENERAL, + initialSection = SettingsSection.GENERAL, + ) + + assertEquals(SettingsSection.THEME, state.selectedSection) + assertEquals(listOf(SettingsSection.THEME), state.filteredSections) + } + + @Test + fun browseReturnsAllSectionsForEmptyQuery() { + val state = SettingsShell.browse( + query = "", + selectedSection = null, + initialSection = SettingsSection.GENERAL, + ) + + assertEquals(SettingsSection.GENERAL, state.selectedSection) + assertEquals(SettingsShell.sections().size, state.matchCount) + } + + @Test + fun workspaceNavigationPromotesSettingsSectionSelection() { + val state = MainWorkspaceNavigator.navigateToSettings( + currentState = MainWorkspaceNavigator.initialState(), + section = SettingsSection.AI, + ) + + assertEquals(MainNavigationItem.SETTINGS, state.selectedNavigationItem) + assertEquals(SettingsSection.AI, state.selectedSettingsSection) + } + + @Test + fun sectionDefinitionsExposeStableAccessibilityIdentifiers() { + assertTrue(SettingsShell.sections().any { it.accessibilityIdentifier == "settings.tab.theme" }) + } +} diff --git a/shared/ui-theme/build.gradle.kts b/shared/ui-theme/build.gradle.kts new file mode 100644 index 0000000..2c52944 --- /dev/null +++ b/shared/ui-theme/build.gradle.kts @@ -0,0 +1,36 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + val macosArm64Target = macosArm64() + val macosX64Target = macosX64() + + val xcframework = XCFramework("PindropSharedUITheme") + + listOf( + macosArm64Target, + macosX64Target, + ).forEach { target -> + target.binaries.framework { + baseName = "PindropSharedUITheme" + xcframework.add(this) + } + } + + sourceSets { + commonMain.dependencies { + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/shared/ui-theme/src/commonMain/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngine.kt b/shared/ui-theme/src/commonMain/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngine.kt new file mode 100644 index 0000000..a4940ed --- /dev/null +++ b/shared/ui-theme/src/commonMain/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngine.kt @@ -0,0 +1,590 @@ +package tech.watzon.pindrop.shared.uitheme + +import kotlin.math.roundToInt + +enum class ThemeMode { + SYSTEM, + LIGHT, + DARK, +} + +enum class ThemeVariant { + LIGHT, + DARK, +} + +enum class SurfaceStyle { + SOLID, + ELEVATED, + MUTED, + TRANSLUCENT, + OVERLAY_STRONG, +} + +enum class SidebarTreatment { + SOLID, + TRANSLUCENT, +} + +enum class OverlayTreatment { + SOLID, + BLURRED, + HIGH_CONTRAST, +} + +enum class WindowChromeTreatment { + STANDARD, + UNIFIED, + TRANSPARENT_TITLEBAR, +} + +enum class TypographyDesign { + ROUNDED, + MONOSPACED, +} + +data class ThemeProfile( + val accentHex: String, + val backgroundHex: String, + val foregroundHex: String, + val contrast: Double, + val successHex: String, + val warningHex: String, + val dangerHex: String, + val processingHex: String, +) + +data class ThemePreset( + val id: String, + val title: String, + val summary: String, + val badgeText: String, + val badgeBackgroundHex: String, + val badgeForegroundHex: String, + val lightTheme: ThemeProfile, + val darkTheme: ThemeProfile, +) { + fun profileFor(variant: ThemeVariant): ThemeProfile { + return when (variant) { + ThemeVariant.LIGHT -> lightTheme + ThemeVariant.DARK -> darkTheme + } + } +} + +data class ThemeSelection( + val mode: ThemeMode, + val lightPresetId: String, + val darkPresetId: String, +) + +data class ThemeCapabilities( + val supportsTranslucentSidebar: Boolean, + val supportsWindowMaterial: Boolean, + val supportsOverlayBlur: Boolean, + val supportsNativeVibrancy: Boolean, + val supportsUnifiedTitlebar: Boolean, +) + +data class ColorTokenValue( + val red: Int, + val green: Int, + val blue: Int, + val alpha: Int = 255, +) + +data class ShadowTokenValue( + val color: ColorTokenValue, + val radius: Double, + val x: Double, + val y: Double, +) + +data class TypographyTokenValue( + val size: Double, + val weight: Int, + val design: TypographyDesign, +) + +data class SpacingScale( + val xxs: Double, + val xs: Double, + val sm: Double, + val md: Double, + val lg: Double, + val xl: Double, + val xxl: Double, + val xxxl: Double, + val huge: Double, +) + +data class RadiusScale( + val sm: Double, + val md: Double, + val lg: Double, + val xl: Double, + val full: Double, +) + +data class ShadowScale( + val sm: ShadowTokenValue, + val md: ShadowTokenValue, + val lg: ShadowTokenValue, +) + +data class TypographyScale( + val largeTitle: TypographyTokenValue, + val title: TypographyTokenValue, + val headline: TypographyTokenValue, + val subheadline: TypographyTokenValue, + val body: TypographyTokenValue, + val bodySmall: TypographyTokenValue, + val caption: TypographyTokenValue, + val tiny: TypographyTokenValue, + val mono: TypographyTokenValue, + val monoSmall: TypographyTokenValue, + val statLarge: TypographyTokenValue, + val statMedium: TypographyTokenValue, +) + +data class ThemePreviewModel( + val presetId: String, + val title: String, + val summary: String, + val badgeText: String, + val badgeBackground: ColorTokenValue, + val badgeForeground: ColorTokenValue, + val background: ColorTokenValue, + val foreground: ColorTokenValue, + val accent: ColorTokenValue, +) + +data class ResolvedThemeTokens( + val windowBackground: ColorTokenValue, + val sidebarBackground: ColorTokenValue, + val contentBackground: ColorTokenValue, + val surfaceBackground: ColorTokenValue, + val elevatedSurface: ColorTokenValue, + val mutedSurface: ColorTokenValue, + val inputBackground: ColorTokenValue, + val inputBorder: ColorTokenValue, + val inputBorderFocused: ColorTokenValue, + val accent: ColorTokenValue, + val accentSecondary: ColorTokenValue, + val accentBackground: ColorTokenValue, + val textPrimary: ColorTokenValue, + val textSecondary: ColorTokenValue, + val textTertiary: ColorTokenValue, + val border: ColorTokenValue, + val divider: ColorTokenValue, + val success: ColorTokenValue, + val successBackground: ColorTokenValue, + val warning: ColorTokenValue, + val warningBackground: ColorTokenValue, + val error: ColorTokenValue, + val errorBackground: ColorTokenValue, + val recording: ColorTokenValue, + val processing: ColorTokenValue, + val sidebarItemHover: ColorTokenValue, + val sidebarItemActive: ColorTokenValue, + val overlaySurface: ColorTokenValue, + val overlaySurfaceStrong: ColorTokenValue, + val overlayLine: ColorTokenValue, + val overlayTextPrimary: ColorTokenValue, + val overlayTextSecondary: ColorTokenValue, + val overlayWaveform: ColorTokenValue, + val overlayRecording: ColorTokenValue, + val overlayWarning: ColorTokenValue, + val overlayTooltipAccent: ColorTokenValue, + val shadow: ColorTokenValue, + val spacing: SpacingScale, + val radius: RadiusScale, + val shadowScale: ShadowScale, + val typography: TypographyScale, +) + +data class ResolvedTheme( + val effectiveVariant: ThemeVariant, + val selectedPreset: ThemePreset, + val requestedSidebarTreatment: SidebarTreatment, + val adaptedSidebarTreatment: SidebarTreatment, + val requestedOverlayTreatment: OverlayTreatment, + val adaptedOverlayTreatment: OverlayTreatment, + val requestedWindowChromeTreatment: WindowChromeTreatment, + val adaptedWindowChromeTreatment: WindowChromeTreatment, + val tokens: ResolvedThemeTokens, +) + +object ThemeCatalog { + const val defaultPresetId: String = "pindrop" + + private val presetList = listOf( + ThemePreset( + id = "pindrop", + title = "Pindrop", + summary = "Warm editorial surfaces with a copper signal accent.", + badgeText = "Pd", + badgeBackgroundHex = "#F7F1E8", + badgeForegroundHex = "#C56E42", + lightTheme = ThemeProfile("#C56E42", "#F7F1E8", "#221A14", 50.0, "#2E8B67", "#A9692D", "#C95452", "#4D78D6"), + darkTheme = ThemeProfile("#E19260", "#15120F", "#F2E5D8", 66.0, "#53B48A", "#D09049", "#E5726E", "#74A2FF"), + ), + ThemePreset( + id = "paper", + title = "Paper", + summary = "Quiet parchment tones with ink-forward contrast.", + badgeText = "Aa", + badgeBackgroundHex = "#FBF7EF", + badgeForegroundHex = "#2E4E73", + lightTheme = ThemeProfile("#2E4E73", "#FBF7EF", "#1A1712", 46.0, "#2D7D5A", "#9C6B24", "#BD514A", "#3A67C3"), + darkTheme = ThemeProfile("#89A9D4", "#1A1816", "#F4EEE5", 62.0, "#58B48B", "#D09B53", "#E87C74", "#7FA7FF"), + ), + ThemePreset( + id = "harbor", + title = "Harbor", + summary = "Cool blue-gray chrome with a crisp marine accent.", + badgeText = "Hb", + badgeBackgroundHex = "#EFF5F7", + badgeForegroundHex = "#14708A", + lightTheme = ThemeProfile("#14708A", "#EFF5F7", "#14232B", 48.0, "#2F8663", "#B0702D", "#C85652", "#2F78D0"), + darkTheme = ThemeProfile("#5AB4D4", "#0F171C", "#E3F0F5", 67.0, "#5FB98C", "#D59A4F", "#E3716D", "#69A8FF"), + ), + ThemePreset( + id = "evergreen", + title = "Evergreen", + summary = "Forest-tinted utility palette with a calm studio feel.", + badgeText = "Eg", + badgeBackgroundHex = "#F3F5EE", + badgeForegroundHex = "#4D7A4A", + lightTheme = ThemeProfile("#4D7A4A", "#F3F5EE", "#1C2019", 47.0, "#3A8B5B", "#AA6D26", "#B84F49", "#4A74C9"), + darkTheme = ThemeProfile("#87B57D", "#101411", "#E6EEE1", 65.0, "#64BC85", "#D29648", "#DF6F68", "#7EA7FF"), + ), + ThemePreset( + id = "graphite", + title = "Graphite", + summary = "Neutral monochrome with a high-signal cobalt edge.", + badgeText = "Gr", + badgeBackgroundHex = "#F4F5F7", + badgeForegroundHex = "#4B65D6", + lightTheme = ThemeProfile("#4B65D6", "#F4F5F7", "#16181D", 49.0, "#2C8A67", "#A66821", "#C34C50", "#507BFF"), + darkTheme = ThemeProfile("#7D93FF", "#101114", "#ECEFF4", 70.0, "#5DBD93", "#D69D55", "#E77A80", "#87A7FF"), + ), + ThemePreset( + id = "signal", + title = "Signal", + summary = "Dark broadcast palette with a vivid red-orange pulse.", + badgeText = "Sg", + badgeBackgroundHex = "#181211", + badgeForegroundHex = "#F06D4F", + lightTheme = ThemeProfile("#D95E45", "#FBF4F1", "#251816", 51.0, "#2C8863", "#AF6A21", "#C94E4B", "#466AD4"), + darkTheme = ThemeProfile("#F06D4F", "#181211", "#F5E7E2", 72.0, "#53B98A", "#DD9745", "#F5847A", "#7EA4FF"), + ), + ) + + fun presets(): List = presetList + + fun preset(id: String?): ThemePreset { + return presetList.firstOrNull { it.id == id } + ?: presetList.firstOrNull { it.id == defaultPresetId } + ?: presetList.first() + } + + fun previewModels(variant: ThemeVariant): List { + return presetList.map { preset -> + val profile = preset.profileFor(variant) + ThemePreviewModel( + presetId = preset.id, + title = preset.title, + summary = preset.summary, + badgeText = preset.badgeText, + badgeBackground = colorFromHex(preset.badgeBackgroundHex) ?: colorFromHex("#FFFFFF")!!, + badgeForeground = colorFromHex(preset.badgeForegroundHex) ?: colorFromHex("#000000")!!, + background = colorFromHex(profile.backgroundHex) ?: colorFromHex("#FFFFFF")!!, + foreground = colorFromHex(profile.foregroundHex) ?: colorFromHex("#000000")!!, + accent = colorFromHex(profile.accentHex) ?: colorFromHex("#FF7A00")!!, + ) + } + } +} + +object ThemeEngine { + fun resolveTheme( + selection: ThemeSelection, + systemVariant: ThemeVariant, + capabilities: ThemeCapabilities, + ): ResolvedTheme { + val effectiveVariant = when (selection.mode) { + ThemeMode.SYSTEM -> systemVariant + ThemeMode.LIGHT -> ThemeVariant.LIGHT + ThemeMode.DARK -> ThemeVariant.DARK + } + val presetId = when (effectiveVariant) { + ThemeVariant.LIGHT -> selection.lightPresetId + ThemeVariant.DARK -> selection.darkPresetId + } + val preset = ThemeCatalog.preset(presetId) + val profile = preset.profileFor(effectiveVariant) + val requestedSidebarTreatment = SidebarTreatment.TRANSLUCENT + val requestedOverlayTreatment = OverlayTreatment.BLURRED + val requestedWindowChromeTreatment = WindowChromeTreatment.TRANSPARENT_TITLEBAR + + return ResolvedTheme( + effectiveVariant = effectiveVariant, + selectedPreset = preset, + requestedSidebarTreatment = requestedSidebarTreatment, + adaptedSidebarTreatment = if (capabilities.supportsTranslucentSidebar) { + requestedSidebarTreatment + } else { + SidebarTreatment.SOLID + }, + requestedOverlayTreatment = requestedOverlayTreatment, + adaptedOverlayTreatment = if (capabilities.supportsOverlayBlur) { + requestedOverlayTreatment + } else { + OverlayTreatment.HIGH_CONTRAST + }, + requestedWindowChromeTreatment = requestedWindowChromeTreatment, + adaptedWindowChromeTreatment = when { + capabilities.supportsUnifiedTitlebar && capabilities.supportsWindowMaterial -> WindowChromeTreatment.TRANSPARENT_TITLEBAR + capabilities.supportsUnifiedTitlebar -> WindowChromeTreatment.UNIFIED + else -> WindowChromeTreatment.STANDARD + }, + tokens = resolveTokens(profile, effectiveVariant == ThemeVariant.DARK), + ) + } + + private fun resolveTokens(profile: ThemeProfile, isDark: Boolean): ResolvedThemeTokens { + val background = colorFromHex(profile.backgroundHex) + ?: if (isDark) color(20, 20, 23) else color(247, 245, 240) + val foreground = colorFromHex(profile.foregroundHex) + ?: if (isDark) color(255, 255, 255) else color(0, 0, 0) + val accentBase = colorFromHex(profile.accentHex) ?: color(255, 122, 0) + val successBase = colorFromHex(profile.successHex) ?: color(52, 199, 89) + val warningBase = colorFromHex(profile.warningHex) ?: color(255, 149, 0) + val dangerBase = colorFromHex(profile.dangerHex) ?: color(255, 59, 48) + val processingBase = colorFromHex(profile.processingHex) ?: color(0, 122, 255) + val contrast = profile.contrast.coerceIn(20.0, 80.0) / 100.0 + + val colors = if (isDark) { + DarkTokens( + windowBackground = background, + sidebarBackground = background.lighter(0.035), + contentBackground = background.lighter(0.015), + surfaceBackground = background.lighter(0.055 + contrast * 0.035), + elevatedSurface = background.lighter(0.09 + contrast * 0.045), + mutedSurface = foreground.withAlpha(0.06 + contrast * 0.02), + inputBackground = background.lighter(0.075 + contrast * 0.03), + inputBorder = foreground.withAlpha(0.14 + contrast * 0.06), + inputBorderFocused = accentBase.withAlpha(0.78), + accent = accentBase, + accentSecondary = accentBase.mixed(foreground, 0.22), + accentBackground = accentBase.mixed(background, 0.86), + textPrimary = foreground, + textSecondary = foreground.withAlpha(0.72), + textTertiary = foreground.withAlpha(0.48), + border = foreground.withAlpha(0.11 + contrast * 0.05), + divider = foreground.withAlpha(0.08 + contrast * 0.04), + success = successBase, + successBackground = successBase.mixed(background, 0.88), + warning = warningBase, + warningBackground = warningBase.mixed(background, 0.88), + error = dangerBase, + errorBackground = dangerBase.mixed(background, 0.89), + recording = dangerBase, + processing = processingBase, + sidebarItemHover = foreground.withAlpha(0.065), + sidebarItemActive = accentBase.mixed(background, 0.82), + overlaySurface = background.darker(0.24), + overlaySurfaceStrong = background.darker(0.32), + overlayLine = foreground.withAlpha(0.18), + overlayTextPrimary = color(255, 255, 255).withAlpha(0.96), + overlayTextSecondary = color(255, 255, 255).withAlpha(0.74), + overlayWaveform = accentBase.mixed(color(255, 255, 255), 0.24), + overlayRecording = dangerBase.mixed(color(255, 255, 255), 0.12), + overlayWarning = warningBase, + overlayTooltipAccent = accentBase.mixed(color(255, 255, 255), 0.3), + shadow = color(0, 0, 0), + ) + } else { + DarkTokens( + windowBackground = background, + sidebarBackground = background.darker(0.018), + contentBackground = background.lighter(0.005), + surfaceBackground = background.lighter(0.025), + elevatedSurface = background.darker(0.02 + contrast * 0.01), + mutedSurface = foreground.withAlpha(0.045 + contrast * 0.02), + inputBackground = background.lighter(0.015), + inputBorder = foreground.withAlpha(0.14 + contrast * 0.04), + inputBorderFocused = accentBase.withAlpha(0.72), + accent = accentBase, + accentSecondary = accentBase.mixed(foreground, 0.18), + accentBackground = accentBase.mixed(background, 0.92), + textPrimary = foreground, + textSecondary = foreground.withAlpha(0.7), + textTertiary = foreground.withAlpha(0.48), + border = foreground.withAlpha(0.1 + contrast * 0.04), + divider = foreground.withAlpha(0.07 + contrast * 0.03), + success = successBase, + successBackground = successBase.mixed(background, 0.93), + warning = warningBase, + warningBackground = warningBase.mixed(background, 0.93), + error = dangerBase, + errorBackground = dangerBase.mixed(background, 0.94), + recording = dangerBase, + processing = processingBase, + sidebarItemHover = foreground.withAlpha(0.05), + sidebarItemActive = accentBase.mixed(background, 0.87), + overlaySurface = background.darker(0.82), + overlaySurfaceStrong = background.darker(0.9), + overlayLine = color(255, 255, 255).withAlpha(0.14), + overlayTextPrimary = color(255, 255, 255).withAlpha(0.96), + overlayTextSecondary = color(255, 255, 255).withAlpha(0.74), + overlayWaveform = accentBase.mixed(color(255, 255, 255), 0.42), + overlayRecording = dangerBase.mixed(color(255, 255, 255), 0.18), + overlayWarning = warningBase.mixed(color(255, 255, 255), 0.18), + overlayTooltipAccent = accentBase.mixed(color(255, 255, 255), 0.42), + shadow = foreground, + ) + } + + val spacing = SpacingScale(4.0, 6.0, 10.0, 14.0, 18.0, 24.0, 32.0, 40.0, 56.0) + val radius = RadiusScale(8.0, 12.0, 18.0, 24.0, 9999.0) + val shadowScale = ShadowScale( + sm = ShadowTokenValue(colors.shadow.withAlpha(0.08), 6.0, 0.0, 2.0), + md = ShadowTokenValue(colors.shadow.withAlpha(0.14), 16.0, 0.0, 8.0), + lg = ShadowTokenValue(colors.shadow.withAlpha(0.2), 30.0, 0.0, 18.0), + ) + val typography = TypographyScale( + largeTitle = TypographyTokenValue(30.0, 600, TypographyDesign.ROUNDED), + title = TypographyTokenValue(21.0, 600, TypographyDesign.ROUNDED), + headline = TypographyTokenValue(16.0, 600, TypographyDesign.ROUNDED), + subheadline = TypographyTokenValue(14.0, 600, TypographyDesign.ROUNDED), + body = TypographyTokenValue(14.0, 400, TypographyDesign.ROUNDED), + bodySmall = TypographyTokenValue(13.0, 400, TypographyDesign.ROUNDED), + caption = TypographyTokenValue(12.0, 500, TypographyDesign.ROUNDED), + tiny = TypographyTokenValue(11.0, 500, TypographyDesign.ROUNDED), + mono = TypographyTokenValue(13.0, 500, TypographyDesign.MONOSPACED), + monoSmall = TypographyTokenValue(11.0, 500, TypographyDesign.MONOSPACED), + statLarge = TypographyTokenValue(32.0, 700, TypographyDesign.ROUNDED), + statMedium = TypographyTokenValue(24.0, 600, TypographyDesign.ROUNDED), + ) + + return ResolvedThemeTokens( + windowBackground = colors.windowBackground, + sidebarBackground = colors.sidebarBackground, + contentBackground = colors.contentBackground, + surfaceBackground = colors.surfaceBackground, + elevatedSurface = colors.elevatedSurface, + mutedSurface = colors.mutedSurface, + inputBackground = colors.inputBackground, + inputBorder = colors.inputBorder, + inputBorderFocused = colors.inputBorderFocused, + accent = colors.accent, + accentSecondary = colors.accentSecondary, + accentBackground = colors.accentBackground, + textPrimary = colors.textPrimary, + textSecondary = colors.textSecondary, + textTertiary = colors.textTertiary, + border = colors.border, + divider = colors.divider, + success = colors.success, + successBackground = colors.successBackground, + warning = colors.warning, + warningBackground = colors.warningBackground, + error = colors.error, + errorBackground = colors.errorBackground, + recording = colors.recording, + processing = colors.processing, + sidebarItemHover = colors.sidebarItemHover, + sidebarItemActive = colors.sidebarItemActive, + overlaySurface = colors.overlaySurface, + overlaySurfaceStrong = colors.overlaySurfaceStrong, + overlayLine = colors.overlayLine, + overlayTextPrimary = colors.overlayTextPrimary, + overlayTextSecondary = colors.overlayTextSecondary, + overlayWaveform = colors.overlayWaveform, + overlayRecording = colors.overlayRecording, + overlayWarning = colors.overlayWarning, + overlayTooltipAccent = colors.overlayTooltipAccent, + shadow = colors.shadow, + spacing = spacing, + radius = radius, + shadowScale = shadowScale, + typography = typography, + ) + } +} + +private data class DarkTokens( + val windowBackground: ColorTokenValue, + val sidebarBackground: ColorTokenValue, + val contentBackground: ColorTokenValue, + val surfaceBackground: ColorTokenValue, + val elevatedSurface: ColorTokenValue, + val mutedSurface: ColorTokenValue, + val inputBackground: ColorTokenValue, + val inputBorder: ColorTokenValue, + val inputBorderFocused: ColorTokenValue, + val accent: ColorTokenValue, + val accentSecondary: ColorTokenValue, + val accentBackground: ColorTokenValue, + val textPrimary: ColorTokenValue, + val textSecondary: ColorTokenValue, + val textTertiary: ColorTokenValue, + val border: ColorTokenValue, + val divider: ColorTokenValue, + val success: ColorTokenValue, + val successBackground: ColorTokenValue, + val warning: ColorTokenValue, + val warningBackground: ColorTokenValue, + val error: ColorTokenValue, + val errorBackground: ColorTokenValue, + val recording: ColorTokenValue, + val processing: ColorTokenValue, + val sidebarItemHover: ColorTokenValue, + val sidebarItemActive: ColorTokenValue, + val overlaySurface: ColorTokenValue, + val overlaySurfaceStrong: ColorTokenValue, + val overlayLine: ColorTokenValue, + val overlayTextPrimary: ColorTokenValue, + val overlayTextSecondary: ColorTokenValue, + val overlayWaveform: ColorTokenValue, + val overlayRecording: ColorTokenValue, + val overlayWarning: ColorTokenValue, + val overlayTooltipAccent: ColorTokenValue, + val shadow: ColorTokenValue, +) + +private fun color(red: Int, green: Int, blue: Int, alpha: Int = 255): ColorTokenValue { + return ColorTokenValue(red, green, blue, alpha) +} + +private fun colorFromHex(hex: String): ColorTokenValue? { + val cleaned = hex.trim().removePrefix("#") + if (cleaned.length != 6) return null + val hexValue = cleaned.toIntOrNull(16) ?: return null + return color( + red = (hexValue shr 16) and 0xFF, + green = (hexValue shr 8) and 0xFF, + blue = hexValue and 0xFF, + ) +} + +private fun ColorTokenValue.withAlpha(alpha: Double): ColorTokenValue { + return copy(alpha = (alpha.coerceIn(0.0, 1.0) * 255.0).roundToInt()) +} + +private fun ColorTokenValue.mixed(other: ColorTokenValue, ratio: Double): ColorTokenValue { + val clampedRatio = ratio.coerceIn(0.0, 1.0) + val inverse = 1.0 - clampedRatio + return color( + red = ((red * inverse) + (other.red * clampedRatio)).roundToInt(), + green = ((green * inverse) + (other.green * clampedRatio)).roundToInt(), + blue = ((blue * inverse) + (other.blue * clampedRatio)).roundToInt(), + alpha = ((alpha * inverse) + (other.alpha * clampedRatio)).roundToInt(), + ) +} + +private fun ColorTokenValue.lighter(amount: Double): ColorTokenValue = mixed(color(255, 255, 255), amount) + +private fun ColorTokenValue.darker(amount: Double): ColorTokenValue = mixed(color(0, 0, 0), amount) diff --git a/shared/ui-theme/src/commonTest/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngineTest.kt b/shared/ui-theme/src/commonTest/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngineTest.kt new file mode 100644 index 0000000..bd037fa --- /dev/null +++ b/shared/ui-theme/src/commonTest/kotlin/tech/watzon/pindrop/shared/uitheme/ThemeEngineTest.kt @@ -0,0 +1,85 @@ +package tech.watzon.pindrop.shared.uitheme + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ThemeEngineTest { + private val macCapabilities = ThemeCapabilities( + supportsTranslucentSidebar = true, + supportsWindowMaterial = true, + supportsOverlayBlur = true, + supportsNativeVibrancy = true, + supportsUnifiedTitlebar = true, + ) + + @Test + fun presetLookupFallsBackToDefault() { + assertEquals(ThemeCatalog.defaultPresetId, ThemeCatalog.preset("missing").id) + } + + @Test + fun resolveThemeUsesSystemVariantWhenModeIsSystem() { + val resolved = ThemeEngine.resolveTheme( + selection = ThemeSelection(ThemeMode.SYSTEM, "paper", "signal"), + systemVariant = ThemeVariant.DARK, + capabilities = macCapabilities, + ) + + assertEquals(ThemeVariant.DARK, resolved.effectiveVariant) + assertEquals("signal", resolved.selectedPreset.id) + } + + @Test + fun resolveThemeClampsContrastAndProducesOpaqueWindowBackground() { + val preset = ThemePreset( + id = "contrast-test", + title = "Contrast", + summary = "Contrast", + badgeText = "Ct", + badgeBackgroundHex = "#FFFFFF", + badgeForegroundHex = "#000000", + lightTheme = ThemeProfile("#445566", "#EEEEEE", "#111111", 500.0, "#00FF00", "#FFAA00", "#FF0000", "#0000FF"), + darkTheme = ThemeProfile("#445566", "#111111", "#EEEEEE", -100.0, "#00FF00", "#FFAA00", "#FF0000", "#0000FF"), + ) + + assertEquals("contrast-test", preset.id) + + val resolved = ThemeEngine.resolveTheme( + selection = ThemeSelection(ThemeMode.LIGHT, "contrast-test", "contrast-test"), + systemVariant = ThemeVariant.LIGHT, + capabilities = macCapabilities, + ) + + assertEquals(255, resolved.tokens.windowBackground.alpha) + assertTrue(resolved.tokens.border.alpha > 0) + } + + @Test + fun unsupportedCapabilitiesProduceDeterministicFallbackTreatments() { + val resolved = ThemeEngine.resolveTheme( + selection = ThemeSelection(ThemeMode.DARK, "pindrop", "signal"), + systemVariant = ThemeVariant.DARK, + capabilities = ThemeCapabilities( + supportsTranslucentSidebar = false, + supportsWindowMaterial = false, + supportsOverlayBlur = false, + supportsNativeVibrancy = false, + supportsUnifiedTitlebar = false, + ), + ) + + assertEquals(SidebarTreatment.SOLID, resolved.adaptedSidebarTreatment) + assertEquals(OverlayTreatment.HIGH_CONTRAST, resolved.adaptedOverlayTreatment) + assertEquals(WindowChromeTreatment.STANDARD, resolved.adaptedWindowChromeTreatment) + } + + @Test + fun previewModelsExposeStablePaletteMetadata() { + val previews = ThemeCatalog.previewModels(ThemeVariant.LIGHT) + + assertFalse(previews.isEmpty()) + assertTrue(previews.any { it.presetId == ThemeCatalog.defaultPresetId }) + } +} diff --git a/shared/ui-workspace/build.gradle.kts b/shared/ui-workspace/build.gradle.kts new file mode 100644 index 0000000..ed71848 --- /dev/null +++ b/shared/ui-workspace/build.gradle.kts @@ -0,0 +1,33 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + val macosArm64Target = macosArm64() + val macosX64Target = macosX64() + + val xcframework = XCFramework("PindropSharedUIWorkspace") + + listOf(macosArm64Target, macosX64Target).forEach { target -> + target.binaries.framework { + baseName = "PindropSharedUIWorkspace" + xcframework.add(this) + } + } + + sourceSets { + commonMain.dependencies { + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/shared/ui-workspace/src/commonMain/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentation.kt b/shared/ui-workspace/src/commonMain/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentation.kt new file mode 100644 index 0000000..874b93b --- /dev/null +++ b/shared/ui-workspace/src/commonMain/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentation.kt @@ -0,0 +1,567 @@ +package tech.watzon.pindrop.shared.uiworkspace + +import kotlin.math.max + +data class DashboardRecordSnapshot( + val text: String, + val durationSeconds: Double, +) + +data class DashboardViewState( + val greetingKey: String, + val totalSessions: Int, + val totalWords: Int, + val totalDurationSeconds: Double, + val averageWordsPerMinute: Double, + val shouldShowHotkeyReminder: Boolean, +) + +object DashboardPresenter { + fun present( + records: List, + currentHour: Int, + hasDismissedHotkeyReminder: Boolean, + ): DashboardViewState { + val totalSessions = records.size + val totalWords = records.sumOf { record -> + record.text.trim() + .split(Regex("\\s+")) + .count { token -> token.isNotEmpty() } + } + val totalDurationSeconds = records.sumOf(DashboardRecordSnapshot::durationSeconds) + val minutes = totalDurationSeconds / 60.0 + val averageWordsPerMinute = if (totalDurationSeconds > 0) { + totalWords.toDouble() / max(minutes, 1.0) + } else { + 0.0 + } + + return DashboardViewState( + greetingKey = greetingKeyForHour(currentHour), + totalSessions = totalSessions, + totalWords = totalWords, + totalDurationSeconds = totalDurationSeconds, + averageWordsPerMinute = averageWordsPerMinute, + shouldShowHotkeyReminder = !hasDismissedHotkeyReminder, + ) + } + + private fun greetingKeyForHour(currentHour: Int): String = when (currentHour) { + in 5..11 -> "Good morning" + in 12..16 -> "Good afternoon" + in 17..21 -> "Good evening" + else -> "Good night" + } +} + +enum class MediaLibrarySortModeCore { + NEWEST, + OLDEST, + NAME_ASCENDING, + NAME_DESCENDING, +} + +data class MediaFolderSnapshot( + val id: String, + val name: String, + val itemCount: Int, +) + +data class MediaRecordSnapshot( + val id: String, + val folderId: String?, + val timestampEpochMillis: Long, + val searchText: String, + val sortName: String, +) + +enum class MediaLibraryEmptyStateKind { + NONE, + LIBRARY_EMPTY, + SEARCH_EMPTY, + FOLDER_EMPTY, + FOLDER_SEARCH_EMPTY, +} + +data class MediaLibraryBrowseState( + val trimmedSearchText: String, + val selectedFolderId: String?, + val visibleFolderIds: List, + val visibleRecordIds: List, + val filteredFolderCount: Int, + val filteredRecordCount: Int, + val totalRecordCountForSelectedFolder: Int, + val emptyStateKind: MediaLibraryEmptyStateKind, +) + +object MediaLibraryPresenter { + fun browse( + folders: List, + records: List, + selectedFolderId: String?, + searchText: String, + sortMode: MediaLibrarySortModeCore, + ): MediaLibraryBrowseState { + val trimmedSearchText = searchText.trim() + val selectedFolder = folders.firstOrNull { it.id == selectedFolderId } + + val visibleFolders = if (selectedFolder == null) { + if (trimmedSearchText.isEmpty()) { + folders + } else { + folders.filter { folder -> + folder.name.contains(trimmedSearchText, ignoreCase = true) + } + } + } else { + emptyList() + } + + val visibleRecords = records + .asSequence() + .filter { record -> + when { + selectedFolder == null -> record.folderId == null + else -> record.folderId == selectedFolder.id + } + } + .filter { record -> + trimmedSearchText.isEmpty() || + record.searchText.contains(trimmedSearchText, ignoreCase = true) + } + .sortedWith(sortComparator(sortMode)) + .toList() + + val totalRecordCountForSelectedFolder = records.count { it.folderId == selectedFolder?.id } + val emptyStateKind = when { + visibleFolders.isNotEmpty() || visibleRecords.isNotEmpty() -> MediaLibraryEmptyStateKind.NONE + selectedFolder != null && trimmedSearchText.isNotEmpty() -> MediaLibraryEmptyStateKind.FOLDER_SEARCH_EMPTY + selectedFolder != null -> MediaLibraryEmptyStateKind.FOLDER_EMPTY + trimmedSearchText.isNotEmpty() -> MediaLibraryEmptyStateKind.SEARCH_EMPTY + else -> MediaLibraryEmptyStateKind.LIBRARY_EMPTY + } + + return MediaLibraryBrowseState( + trimmedSearchText = trimmedSearchText, + selectedFolderId = selectedFolder?.id, + visibleFolderIds = visibleFolders.map(MediaFolderSnapshot::id), + visibleRecordIds = visibleRecords.map(MediaRecordSnapshot::id), + filteredFolderCount = visibleFolders.size, + filteredRecordCount = visibleRecords.size, + totalRecordCountForSelectedFolder = totalRecordCountForSelectedFolder, + emptyStateKind = emptyStateKind, + ) + } + + private fun sortComparator(sortMode: MediaLibrarySortModeCore): Comparator = when (sortMode) { + MediaLibrarySortModeCore.NEWEST -> compareByDescending(MediaRecordSnapshot::timestampEpochMillis) + MediaLibrarySortModeCore.OLDEST -> compareBy(MediaRecordSnapshot::timestampEpochMillis) + MediaLibrarySortModeCore.NAME_ASCENDING -> compareBy(String.CASE_INSENSITIVE_ORDER, MediaRecordSnapshot::sortName) + MediaLibrarySortModeCore.NAME_DESCENDING -> compareByDescending(String.CASE_INSENSITIVE_ORDER, MediaRecordSnapshot::sortName) + } +} + +data class HistoryRecordSnapshot( + val id: String, + val timestampEpochMillis: Long, +) + +enum class HistoryContentStateKind { + LOADING, + EMPTY_LIBRARY, + EMPTY_SEARCH, + POPULATED, + ERROR, +} + +enum class HistorySectionKind { + TODAY, + YESTERDAY, + DATE, +} + +data class HistorySectionState( + val kind: HistorySectionKind, + val representativeTimestampEpochMillis: Long, + val recordIds: List, +) + +data class HistoryViewState( + val trimmedSearchText: String, + val totalTranscriptionsCount: Int, + val selectedRecordId: String?, + val contentStateKind: HistoryContentStateKind, + val canExport: Boolean, + val shouldShowLoadingMoreIndicator: Boolean, + val sections: List, +) + +object HistoryPresenter { + fun present( + records: List, + totalTranscriptionsCount: Int, + searchText: String, + selectedRecordId: String?, + hasLoadedInitialPage: Boolean, + isLoadingPage: Boolean, + errorMessage: String?, + nowEpochMillis: Long, + timeZoneOffsetMinutes: Int, + ): HistoryViewState { + val trimmedSearchText = searchText.trim() + val contentStateKind = when { + !errorMessage.isNullOrBlank() -> HistoryContentStateKind.ERROR + !hasLoadedInitialPage -> HistoryContentStateKind.LOADING + totalTranscriptionsCount == 0 && trimmedSearchText.isNotEmpty() -> HistoryContentStateKind.EMPTY_SEARCH + totalTranscriptionsCount == 0 -> HistoryContentStateKind.EMPTY_LIBRARY + else -> HistoryContentStateKind.POPULATED + } + + val visibleRecordIds = records.map(HistoryRecordSnapshot::id).toSet() + val normalizedSelectedRecordId = selectedRecordId?.takeIf { visibleRecordIds.contains(it) } + + return HistoryViewState( + trimmedSearchText = trimmedSearchText, + totalTranscriptionsCount = totalTranscriptionsCount, + selectedRecordId = normalizedSelectedRecordId, + contentStateKind = contentStateKind, + canExport = totalTranscriptionsCount > 0, + shouldShowLoadingMoreIndicator = isLoadingPage && hasLoadedInitialPage && records.isNotEmpty(), + sections = groupSections(records, nowEpochMillis, timeZoneOffsetMinutes), + ) + } + + private fun groupSections( + records: List, + nowEpochMillis: Long, + timeZoneOffsetMinutes: Int, + ): List { + if (records.isEmpty()) return emptyList() + + val todayDay = epochDay(nowEpochMillis, timeZoneOffsetMinutes) + val grouped = linkedMapOf, MutableList>() + + for (record in records.sortedByDescending(HistoryRecordSnapshot::timestampEpochMillis)) { + val recordDay = epochDay(record.timestampEpochMillis, timeZoneOffsetMinutes) + val kind = when (recordDay) { + todayDay -> HistorySectionKind.TODAY + todayDay - 1 -> HistorySectionKind.YESTERDAY + else -> HistorySectionKind.DATE + } + val key = when (kind) { + HistorySectionKind.TODAY -> kind to todayDay + HistorySectionKind.YESTERDAY -> kind to (todayDay - 1) + HistorySectionKind.DATE -> kind to recordDay + } + grouped.getOrPut(key) { mutableListOf() }.add(record) + } + + return grouped.entries.map { (key, bucket) -> + HistorySectionState( + kind = key.first, + representativeTimestampEpochMillis = bucket.first().timestampEpochMillis, + recordIds = bucket.map(HistoryRecordSnapshot::id), + ) + } + } + + private fun epochDay(epochMillis: Long, timeZoneOffsetMinutes: Int): Long { + val adjustedMillis = epochMillis + timeZoneOffsetMinutes.toLong() * 60_000L + return floorDiv(adjustedMillis, 86_400_000L) + } + + private fun floorDiv(value: Long, divisor: Long): Long { + var quotient = value / divisor + val remainder = value % divisor + if (remainder != 0L && (value xor divisor) < 0) { + quotient -= 1 + } + return quotient + } +} + +enum class DictionarySectionCore { + REPLACEMENTS, + VOCABULARY, +} + +enum class DictionaryContentStateKind { + POPULATED, + EMPTY, + ERROR, +} + +data class ReplacementEntrySnapshot( + val id: String, + val originals: List, + val replacement: String, + val sortOrder: Int, +) + +data class VocabularyWordSnapshot( + val id: String, + val word: String, +) + +data class DictionaryViewState( + val selectedSection: DictionarySectionCore, + val totalItemCount: Int, + val visibleReplacementIds: List, + val visibleVocabularyIds: List, + val canAdd: Boolean, + val contentStateKind: DictionaryContentStateKind, +) + +object DictionaryPresenter { + fun present( + selectedSection: DictionarySectionCore, + replacements: List, + vocabularyWords: List, + primaryInput: String, + secondaryInput: String, + errorMessage: String?, + ): DictionaryViewState { + val sortedReplacementIds = replacements + .sortedBy(ReplacementEntrySnapshot::sortOrder) + .map(ReplacementEntrySnapshot::id) + val sortedVocabularyIds = vocabularyWords + .sortedBy { it.word.lowercase() } + .map(VocabularyWordSnapshot::id) + val canAdd = when (selectedSection) { + DictionarySectionCore.REPLACEMENTS -> primaryInput.trim().isNotEmpty() && secondaryInput.trim().isNotEmpty() + DictionarySectionCore.VOCABULARY -> primaryInput.trim().isNotEmpty() + } + val selectedContentIsEmpty = when (selectedSection) { + DictionarySectionCore.REPLACEMENTS -> sortedReplacementIds.isEmpty() + DictionarySectionCore.VOCABULARY -> sortedVocabularyIds.isEmpty() + } + + return DictionaryViewState( + selectedSection = selectedSection, + totalItemCount = replacements.size + vocabularyWords.size, + visibleReplacementIds = sortedReplacementIds, + visibleVocabularyIds = sortedVocabularyIds, + canAdd = canAdd, + contentStateKind = when { + !errorMessage.isNullOrBlank() -> DictionaryContentStateKind.ERROR + selectedContentIsEmpty -> DictionaryContentStateKind.EMPTY + else -> DictionaryContentStateKind.POPULATED + }, + ) + } +} + +enum class NotesSortOrderCore { + ASCENDING, + DESCENDING, +} + +enum class NotesContentStateKind { + POPULATED, + EMPTY_LIBRARY, + EMPTY_SEARCH, + ERROR, +} + +data class NoteSnapshot( + val id: String, + val title: String, + val content: String, + val tags: List, + val updatedAtEpochMillis: Long, +) + +data class NotesViewState( + val trimmedSearchText: String, + val sortOrder: NotesSortOrderCore, + val selectedNoteId: String?, + val visibleNoteIds: List, + val totalVisibleCount: Int, + val contentStateKind: NotesContentStateKind, +) + +object NotesPresenter { + fun present( + notes: List, + searchText: String, + sortOrder: NotesSortOrderCore, + selectedNoteId: String?, + errorMessage: String?, + ): NotesViewState { + val trimmedSearchText = searchText.trim() + val sortedNotes = when (sortOrder) { + NotesSortOrderCore.ASCENDING -> notes.sortedBy(NoteSnapshot::updatedAtEpochMillis) + NotesSortOrderCore.DESCENDING -> notes.sortedByDescending(NoteSnapshot::updatedAtEpochMillis) + } + val filteredNotes = if (trimmedSearchText.isEmpty()) { + sortedNotes + } else { + sortedNotes.filter { note -> + note.title.contains(trimmedSearchText, ignoreCase = true) || + note.content.contains(trimmedSearchText, ignoreCase = true) || + note.tags.any { tag -> tag.contains(trimmedSearchText, ignoreCase = true) } + } + } + val visibleNoteIds = filteredNotes.map(NoteSnapshot::id) + + return NotesViewState( + trimmedSearchText = trimmedSearchText, + sortOrder = sortOrder, + selectedNoteId = selectedNoteId?.takeIf { candidate -> visibleNoteIds.contains(candidate) }, + visibleNoteIds = visibleNoteIds, + totalVisibleCount = visibleNoteIds.size, + contentStateKind = when { + !errorMessage.isNullOrBlank() -> NotesContentStateKind.ERROR + visibleNoteIds.isNotEmpty() -> NotesContentStateKind.POPULATED + trimmedSearchText.isNotEmpty() -> NotesContentStateKind.EMPTY_SEARCH + else -> NotesContentStateKind.EMPTY_LIBRARY + }, + ) + } +} + +enum class ModelsFilterCore { + RECOMMENDED, + LOCAL, + CLOUD, + COMING_SOON, + ALL, +} + +enum class ModelsContentStateKind { + POPULATED, + EMPTY_LIBRARY, + EMPTY_SEARCH, +} + +data class ModelCatalogEntrySnapshot( + val id: String, + val name: String, + val displayName: String, + val description: String, + val providerName: String, + val isLocal: Boolean, + val isRecommended: Boolean, + val availability: String, +) + +data class ModelsBrowseState( + val trimmedSearchText: String, + val selectedFilter: ModelsFilterCore, + val effectiveFilter: ModelsFilterCore, + val visibleModelIds: List, + val contentStateKind: ModelsContentStateKind, +) + +object ModelsPresenter { + fun browse( + models: List, + selectedFilter: ModelsFilterCore, + searchText: String, + ): ModelsBrowseState { + val trimmedSearchText = searchText.trim() + val effectiveFilter = if (trimmedSearchText.isEmpty()) selectedFilter else ModelsFilterCore.ALL + val filteredModels = models.filter { model -> + matchesFilter(model, effectiveFilter) && matchesSearch(model, trimmedSearchText) + } + + return ModelsBrowseState( + trimmedSearchText = trimmedSearchText, + selectedFilter = selectedFilter, + effectiveFilter = effectiveFilter, + visibleModelIds = filteredModels.map(ModelCatalogEntrySnapshot::id), + contentStateKind = when { + filteredModels.isNotEmpty() -> ModelsContentStateKind.POPULATED + trimmedSearchText.isNotEmpty() -> ModelsContentStateKind.EMPTY_SEARCH + else -> ModelsContentStateKind.EMPTY_LIBRARY + }, + ) + } + + private fun matchesFilter(model: ModelCatalogEntrySnapshot, filter: ModelsFilterCore): Boolean = when (filter) { + ModelsFilterCore.RECOMMENDED -> model.isRecommended + ModelsFilterCore.LOCAL -> model.isLocal && model.availability == "available" + ModelsFilterCore.CLOUD -> !model.isLocal && model.availability == "available" + ModelsFilterCore.COMING_SOON -> model.availability == "comingSoon" + ModelsFilterCore.ALL -> true + } + + private fun matchesSearch(model: ModelCatalogEntrySnapshot, query: String): Boolean { + if (query.isEmpty()) return true + return model.displayName.contains(query, ignoreCase = true) || + model.name.contains(query, ignoreCase = true) || + model.description.contains(query, ignoreCase = true) || + model.providerName.contains(query, ignoreCase = true) + } +} + +data class TranscribeJobSnapshot( + val stage: String, + val requestDisplayName: String, + val progress: Double?, + val errorMessage: String?, + val detail: String, +) + +data class TranscribeLibraryViewState( + val selectedFolderId: String?, + val selectedFolderName: String?, + val trimmedSearchText: String, + val filteredFolderCount: Int, + val filteredRecordCount: Int, + val totalRecordCountForSelectedFolder: Int, + val shouldShowBackButton: Boolean, + val canSubmitDraftLink: Boolean, + val shouldShowDraftLinkClearButton: Boolean, + val shouldShowLibraryEmptyState: Boolean, + val emptyStateTitleKey: String, + val emptyStateMessageKey: String, + val emptyStateIconName: String, +) + +object TranscribeLibraryPresenter { + fun present( + selectedFolderId: String?, + selectedFolderName: String?, + draftLink: String, + librarySearchText: String, + browseState: MediaLibraryBrowseState, + ): TranscribeLibraryViewState { + val trimmedDraftLink = draftLink.trim() + val emptyStateTitleKey = when (browseState.emptyStateKind) { + MediaLibraryEmptyStateKind.FOLDER_EMPTY -> "No items in %@" + MediaLibraryEmptyStateKind.FOLDER_SEARCH_EMPTY, + MediaLibraryEmptyStateKind.SEARCH_EMPTY, + MediaLibraryEmptyStateKind.NONE -> "No results found" + MediaLibraryEmptyStateKind.LIBRARY_EMPTY -> "No media transcriptions yet" + } + val emptyStateMessageKey = when (browseState.emptyStateKind) { + MediaLibraryEmptyStateKind.FOLDER_EMPTY -> "Import or transcribe media while this folder is selected to save items here." + MediaLibraryEmptyStateKind.FOLDER_SEARCH_EMPTY -> "Try a different search term in this folder." + MediaLibraryEmptyStateKind.LIBRARY_EMPTY -> "Imported files and web links will appear here once processing completes." + MediaLibraryEmptyStateKind.SEARCH_EMPTY, + MediaLibraryEmptyStateKind.NONE -> "Try a different search term." + } + val emptyStateIconName = if (browseState.trimmedSearchText.isEmpty()) { + "folder.badge.questionmark" + } else { + "magnifyingglass" + } + + return TranscribeLibraryViewState( + selectedFolderId = selectedFolderId, + selectedFolderName = selectedFolderName, + trimmedSearchText = librarySearchText.trim(), + filteredFolderCount = browseState.filteredFolderCount, + filteredRecordCount = browseState.filteredRecordCount, + totalRecordCountForSelectedFolder = browseState.totalRecordCountForSelectedFolder, + shouldShowBackButton = selectedFolderId != null, + canSubmitDraftLink = trimmedDraftLink.isNotEmpty(), + shouldShowDraftLinkClearButton = trimmedDraftLink.isNotEmpty(), + shouldShowLibraryEmptyState = browseState.emptyStateKind != MediaLibraryEmptyStateKind.NONE, + emptyStateTitleKey = emptyStateTitleKey, + emptyStateMessageKey = emptyStateMessageKey, + emptyStateIconName = emptyStateIconName, + ) + } +} diff --git a/shared/ui-workspace/src/commonTest/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentationTest.kt b/shared/ui-workspace/src/commonTest/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentationTest.kt new file mode 100644 index 0000000..b1b66a2 --- /dev/null +++ b/shared/ui-workspace/src/commonTest/kotlin/tech/watzon/pindrop/shared/uiworkspace/WorkspacePresentationTest.kt @@ -0,0 +1,246 @@ +package tech.watzon.pindrop.shared.uiworkspace + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class WorkspacePresentationTest { + @Test + fun dashboardPresenterCalculatesGreetingAndStats() { + val state = DashboardPresenter.present( + records = listOf( + DashboardRecordSnapshot(text = "one two three", durationSeconds = 30.0), + DashboardRecordSnapshot(text = "four five", durationSeconds = 30.0), + ), + currentHour = 9, + hasDismissedHotkeyReminder = false, + ) + + assertEquals("Good morning", state.greetingKey) + assertEquals(2, state.totalSessions) + assertEquals(5, state.totalWords) + assertEquals(5.0, state.averageWordsPerMinute) + assertTrue(state.shouldShowHotkeyReminder) + } + + @Test + fun mediaLibraryPresenterFiltersFoldersAndRecords() { + val state = MediaLibraryPresenter.browse( + folders = listOf( + MediaFolderSnapshot(id = "folder-a", name = "Calls", itemCount = 1), + MediaFolderSnapshot(id = "folder-b", name = "Meetings", itemCount = 1), + ), + records = listOf( + MediaRecordSnapshot( + id = "record-1", + folderId = null, + timestampEpochMillis = 30, + searchText = "Quarterly planning review", + sortName = "Quarterly planning review", + ), + MediaRecordSnapshot( + id = "record-2", + folderId = "folder-a", + timestampEpochMillis = 20, + searchText = "Call summary", + sortName = "Call summary", + ), + ), + selectedFolderId = null, + searchText = "plan", + sortMode = MediaLibrarySortModeCore.NEWEST, + ) + + assertEquals(emptyList(), state.visibleFolderIds) + assertEquals(listOf("record-1"), state.visibleRecordIds) + assertEquals(MediaLibraryEmptyStateKind.NONE, state.emptyStateKind) + } + + @Test + fun mediaLibraryPresenterReportsFolderEmptyStates() { + val state = MediaLibraryPresenter.browse( + folders = listOf(MediaFolderSnapshot(id = "folder-a", name = "Calls", itemCount = 0)), + records = emptyList(), + selectedFolderId = "folder-a", + searchText = "", + sortMode = MediaLibrarySortModeCore.NEWEST, + ) + + assertEquals(MediaLibraryEmptyStateKind.FOLDER_EMPTY, state.emptyStateKind) + assertFalse(state.visibleRecordIds.isNotEmpty()) + } + + @Test + fun historyPresenterDerivesSectionsAndEmptyStates() { + val now = 1_700_000_000_000L + val state = HistoryPresenter.present( + records = listOf( + HistoryRecordSnapshot(id = "today", timestampEpochMillis = now), + HistoryRecordSnapshot(id = "yesterday", timestampEpochMillis = now - 86_400_000L), + HistoryRecordSnapshot(id = "older", timestampEpochMillis = now - (2 * 86_400_000L)), + ), + totalTranscriptionsCount = 3, + searchText = "", + selectedRecordId = "yesterday", + hasLoadedInitialPage = true, + isLoadingPage = false, + errorMessage = null, + nowEpochMillis = now, + timeZoneOffsetMinutes = 0, + ) + + assertEquals(HistoryContentStateKind.POPULATED, state.contentStateKind) + assertEquals("yesterday", state.selectedRecordId) + assertEquals(3, state.sections.size) + assertEquals(HistorySectionKind.TODAY, state.sections[0].kind) + assertEquals(HistorySectionKind.YESTERDAY, state.sections[1].kind) + assertEquals(HistorySectionKind.DATE, state.sections[2].kind) + } + + @Test + fun historyPresenterReportsSearchEmptyState() { + val state = HistoryPresenter.present( + records = emptyList(), + totalTranscriptionsCount = 0, + searchText = "plan", + selectedRecordId = null, + hasLoadedInitialPage = true, + isLoadingPage = false, + errorMessage = null, + nowEpochMillis = 0, + timeZoneOffsetMinutes = 0, + ) + + assertEquals(HistoryContentStateKind.EMPTY_SEARCH, state.contentStateKind) + assertFalse(state.canExport) + } + + @Test + fun dictionaryPresenterSortsEntriesAndValidatesAddForm() { + val state = DictionaryPresenter.present( + selectedSection = DictionarySectionCore.REPLACEMENTS, + replacements = listOf( + ReplacementEntrySnapshot(id = "b", originals = listOf("beta"), replacement = "B", sortOrder = 2), + ReplacementEntrySnapshot(id = "a", originals = listOf("alpha"), replacement = "A", sortOrder = 1), + ), + vocabularyWords = listOf(VocabularyWordSnapshot(id = "v1", word = "Zebra")), + primaryInput = "source", + secondaryInput = "target", + errorMessage = null, + ) + + assertEquals(3, state.totalItemCount) + assertEquals(listOf("a", "b"), state.visibleReplacementIds) + assertTrue(state.canAdd) + assertEquals(DictionaryContentStateKind.POPULATED, state.contentStateKind) + } + + @Test + fun notesPresenterFiltersAndTracksEmptySearchState() { + val state = NotesPresenter.present( + notes = listOf( + NoteSnapshot( + id = "1", + title = "Meeting Notes", + content = "Quarterly planning session", + tags = listOf("planning"), + updatedAtEpochMillis = 20, + ), + NoteSnapshot( + id = "2", + title = "Ideas", + content = "Ship desktop rewrite", + tags = listOf("product"), + updatedAtEpochMillis = 10, + ), + ), + searchText = "quarterly", + sortOrder = NotesSortOrderCore.DESCENDING, + selectedNoteId = "2", + errorMessage = null, + ) + + assertEquals(listOf("1"), state.visibleNoteIds) + assertEquals(null, state.selectedNoteId) + assertEquals(NotesContentStateKind.POPULATED, state.contentStateKind) + + val emptyState = NotesPresenter.present( + notes = emptyList(), + searchText = "missing", + sortOrder = NotesSortOrderCore.ASCENDING, + selectedNoteId = null, + errorMessage = null, + ) + + assertEquals(NotesContentStateKind.EMPTY_SEARCH, emptyState.contentStateKind) + } + + @Test + fun modelsPresenterFiltersUsingSharedCatalogRules() { + val state = ModelsPresenter.browse( + models = listOf( + ModelCatalogEntrySnapshot( + id = "recommended-local", + name = "recommended-local", + displayName = "Recommended Local", + description = "fast local model", + providerName = "WhisperKit", + isLocal = true, + isRecommended = true, + availability = "available", + ), + ModelCatalogEntrySnapshot( + id = "cloud", + name = "cloud", + displayName = "Cloud", + description = "remote model", + providerName = "OpenAI", + isLocal = false, + isRecommended = false, + availability = "available", + ), + ), + selectedFilter = ModelsFilterCore.RECOMMENDED, + searchText = "", + ) + + assertEquals(ModelsFilterCore.RECOMMENDED, state.effectiveFilter) + assertEquals(listOf("recommended-local"), state.visibleModelIds) + + val searched = ModelsPresenter.browse( + models = emptyList(), + selectedFilter = ModelsFilterCore.ALL, + searchText = "missing", + ) + assertEquals(ModelsContentStateKind.EMPTY_SEARCH, searched.contentStateKind) + } + + @Test + fun transcribeLibraryPresenterDerivesEmptyStateAndActions() { + val browseState = MediaLibraryBrowseState( + trimmedSearchText = "", + selectedFolderId = "folder-1", + visibleFolderIds = emptyList(), + visibleRecordIds = emptyList(), + filteredFolderCount = 0, + filteredRecordCount = 0, + totalRecordCountForSelectedFolder = 0, + emptyStateKind = MediaLibraryEmptyStateKind.FOLDER_EMPTY, + ) + + val state = TranscribeLibraryPresenter.present( + selectedFolderId = "folder-1", + selectedFolderName = "Calls", + draftLink = " https://example.com/video ", + librarySearchText = "", + browseState = browseState, + ) + + assertTrue(state.shouldShowBackButton) + assertTrue(state.canSubmitDraftLink) + assertTrue(state.shouldShowLibraryEmptyState) + assertEquals("No items in %@", state.emptyStateTitleKey) + assertEquals("folder.badge.questionmark", state.emptyStateIconName) + } +} From e031fb7f61eed1e44ded48180c6d7fd9d18b86b2 Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Sat, 28 Mar 2026 20:34:16 -0600 Subject: [PATCH 9/9] chore: prepare v1.15.0 release artifacts --- Pindrop.xcodeproj/project.pbxproj | 16 ++++++++-------- Pindrop/AppCoordinator.swift | 5 ++++- release-notes/v1.15.0.md | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 release-notes/v1.15.0.md diff --git a/Pindrop.xcodeproj/project.pbxproj b/Pindrop.xcodeproj/project.pbxproj index 7de7664..c9bea0d 100644 --- a/Pindrop.xcodeproj/project.pbxproj +++ b/Pindrop.xcodeproj/project.pbxproj @@ -1099,7 +1099,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = MB5789APU7; FRAMEWORK_SEARCH_PATHS = ( @@ -1113,7 +1113,7 @@ ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.14.0; + MARKETING_VERSION = 1.15.0; PRODUCT_BUNDLE_IDENTIFIER = tech.watzon.pindrop.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -1132,7 +1132,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Pindrop/Preview Content\""; DEVELOPMENT_TEAM = MB5789APU7; @@ -1159,7 +1159,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.14.0; + MARKETING_VERSION = 1.15.0; PRODUCT_BUNDLE_IDENTIFIER = tech.watzon.pindrop; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1325,7 +1325,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Pindrop/Preview Content\""; DEVELOPMENT_TEAM = MB5789APU7; @@ -1352,7 +1352,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.14.0; + MARKETING_VERSION = 1.15.0; PRODUCT_BUNDLE_IDENTIFIER = tech.watzon.pindrop; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1392,7 +1392,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = MB5789APU7; FRAMEWORK_SEARCH_PATHS = ( @@ -1406,7 +1406,7 @@ ); GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.14.0; + MARKETING_VERSION = 1.15.0; PRODUCT_BUNDLE_IDENTIFIER = tech.watzon.pindrop.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; diff --git a/Pindrop/AppCoordinator.swift b/Pindrop/AppCoordinator.swift index 3f2c28a..2ef6475 100644 --- a/Pindrop/AppCoordinator.swift +++ b/Pindrop/AppCoordinator.swift @@ -1818,11 +1818,14 @@ final class AppCoordinator { suspendLiveContextSessionUpdates() isProcessing = true statusBarController.setProcessingState() + var didResetProcessingState = false transitionRecordingIndicatorToProcessing() defer { - resetProcessingState() + if !didResetProcessingState { + resetProcessingState() + } } let audioData: Data diff --git a/release-notes/v1.15.0.md b/release-notes/v1.15.0.md new file mode 100644 index 0000000..6ce0aff --- /dev/null +++ b/release-notes/v1.15.0.md @@ -0,0 +1,21 @@ +## What's New + +- **Cross-platform foundation ships with this release** — most business logic has moved into Kotlin Multiplatform shared modules, making transcription orchestration and shared UI state reusable beyond macOS. +- **First step toward Windows/Linux delivery** — shared Kotlin presenters and core modules are now integrated so future desktop ports can build on the same business logic. +- **Kotlin shared presenter layer landed** — app-side coordinator logic now consumes shared presenters for cleaner boundaries and easier future platform ports. + +## Improvements + +- **Kotlin bridge and Gradle pipeline updates** reduced platform-specific coupling and keep shared module builds lighter. +- **Media transcription flow is streamlined** with unified state handling between audio/media paths. +- **Settings and locale behavior improved** so app language changes are captured in snapshots used by UI/state synchronization. + +## Bug Fixes + +- **Provider handling** is more resilient when a transcription provider is unavailable or unsupported. +- **Model download progress strings** and locale lookups are more stable, including the newly added Turkish strings path. +- **Floating indicator behavior** improves around screen tracking and compact hover interactions. + +## Full Changelog + +https://github.com/watzon/pindrop/compare/v1.14.0...v1.15.0