update mobile dictation controls
Add mobile permission approval flow, simplify dictation settings into toggles, and remove oversized Whisper models while syncing the iOS project with the current runtime configuration.pull/19545/head
parent
abf79ae24c
commit
bcf7817127
|
|
@ -0,0 +1,578 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
4318840A4939F9117F5CB295 /* libPods-mobilevoice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D01178681A4FA33E647BDA6 /* libPods-mobilevoice.a */; };
|
||||
5BCC3D8ACE1241D9ADF08705 /* expo.icon in Resources */ = {isa = PBXBuildFile; fileRef = F1C6EB0F46C84143B321E16B /* expo.icon */; };
|
||||
7F4CD5196803F15ED532DB9D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5883CCA53017A900D5BA6B8 /* ExpoModulesProvider.swift */; };
|
||||
A9D355AEC99B2C485E3E171E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BFF46FE16E7CF5862CD6C307 /* PrivacyInfo.xcprivacy */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
13B07F961A680F5B00A75B9A /* mobilevoice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mobilevoice.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = mobilevoice/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = mobilevoice/Info.plist; sourceTree = "<group>"; };
|
||||
2EF11B4CD2A527AE1396409B /* Pods-mobilevoice.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-mobilevoice.release.xcconfig"; path = "Target Support Files/Pods-mobilevoice/Pods-mobilevoice.release.xcconfig"; sourceTree = "<group>"; };
|
||||
791CA91656B547FFB3774C02 /* Pods-mobilevoice.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-mobilevoice.debug.xcconfig"; path = "Target Support Files/Pods-mobilevoice/Pods-mobilevoice.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9D01178681A4FA33E647BDA6 /* libPods-mobilevoice.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-mobilevoice.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = mobilevoice/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
BFF46FE16E7CF5862CD6C307 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = mobilevoice/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
E5883CCA53017A900D5BA6B8 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-mobilevoice/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = mobilevoice/AppDelegate.swift; sourceTree = "<group>"; };
|
||||
F11748442D0722820044C1D9 /* mobilevoice-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "mobilevoice-Bridging-Header.h"; path = "mobilevoice/mobilevoice-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
F1C6EB0F46C84143B321E16B /* expo.icon */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = expo.icon; path = mobilevoice/expo.icon; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4318840A4939F9117F5CB295 /* libPods-mobilevoice.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* mobilevoice */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
||||
F11748442D0722820044C1D9 /* mobilevoice-Bridging-Header.h */,
|
||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||
F1C6EB0F46C84143B321E16B /* expo.icon */,
|
||||
BFF46FE16E7CF5862CD6C307 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = mobilevoice;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
9D01178681A4FA33E647BDA6 /* libPods-mobilevoice.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6FA8507500F4DE261E8F6EB0 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
791CA91656B547FFB3774C02 /* Pods-mobilevoice.debug.xcconfig */,
|
||||
2EF11B4CD2A527AE1396409B /* Pods-mobilevoice.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* mobilevoice */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
6FA8507500F4DE261E8F6EB0 /* Pods */,
|
||||
8768F75DE2083C7536A3A210 /* ExpoModulesProviders */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* mobilevoice.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8768F75DE2083C7536A3A210 /* ExpoModulesProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C0F0F32FD747A33E1176E0FD /* mobilevoice */,
|
||||
);
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB2F792B24A3F905000567C9 /* Supporting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */,
|
||||
);
|
||||
name = Supporting;
|
||||
path = mobilevoice/Supporting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C0F0F32FD747A33E1176E0FD /* mobilevoice */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E5883CCA53017A900D5BA6B8 /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = mobilevoice;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* mobilevoice */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "mobilevoice" */;
|
||||
buildPhases = (
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||
92A85735D2EE37AB89277662 /* [Expo] Configure project */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||
B09C32BB889007E7F37BBC9C /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = mobilevoice;
|
||||
productName = mobilevoice;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* mobilevoice.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1130;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1250;
|
||||
DevelopmentTeam = "9G68SMNHEU";
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "mobilevoice" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* mobilevoice */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
5BCC3D8ACE1241D9ADF08705 /* expo.icon in Resources */,
|
||||
A9D355AEC99B2C485E3E171E /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
|
||||
};
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-mobilevoice-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-mobilevoice/Pods-mobilevoice-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoNotifications/ExpoNotifications_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoTaskManager/ExpoTaskManager_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-mobilevoice/Pods-mobilevoice-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
92A85735D2EE37AB89277662 /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
"$(SRCROOT)/mobilevoice/mobilevoice.entitlements",
|
||||
"$(SRCROOT)/Pods/Target Support Files/Pods-mobilevoice/expo-configure-project.sh",
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(SRCROOT)/Pods/Target Support Files/Pods-mobilevoice/ExpoModulesProvider.swift",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-mobilevoice/expo-configure-project.sh\"\n";
|
||||
};
|
||||
B09C32BB889007E7F37BBC9C /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-mobilevoice/Pods-mobilevoice-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/RNAudioAPI/libavcodec.framework/libavcodec",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/RNAudioAPI/libavformat.framework/libavformat",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/RNAudioAPI/libavutil.framework/libavutil",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/RNAudioAPI/libswresample.framework/libswresample",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/react-native-executorch/ExecutorchLib.framework/ExecutorchLib",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavcodec.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavformat.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavutil.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswresample.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExecutorchLib.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-mobilevoice/Pods-mobilevoice-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
||||
7F4CD5196803F15ED532DB9D /* ExpoModulesProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 791CA91656B547FFB3774C02 /* Pods-mobilevoice.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = expo;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = mobilevoice/mobilevoice.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"FB_SONARKIT_ENABLED=1",
|
||||
);
|
||||
INFOPLIST_FILE = mobilevoice/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anomalyco.mobilevoice;
|
||||
PRODUCT_NAME = mobilevoice;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "mobilevoice/mobilevoice-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
DEVELOPMENT_TEAM = "9G68SMNHEU";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 2EF11B4CD2A527AE1396409B /* Pods-mobilevoice.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = expo;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = mobilevoice/mobilevoice.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = mobilevoice/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anomalyco.mobilevoice;
|
||||
PRODUCT_NAME = mobilevoice;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "mobilevoice/mobilevoice-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
DEVELOPMENT_TEAM = "9G68SMNHEU";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "mobilevoice" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "mobilevoice" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Control</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>mobilevoice</string>
|
||||
<string>com.anomalyco.mobilevoice</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>ts.net</key>
|
||||
<dict>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app needs microphone access for live speech-to-text dictation.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
</array>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
LayoutChangeEvent,
|
||||
Linking,
|
||||
Platform,
|
||||
Switch,
|
||||
} from "react-native"
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
|
|
@ -34,8 +35,9 @@ import { AudioPcmStreamAdapter } from "whisper.rn/src/realtime-transcription/ada
|
|||
import { AudioManager } from "react-native-audio-api"
|
||||
import * as FileSystem from "expo-file-system/legacy"
|
||||
import { fetch as expoFetch } from "expo/fetch"
|
||||
import { buildPermissionCardModel } from "@/lib/pending-permissions"
|
||||
import { unregisterRelayDevice } from "@/lib/relay-client"
|
||||
import { useMonitoring, type MonitorJob } from "@/hooks/use-monitoring"
|
||||
import { useMonitoring, type MonitorJob, type PermissionDecision } from "@/hooks/use-monitoring"
|
||||
import { looksLikeLocalHost, useServerSessions } from "@/hooks/use-server-sessions"
|
||||
import { ensureNotificationPermissions, getDevicePushToken } from "@/notifications/monitoring-notifications"
|
||||
|
||||
|
|
@ -77,15 +79,6 @@ const WHISPER_MODELS = [
|
|||
"ggml-medium-q5_0.bin",
|
||||
"ggml-medium-q8_0.bin",
|
||||
"ggml-medium.bin",
|
||||
"ggml-large-v1.bin",
|
||||
"ggml-large-v2-q5_0.bin",
|
||||
"ggml-large-v2-q8_0.bin",
|
||||
"ggml-large-v2.bin",
|
||||
"ggml-large-v3-q5_0.bin",
|
||||
"ggml-large-v3-turbo-q5_0.bin",
|
||||
"ggml-large-v3-turbo-q8_0.bin",
|
||||
"ggml-large-v3-turbo.bin",
|
||||
"ggml-large-v3.bin",
|
||||
] as const
|
||||
|
||||
type WhisperModelID = (typeof WHISPER_MODELS)[number]
|
||||
|
|
@ -119,15 +112,6 @@ const WHISPER_MODEL_LABELS: Record<WhisperModelID, string> = {
|
|||
"ggml-medium-q5_0.bin": "medium q5_0",
|
||||
"ggml-medium-q8_0.bin": "medium q8_0",
|
||||
"ggml-medium.bin": "medium",
|
||||
"ggml-large-v1.bin": "large-v1",
|
||||
"ggml-large-v2-q5_0.bin": "large-v2 q5_0",
|
||||
"ggml-large-v2-q8_0.bin": "large-v2 q8_0",
|
||||
"ggml-large-v2.bin": "large-v2",
|
||||
"ggml-large-v3-q5_0.bin": "large-v3 q5_0",
|
||||
"ggml-large-v3-turbo-q5_0.bin": "large-v3 turbo q5_0",
|
||||
"ggml-large-v3-turbo-q8_0.bin": "large-v3 turbo q8_0",
|
||||
"ggml-large-v3-turbo.bin": "large-v3 turbo",
|
||||
"ggml-large-v3.bin": "large-v3",
|
||||
}
|
||||
|
||||
const WHISPER_MODEL_SIZES: Record<WhisperModelID, number> = {
|
||||
|
|
@ -155,15 +139,6 @@ const WHISPER_MODEL_SIZES: Record<WhisperModelID, number> = {
|
|||
"ggml-medium-q5_0.bin": 539212467,
|
||||
"ggml-medium-q8_0.bin": 823369779,
|
||||
"ggml-medium.bin": 1533763059,
|
||||
"ggml-large-v1.bin": 3094623691,
|
||||
"ggml-large-v2-q5_0.bin": 1080732091,
|
||||
"ggml-large-v2-q8_0.bin": 1656129691,
|
||||
"ggml-large-v2.bin": 3094623691,
|
||||
"ggml-large-v3-q5_0.bin": 1081140203,
|
||||
"ggml-large-v3-turbo-q5_0.bin": 574041195,
|
||||
"ggml-large-v3-turbo-q8_0.bin": 874188075,
|
||||
"ggml-large-v3-turbo.bin": 1624555275,
|
||||
"ggml-large-v3.bin": 3095033483,
|
||||
}
|
||||
|
||||
function isWhisperModelID(value: unknown): value is WhisperModelID {
|
||||
|
|
@ -271,6 +246,7 @@ type Scan = {
|
|||
type WhisperSavedState = {
|
||||
defaultModel: WhisperModelID
|
||||
mode: TranscriptionMode
|
||||
autoSendOnDictationEnd: boolean
|
||||
}
|
||||
|
||||
type OnboardingSavedState = {
|
||||
|
|
@ -402,6 +378,7 @@ export default function DictationScreen() {
|
|||
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||
const [isPreparingWhisperModel, setIsPreparingWhisperModel] = useState(true)
|
||||
const [transcriptionMode, setTranscriptionMode] = useState<TranscriptionMode>(DEFAULT_TRANSCRIPTION_MODE)
|
||||
const [autoSendOnDictationEnd, setAutoSendOnDictationEnd] = useState(false)
|
||||
const [isTranscribingBulk, setIsTranscribingBulk] = useState(false)
|
||||
const [whisperError, setWhisperError] = useState("")
|
||||
const [transcribedText, setTranscribedText] = useState("")
|
||||
|
|
@ -413,6 +390,7 @@ export default function DictationScreen() {
|
|||
const [agentStateDismissed, setAgentStateDismissed] = useState(false)
|
||||
const [dropdownMode, setDropdownMode] = useState<DropdownMode>("none")
|
||||
const [dropdownRenderMode, setDropdownRenderMode] = useState<Exclude<DropdownMode, "none">>("server")
|
||||
const [sessionCreateMode, setSessionCreateMode] = useState<"same" | "root" | null>(null)
|
||||
const [scanOpen, setScanOpen] = useState(false)
|
||||
const [camGranted, setCamGranted] = useState(false)
|
||||
const [waveformLevels, setWaveformLevels] = useState<number[]>(Array.from({ length: 24 }, () => 0))
|
||||
|
|
@ -437,6 +415,7 @@ export default function DictationScreen() {
|
|||
const bulkAudioChunksRef = useRef<Uint8Array[]>([])
|
||||
const bulkTranscriptionJobRef = useRef(0)
|
||||
const downloadProgressRef = useRef(0)
|
||||
const autoSendSignatureRef = useRef("")
|
||||
const waveformPulseIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const sendSettleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const scanLockRef = useRef(false)
|
||||
|
|
@ -462,15 +441,20 @@ export default function DictationScreen() {
|
|||
selectSession,
|
||||
removeServer,
|
||||
addServer,
|
||||
createSession,
|
||||
findServerForSession,
|
||||
} = useServerSessions()
|
||||
|
||||
const {
|
||||
beginMonitoring,
|
||||
activePermissionRequest,
|
||||
devicePushToken,
|
||||
latestAssistantResponse,
|
||||
monitorJob,
|
||||
monitorStatus,
|
||||
pendingPermissionCount,
|
||||
respondingPermissionID,
|
||||
respondToPermission,
|
||||
setDevicePushToken,
|
||||
setMonitorStatus,
|
||||
} = useMonitoring({
|
||||
|
|
@ -727,6 +711,7 @@ export default function DictationScreen() {
|
|||
|
||||
let nextDefaultModel: WhisperModelID = DEFAULT_WHISPER_MODEL
|
||||
let nextMode: TranscriptionMode = DEFAULT_TRANSCRIPTION_MODE
|
||||
let nextAutoSendOnDictationEnd = false
|
||||
try {
|
||||
const data = await FileSystem.readAsStringAsync(WHISPER_SETTINGS_FILE)
|
||||
if (data) {
|
||||
|
|
@ -737,6 +722,9 @@ export default function DictationScreen() {
|
|||
if (isTranscriptionMode(parsed.mode)) {
|
||||
nextMode = parsed.mode
|
||||
}
|
||||
if (parsed.autoSendOnDictationEnd === true) {
|
||||
nextAutoSendOnDictationEnd = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Use default settings if state file is missing or invalid.
|
||||
|
|
@ -747,6 +735,7 @@ export default function DictationScreen() {
|
|||
whisperRestoredRef.current = true
|
||||
setDefaultWhisperModel(nextDefaultModel)
|
||||
setTranscriptionMode(nextMode)
|
||||
setAutoSendOnDictationEnd(nextAutoSendOnDictationEnd)
|
||||
|
||||
await refreshInstalledWhisperModels()
|
||||
|
||||
|
|
@ -768,9 +757,13 @@ export default function DictationScreen() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!whisperRestoredRef.current) return
|
||||
const payload: WhisperSavedState = { defaultModel: defaultWhisperModel, mode: transcriptionMode }
|
||||
const payload: WhisperSavedState = {
|
||||
defaultModel: defaultWhisperModel,
|
||||
mode: transcriptionMode,
|
||||
autoSendOnDictationEnd,
|
||||
}
|
||||
void FileSystem.writeAsStringAsync(WHISPER_SETTINGS_FILE, JSON.stringify(payload)).catch(() => {})
|
||||
}, [defaultWhisperModel, transcriptionMode])
|
||||
}, [autoSendOnDictationEnd, defaultWhisperModel, transcriptionMode])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -1140,6 +1133,26 @@ export default function DictationScreen() {
|
|||
setAgentStateDismissed(true)
|
||||
}, [])
|
||||
|
||||
const handlePermissionDecision = useCallback(
|
||||
(reply: PermissionDecision) => {
|
||||
if (!activePermissionRequest || !activeServerId) return
|
||||
|
||||
void Haptics.selectionAsync().catch(() => {})
|
||||
void respondToPermission({
|
||||
serverID: activeServerId,
|
||||
sessionID: activePermissionRequest.sessionID,
|
||||
requestID: activePermissionRequest.id,
|
||||
reply,
|
||||
}).catch((error) => {
|
||||
Alert.alert(
|
||||
"Could not send decision",
|
||||
error instanceof Error ? error.message : "OpenCode did not accept that decision.",
|
||||
)
|
||||
})
|
||||
},
|
||||
[activePermissionRequest, activeServerId, respondToPermission],
|
||||
)
|
||||
|
||||
const resetTranscriptState = useCallback(() => {
|
||||
if (isRecordingRef.current) {
|
||||
stopRecording()
|
||||
|
|
@ -1454,6 +1467,7 @@ export default function DictationScreen() {
|
|||
|
||||
const modelDownloading = downloadingModelID !== null
|
||||
const modelLoading = isPreparingWhisperModel || activeWhisperModel == null || modelDownloading || isTranscribingBulk
|
||||
const dictationSettingsLocked = isRecording || isTranscribingBulk || isSending
|
||||
let modelLoadingState: "downloading" | "loading" | "ready" = "ready"
|
||||
if (modelDownloading) {
|
||||
modelLoadingState = "downloading"
|
||||
|
|
@ -1466,20 +1480,29 @@ export default function DictationScreen() {
|
|||
: WHISPER_MODEL_LABELS[defaultWhisperModel]
|
||||
const hasTranscript = transcribedText.trim().length > 0
|
||||
const hasAssistantResponse = latestAssistantResponse.trim().length > 0
|
||||
const activePermissionCard = activePermissionRequest ? buildPermissionCardModel(activePermissionRequest) : null
|
||||
const hasPendingPermission = activePermissionRequest !== null && activePermissionCard !== null
|
||||
const hasAgentActivity = hasAssistantResponse || monitorStatus.trim().length > 0 || monitorJob !== null
|
||||
const shouldShowAgentStateCard = hasAgentActivity && !agentStateDismissed
|
||||
const shouldShowAgentStateCard = !hasPendingPermission && hasAgentActivity && !agentStateDismissed
|
||||
const showsCompleteState = monitorStatus.toLowerCase().includes("complete")
|
||||
let agentStateIcon: "loading" | "done" = "loading"
|
||||
if (monitorJob === null && (hasAssistantResponse || showsCompleteState)) {
|
||||
agentStateIcon = "done"
|
||||
}
|
||||
const agentStateText = hasAssistantResponse ? latestAssistantResponse : "Waiting for agent…"
|
||||
const shouldShowSend = hasCompletedSession && hasTranscript
|
||||
const shouldShowSend = hasCompletedSession && hasTranscript && !hasPendingPermission
|
||||
const activeServer = servers.find((s) => s.id === activeServerId) ?? null
|
||||
const activeSession = activeServer?.sessions.find((s) => s.id === activeSessionId) ?? null
|
||||
const canSendToSession = !!activeServer && activeServer.status === "online" && !!activeSession
|
||||
const isReplyingToActivePermission =
|
||||
activePermissionRequest !== null && respondingPermissionID === activePermissionRequest.id
|
||||
const displayedTranscript = isSending ? "" : transcribedText
|
||||
const isDropdownOpen = dropdownMode !== "none"
|
||||
const effectiveDropdownMode = isDropdownOpen ? dropdownMode : dropdownRenderMode
|
||||
const isCreatingSession = sessionCreateMode !== null
|
||||
const showSessionCreationChoices =
|
||||
effectiveDropdownMode === "session" && !!activeServer && activeServer.status === "online"
|
||||
const sessionCreationChoiceCount = showSessionCreationChoices ? (activeSession ? 2 : 1) : 0
|
||||
const headerTitle = activeServer?.name ?? "No server configured"
|
||||
let headerDotStyle = styles.serverStatusOffline
|
||||
if (activeServer?.status === "online") {
|
||||
|
|
@ -1534,6 +1557,46 @@ export default function DictationScreen() {
|
|||
})
|
||||
}, [shouldShowSend, sendVisibility])
|
||||
|
||||
useEffect(() => {
|
||||
const text = transcribedText.trim()
|
||||
if (!hasCompletedSession || text.length === 0) {
|
||||
autoSendSignatureRef.current = ""
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!autoSendOnDictationEnd ||
|
||||
isRecording ||
|
||||
isTranscribingBulk ||
|
||||
isSending ||
|
||||
hasPendingPermission ||
|
||||
!activeServerId ||
|
||||
!activeSessionId
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const signature = `${activeServerId}:${activeSessionId}:${transcriptionMode}:${text}`
|
||||
if (autoSendSignatureRef.current === signature) {
|
||||
return
|
||||
}
|
||||
|
||||
autoSendSignatureRef.current = signature
|
||||
void handleSendTranscript()
|
||||
}, [
|
||||
activeServerId,
|
||||
activeSessionId,
|
||||
autoSendOnDictationEnd,
|
||||
handleSendTranscript,
|
||||
hasCompletedSession,
|
||||
hasPendingPermission,
|
||||
isRecording,
|
||||
isSending,
|
||||
isTranscribingBulk,
|
||||
transcriptionMode,
|
||||
transcribedText,
|
||||
])
|
||||
|
||||
// Parent clips outer half of center-stroke, so only inner half is visible.
|
||||
// borderWidth 6 → 3px visible inward, borderWidth 12 → 6px visible inward.
|
||||
const animatedBorderStyle = useAnimatedStyle(() => {
|
||||
|
|
@ -1590,8 +1653,15 @@ export default function DictationScreen() {
|
|||
const menuRows =
|
||||
effectiveDropdownMode === "server" ? Math.max(servers.length, 1) : Math.max(activeServer?.sessions.length ?? 0, 1)
|
||||
const expandedRowsHeight = Math.min(menuRows, DROPDOWN_VISIBLE_ROWS) * 42
|
||||
const addServerExtraHeight = effectiveDropdownMode === "server" ? 38 : 8
|
||||
const expandedHeaderHeight = 51 + 12 + expandedRowsHeight + addServerExtraHeight
|
||||
const dropdownFooterExtraHeight =
|
||||
effectiveDropdownMode === "server"
|
||||
? 38
|
||||
: sessionCreationChoiceCount === 2
|
||||
? 72
|
||||
: sessionCreationChoiceCount === 1
|
||||
? 38
|
||||
: 8
|
||||
const expandedHeaderHeight = 51 + 12 + expandedRowsHeight + dropdownFooterExtraHeight
|
||||
|
||||
const animatedHeaderStyle = useAnimatedStyle(() => ({
|
||||
height: interpolate(serverMenuProgress.value, [0, 1], [51, expandedHeaderHeight], Extrapolation.CLAMP),
|
||||
|
|
@ -1711,6 +1781,49 @@ export default function DictationScreen() {
|
|||
[selectSession],
|
||||
)
|
||||
|
||||
const handleCreateRootSession = useCallback(() => {
|
||||
if (!activeServer || activeServer.status !== "online" || isCreatingSession) {
|
||||
return
|
||||
}
|
||||
|
||||
setSessionCreateMode("root")
|
||||
void createSession(activeServer.id)
|
||||
.then((created) => {
|
||||
if (!created) {
|
||||
Alert.alert("Could not create session", "Please check that your server is online and try again.")
|
||||
return
|
||||
}
|
||||
|
||||
setDropdownMode("none")
|
||||
})
|
||||
.finally(() => {
|
||||
setSessionCreateMode(null)
|
||||
})
|
||||
}, [activeServer, createSession, isCreatingSession])
|
||||
|
||||
const handleCreateSessionLikeCurrent = useCallback(() => {
|
||||
if (!activeServer || activeServer.status !== "online" || !activeSession || isCreatingSession) {
|
||||
return
|
||||
}
|
||||
|
||||
setSessionCreateMode("same")
|
||||
void createSession(activeServer.id, {
|
||||
directory: activeSession.directory,
|
||||
workspaceID: activeSession.workspaceID,
|
||||
})
|
||||
.then((created) => {
|
||||
if (!created) {
|
||||
Alert.alert("Could not create session", "Please check that your server is online and try again.")
|
||||
return
|
||||
}
|
||||
|
||||
setDropdownMode("none")
|
||||
})
|
||||
.finally(() => {
|
||||
setSessionCreateMode(null)
|
||||
})
|
||||
}, [activeServer, activeSession, createSession, isCreatingSession])
|
||||
|
||||
const handleDeleteServer = useCallback(
|
||||
(id: string) => {
|
||||
const server = serversRef.current.find((s) => s.id === id)
|
||||
|
|
@ -2212,6 +2325,55 @@ export default function DictationScreen() {
|
|||
<Pressable onPress={() => void handleStartScan()} style={styles.addServerButton}>
|
||||
<Text style={styles.addServerButtonText}>Add server by scanning QR code</Text>
|
||||
</Pressable>
|
||||
) : effectiveDropdownMode === "session" && activeServer?.status === "online" ? (
|
||||
<View style={styles.sessionMenuActions}>
|
||||
{activeSession ? (
|
||||
<Pressable
|
||||
onPress={handleCreateSessionLikeCurrent}
|
||||
disabled={isCreatingSession}
|
||||
style={({ pressed }) => [
|
||||
styles.serverRow,
|
||||
styles.sessionMenuActionRow,
|
||||
isCreatingSession && styles.sessionMenuActionButtonDisabled,
|
||||
pressed && styles.clearButtonPressed,
|
||||
]}
|
||||
>
|
||||
<View style={styles.sessionMenuActionInner}>
|
||||
<View style={styles.sessionMenuActionIconSlot}>
|
||||
<SymbolView
|
||||
name={{ ios: "folder.badge.plus", android: "create_new_folder", web: "create_new_folder" }}
|
||||
size={12}
|
||||
tintColor="#9BA3B5"
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.sessionMenuActionText}>
|
||||
{sessionCreateMode === "same" ? "Creating workspace session..." : "New session with workspace"}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<Pressable
|
||||
onPress={handleCreateRootSession}
|
||||
disabled={isCreatingSession}
|
||||
style={({ pressed }) => [
|
||||
styles.serverRow,
|
||||
styles.sessionMenuActionRow,
|
||||
styles.serverRowLast,
|
||||
isCreatingSession && styles.sessionMenuActionButtonDisabled,
|
||||
pressed && styles.clearButtonPressed,
|
||||
]}
|
||||
>
|
||||
<View style={styles.sessionMenuActionInner}>
|
||||
<View style={styles.sessionMenuActionIconSlot}>
|
||||
<SymbolView name={{ ios: "plus", android: "add", web: "add" }} size={12} tintColor="#9BA3B5" />
|
||||
</View>
|
||||
<Text style={styles.sessionMenuActionText}>
|
||||
{sessionCreateMode === "root" ? "Creating new session..." : "New session"}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
|
@ -2219,7 +2381,91 @@ export default function DictationScreen() {
|
|||
|
||||
{/* Transcription area */}
|
||||
<View style={styles.transcriptionArea}>
|
||||
{shouldShowAgentStateCard ? (
|
||||
{hasPendingPermission && activePermissionCard ? (
|
||||
<View style={[styles.splitCard, styles.permissionCard]}>
|
||||
<View style={styles.permissionHeaderRow}>
|
||||
<View style={styles.permissionStatusDot} />
|
||||
<View style={styles.permissionHeaderCopy}>
|
||||
<Text style={styles.replyCardLabel}>Permission</Text>
|
||||
<Text style={styles.permissionStatusText}>
|
||||
{isReplyingToActivePermission
|
||||
? monitorStatus || "Sending decision…"
|
||||
: pendingPermissionCount > 1
|
||||
? `${pendingPermissionCount} requests pending`
|
||||
: "Action needed"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.permissionScroll} contentContainerStyle={styles.permissionContent}>
|
||||
<Text style={styles.permissionEyebrow}>{activePermissionCard.eyebrow}</Text>
|
||||
<Text style={styles.permissionTitle}>{activePermissionCard.title}</Text>
|
||||
<Text style={styles.permissionBody}>{activePermissionCard.body}</Text>
|
||||
|
||||
{activePermissionCard.sections.map((section, index) => (
|
||||
<View
|
||||
key={`permission-section-${section.label}-${index}`}
|
||||
style={[
|
||||
styles.permissionSection,
|
||||
index === activePermissionCard.sections.length - 1 && styles.permissionSectionLast,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.permissionSectionLabel}>{section.label}</Text>
|
||||
<Text style={[styles.permissionSectionText, section.mono && styles.permissionSectionTextMono]}>
|
||||
{section.text}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.permissionFooter}>
|
||||
<Pressable
|
||||
onPress={() => handlePermissionDecision("once")}
|
||||
disabled={isReplyingToActivePermission}
|
||||
style={({ pressed }) => [
|
||||
styles.permissionPrimaryButton,
|
||||
isReplyingToActivePermission && styles.permissionActionDisabled,
|
||||
pressed && styles.clearButtonPressed,
|
||||
]}
|
||||
>
|
||||
{isReplyingToActivePermission ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.permissionPrimaryButtonText}>Allow once</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.permissionSecondaryRow}>
|
||||
{activePermissionRequest.always.length > 0 ? (
|
||||
<Pressable
|
||||
onPress={() => handlePermissionDecision("always")}
|
||||
disabled={isReplyingToActivePermission}
|
||||
style={({ pressed }) => [
|
||||
styles.permissionSecondaryButton,
|
||||
isReplyingToActivePermission && styles.permissionActionDisabled,
|
||||
pressed && styles.clearButtonPressed,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.permissionSecondaryButtonText}>Always allow</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<Pressable
|
||||
onPress={() => handlePermissionDecision("reject")}
|
||||
disabled={isReplyingToActivePermission}
|
||||
style={({ pressed }) => [
|
||||
styles.permissionRejectButton,
|
||||
activePermissionRequest.always.length === 0 && styles.permissionRejectButtonWide,
|
||||
isReplyingToActivePermission && styles.permissionActionDisabled,
|
||||
pressed && styles.clearButtonPressed,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.permissionRejectButtonText}>Reject</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : shouldShowAgentStateCard ? (
|
||||
<View style={styles.splitCardStack}>
|
||||
<View style={[styles.splitCard, styles.replyCard]}>
|
||||
<View style={styles.agentStateHeaderRow}>
|
||||
|
|
@ -2282,9 +2528,9 @@ export default function DictationScreen() {
|
|||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{transcribedText ? (
|
||||
<Text style={styles.transcriptionText}>{transcribedText}</Text>
|
||||
) : (
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
|
@ -2342,9 +2588,9 @@ export default function DictationScreen() {
|
|||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{transcribedText ? (
|
||||
<Text style={styles.transcriptionText}>{transcribedText}</Text>
|
||||
) : (
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
|
@ -2367,7 +2613,7 @@ export default function DictationScreen() {
|
|||
)}
|
||||
</View>
|
||||
|
||||
{/* Record button */}
|
||||
{hasPendingPermission ? null : (
|
||||
<View style={styles.controlsRow} onLayout={handleControlsLayout}>
|
||||
<Pressable
|
||||
onPressIn={handlePressIn}
|
||||
|
|
@ -2421,6 +2667,7 @@ export default function DictationScreen() {
|
|||
</Pressable>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
visible={whisperSettingsOpen}
|
||||
|
|
@ -2464,55 +2711,42 @@ export default function DictationScreen() {
|
|||
<Text style={styles.settingsTextRowValue}>{WHISPER_MODEL_LABELS[defaultWhisperModel]}</Text>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={() => setTranscriptionMode("bulk")}
|
||||
disabled={isRecording || isTranscribingBulk}
|
||||
style={({ pressed }) => [
|
||||
styles.settingsTextRow,
|
||||
(isRecording || isTranscribingBulk) && styles.settingsInlinePressableDisabled,
|
||||
pressed && styles.clearButtonPressed,
|
||||
]}
|
||||
>
|
||||
<View style={styles.settingsTextRow}>
|
||||
<View style={styles.settingsOptionCopy}>
|
||||
<Text style={styles.settingsTextRowTitle}>On Release</Text>
|
||||
<Text style={styles.settingsTextRowMeta}>Transcribe after release</Text>
|
||||
<Text style={styles.settingsTextRowTitle}>Realtime dictation</Text>
|
||||
<Text style={styles.settingsTextRowMeta}>Turn off to transcribe after release</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={transcriptionMode === "realtime"}
|
||||
onValueChange={(enabled) => setTranscriptionMode(enabled ? "realtime" : "bulk")}
|
||||
disabled={dictationSettingsLocked}
|
||||
trackColor={{ false: "#2D2D31", true: "#6A3A33" }}
|
||||
thumbColor={transcriptionMode === "realtime" ? "#FF6B56" : "#F2F2F2"}
|
||||
ios_backgroundColor="#2D2D31"
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.settingsTextRowAction,
|
||||
transcriptionMode === "bulk" && styles.settingsTextRowActionActive,
|
||||
]}
|
||||
>
|
||||
{transcriptionMode === "bulk" ? "Selected" : "Use"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={() => setTranscriptionMode("realtime")}
|
||||
disabled={isRecording || isTranscribingBulk}
|
||||
style={({ pressed }) => [
|
||||
styles.settingsTextRow,
|
||||
(isRecording || isTranscribingBulk) && styles.settingsInlinePressableDisabled,
|
||||
pressed && styles.clearButtonPressed,
|
||||
]}
|
||||
>
|
||||
<View style={styles.settingsTextRow}>
|
||||
<View style={styles.settingsOptionCopy}>
|
||||
<Text style={styles.settingsTextRowTitle}>Realtime</Text>
|
||||
<Text style={styles.settingsTextRowMeta}>Transcribe while you speak</Text>
|
||||
<Text style={styles.settingsTextRowTitle}>Auto send on dictation end</Text>
|
||||
<Text style={styles.settingsTextRowMeta}>Send the transcript as soon as recording finishes</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={autoSendOnDictationEnd}
|
||||
onValueChange={setAutoSendOnDictationEnd}
|
||||
disabled={dictationSettingsLocked}
|
||||
trackColor={{ false: "#2D2D31", true: "#6A3A33" }}
|
||||
thumbColor={autoSendOnDictationEnd ? "#FF6B56" : "#F2F2F2"}
|
||||
ios_backgroundColor="#2D2D31"
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.settingsTextRowAction,
|
||||
transcriptionMode === "realtime" && styles.settingsTextRowActionActive,
|
||||
]}
|
||||
>
|
||||
{transcriptionMode === "realtime" ? "Selected" : "Use"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingsSection}>
|
||||
<Text style={styles.settingsSectionLabel}>MODELS:</Text>
|
||||
<View style={styles.settingsTextRow}>
|
||||
<Text style={styles.settingsMutedText}>Mobile devices currently support models up to `medium`.</Text>
|
||||
</View>
|
||||
{WHISPER_MODELS.map((modelID) => {
|
||||
const installed = installedWhisperModels.includes(modelID)
|
||||
const isDefault = defaultWhisperModel === modelID
|
||||
|
|
@ -2995,6 +3229,35 @@ const styles = StyleSheet.create({
|
|||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
sessionMenuActions: {
|
||||
marginTop: 2,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "#222733",
|
||||
},
|
||||
sessionMenuActionRow: {
|
||||
paddingVertical: 9,
|
||||
},
|
||||
sessionMenuActionInner: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
sessionMenuActionIconSlot: {
|
||||
width: 9,
|
||||
height: 9,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
sessionMenuActionButtonDisabled: {
|
||||
opacity: 0.55,
|
||||
},
|
||||
sessionMenuActionText: {
|
||||
flex: 1,
|
||||
color: "#D6DAE4",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
statusLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
|
@ -3060,6 +3323,152 @@ const styles = StyleSheet.create({
|
|||
replyCard: {
|
||||
paddingTop: 16,
|
||||
},
|
||||
permissionCard: {
|
||||
paddingTop: 16,
|
||||
},
|
||||
permissionHeaderRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 12,
|
||||
},
|
||||
permissionHeaderCopy: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
permissionStatusDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#FFB347",
|
||||
},
|
||||
permissionEyebrow: {
|
||||
color: "#FFB347",
|
||||
fontSize: 11,
|
||||
fontWeight: "800",
|
||||
letterSpacing: 1.1,
|
||||
},
|
||||
permissionStatusText: {
|
||||
color: "#9099AA",
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
permissionScroll: {
|
||||
flex: 1,
|
||||
},
|
||||
permissionContent: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 20,
|
||||
gap: 14,
|
||||
},
|
||||
permissionTitle: {
|
||||
color: "#F7F8FB",
|
||||
fontSize: 30,
|
||||
fontWeight: "800",
|
||||
lineHeight: 36,
|
||||
letterSpacing: -0.7,
|
||||
},
|
||||
permissionBody: {
|
||||
color: "#B2BDCF",
|
||||
fontSize: 17,
|
||||
fontWeight: "500",
|
||||
lineHeight: 24,
|
||||
},
|
||||
permissionSection: {
|
||||
gap: 6,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#242424",
|
||||
},
|
||||
permissionSectionLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
permissionSectionLabel: {
|
||||
color: "#7F8798",
|
||||
fontSize: 11,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.9,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
permissionSectionText: {
|
||||
color: "#E7E7E7",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
lineHeight: 20,
|
||||
},
|
||||
permissionSectionTextMono: {
|
||||
fontFamily: Platform.select({ ios: "Menlo", android: "monospace", web: "monospace" }),
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
color: "#D4D7DE",
|
||||
},
|
||||
permissionFooter: {
|
||||
gap: 10,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 18,
|
||||
paddingTop: 8,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "#21252F",
|
||||
},
|
||||
permissionPrimaryButton: {
|
||||
minHeight: 54,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#1D6FF4",
|
||||
borderWidth: 2,
|
||||
borderColor: "#1557C3",
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
permissionPrimaryButtonText: {
|
||||
color: "#FFFFFF",
|
||||
fontSize: 16,
|
||||
fontWeight: "800",
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
permissionSecondaryRow: {
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
},
|
||||
permissionSecondaryButton: {
|
||||
flex: 1,
|
||||
minHeight: 48,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#1C1E22",
|
||||
borderWidth: 1,
|
||||
borderColor: "#32353D",
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
permissionSecondaryButtonText: {
|
||||
color: "#E0E3EA",
|
||||
fontSize: 14,
|
||||
fontWeight: "700",
|
||||
},
|
||||
permissionRejectButton: {
|
||||
flex: 1,
|
||||
minHeight: 48,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#31181C",
|
||||
borderWidth: 1,
|
||||
borderColor: "#5E2B34",
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
permissionRejectButtonWide: {
|
||||
flex: 1,
|
||||
},
|
||||
permissionRejectButtonText: {
|
||||
color: "#FFCCD2",
|
||||
fontSize: 14,
|
||||
fontWeight: "700",
|
||||
},
|
||||
permissionActionDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
transcriptionPanel: {
|
||||
flex: 1,
|
||||
position: "relative",
|
||||
|
|
@ -3282,6 +3691,9 @@ const styles = StyleSheet.create({
|
|||
borderBottomColor: "#242424",
|
||||
paddingVertical: 10,
|
||||
},
|
||||
settingsToggleRow: {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
settingsMutedText: {
|
||||
color: "#868686",
|
||||
fontSize: 12,
|
||||
|
|
@ -3318,6 +3730,38 @@ const styles = StyleSheet.create({
|
|||
settingsTextRowActionActive: {
|
||||
color: "#FFD8D2",
|
||||
},
|
||||
settingsModeToggle: {
|
||||
flexDirection: "row",
|
||||
backgroundColor: "#17181B",
|
||||
borderWidth: 1,
|
||||
borderColor: "#292A2E",
|
||||
borderRadius: 14,
|
||||
padding: 4,
|
||||
gap: 4,
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
settingsModeToggleOption: {
|
||||
flex: 1,
|
||||
minHeight: 40,
|
||||
borderRadius: 10,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
settingsModeToggleOptionActive: {
|
||||
backgroundColor: "#3F201B",
|
||||
},
|
||||
settingsModeToggleOptionPressed: {
|
||||
opacity: 0.82,
|
||||
},
|
||||
settingsModeToggleText: {
|
||||
color: "#9A9A9A",
|
||||
fontSize: 13,
|
||||
fontWeight: "700",
|
||||
},
|
||||
settingsModeToggleTextActive: {
|
||||
color: "#FFF0EC",
|
||||
},
|
||||
settingsInlineRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ import {
|
|||
type OpenCodeEvent,
|
||||
type MonitorEventType,
|
||||
} from "@/lib/opencode-events"
|
||||
import {
|
||||
parsePendingPermissionRequest,
|
||||
parsePendingPermissionRequests,
|
||||
type PendingPermissionRequest,
|
||||
} from "@/lib/pending-permissions"
|
||||
import { registerRelayDevice, unregisterRelayDevice } from "@/lib/relay-client"
|
||||
import { parseSSEStream } from "@/lib/sse"
|
||||
import { getDevicePushToken, onPushTokenChange } from "@/notifications/monitoring-notifications"
|
||||
|
|
@ -33,6 +38,8 @@ export type MonitorJob = {
|
|||
startedAt: number
|
||||
}
|
||||
|
||||
export type PermissionDecision = "once" | "always" | "reject"
|
||||
|
||||
type SessionRuntimeStatus = "idle" | "busy" | "retry"
|
||||
|
||||
type PermissionPromptState = "idle" | "pending" | "granted" | "denied"
|
||||
|
|
@ -114,6 +121,8 @@ export function useMonitoring({
|
|||
const [monitorJob, setMonitorJob] = useState<MonitorJob | null>(null)
|
||||
const [monitorStatus, setMonitorStatus] = useState("")
|
||||
const [latestAssistantResponse, setLatestAssistantResponse] = useState("")
|
||||
const [pendingPermissions, setPendingPermissions] = useState<PendingPermissionRequest[]>([])
|
||||
const [replyingPermissionID, setReplyingPermissionID] = useState<string | null>(null)
|
||||
const [appState, setAppState] = useState<AppStateStatus>(AppState.currentState)
|
||||
|
||||
const foregroundMonitorAbortRef = useRef<AbortController | null>(null)
|
||||
|
|
@ -127,6 +136,19 @@ export function useMonitoring({
|
|||
const previousPushTokenRef = useRef<string | null>(null)
|
||||
const previousAppStateRef = useRef<AppStateStatus>(AppState.currentState)
|
||||
const latestAssistantRequestRef = useRef(0)
|
||||
const latestPermissionRequestRef = useRef(0)
|
||||
|
||||
const upsertPendingPermission = useCallback(
|
||||
(request: PendingPermissionRequest) => {
|
||||
setPendingPermissions((current) => {
|
||||
const next = current.filter((item) => item.id !== request.id)
|
||||
return [request, ...next]
|
||||
})
|
||||
closeDropdown()
|
||||
setAgentStateDismissed(false)
|
||||
},
|
||||
[closeDropdown, setAgentStateDismissed],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
monitorJobRef.current = monitorJob
|
||||
|
|
@ -243,6 +265,38 @@ export function useMonitoring({
|
|||
[activeSessionIdRef, setAgentStateDismissed],
|
||||
)
|
||||
|
||||
const loadPendingPermissions = useCallback(
|
||||
async (baseURL: string, sessionID: string) => {
|
||||
const requestID = latestPermissionRequestRef.current + 1
|
||||
latestPermissionRequestRef.current = requestID
|
||||
|
||||
const base = baseURL.replace(/\/+$/, "")
|
||||
|
||||
try {
|
||||
const response = await fetch(`${base}/permission`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Permission list failed (${response.status})`)
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as unknown
|
||||
const requests = parsePendingPermissionRequests(payload).filter((item) => item.sessionID === sessionID)
|
||||
|
||||
if (latestPermissionRequestRef.current !== requestID) return
|
||||
if (activeSessionIdRef.current !== sessionID) return
|
||||
|
||||
setPendingPermissions(requests)
|
||||
if (requests.length > 0) {
|
||||
closeDropdown()
|
||||
setAgentStateDismissed(false)
|
||||
}
|
||||
} catch {
|
||||
if (latestPermissionRequestRef.current !== requestID) return
|
||||
if (activeSessionIdRef.current !== sessionID) return
|
||||
}
|
||||
},
|
||||
[activeSessionIdRef, closeDropdown, setAgentStateDismissed],
|
||||
)
|
||||
|
||||
const fetchSessionRuntimeStatus = useCallback(
|
||||
async (baseURL: string, sessionID: string): Promise<SessionRuntimeStatus | null> => {
|
||||
const base = baseURL.replace(/\/+$/, "")
|
||||
|
|
@ -278,6 +332,7 @@ export function useMonitoring({
|
|||
|
||||
if (eventType === "permission") {
|
||||
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning).catch(() => {})
|
||||
void loadPendingPermissions(job.opencodeBaseURL, job.sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +350,7 @@ export function useMonitoring({
|
|||
stopForegroundMonitor()
|
||||
setMonitorJob(null)
|
||||
},
|
||||
[completePlayer, loadLatestAssistantResponse, stopForegroundMonitor],
|
||||
[completePlayer, loadLatestAssistantResponse, loadPendingPermissions, stopForegroundMonitor],
|
||||
)
|
||||
|
||||
const startForegroundMonitor = useCallback(
|
||||
|
|
@ -333,6 +388,13 @@ export function useMonitoring({
|
|||
const sessionID = extractSessionID(parsed)
|
||||
if (sessionID !== job.sessionID) continue
|
||||
|
||||
if (parsed.type === "permission.asked") {
|
||||
const request = parsePendingPermissionRequest(parsed.properties)
|
||||
if (request) {
|
||||
upsertPendingPermission(request)
|
||||
}
|
||||
}
|
||||
|
||||
const eventType = classifyMonitorEvent(parsed)
|
||||
if (!eventType) continue
|
||||
|
||||
|
|
@ -345,7 +407,7 @@ export function useMonitoring({
|
|||
}
|
||||
})()
|
||||
},
|
||||
[handleMonitorEvent, stopForegroundMonitor],
|
||||
[handleMonitorEvent, stopForegroundMonitor, upsertPendingPermission],
|
||||
)
|
||||
|
||||
const beginMonitoring = useCallback(
|
||||
|
|
@ -381,13 +443,22 @@ export function useMonitoring({
|
|||
|
||||
useEffect(() => {
|
||||
setLatestAssistantResponse("")
|
||||
setPendingPermissions([])
|
||||
setAgentStateDismissed(false)
|
||||
if (!activeServerId || !activeSessionId) return
|
||||
|
||||
const server = serversRef.current.find((item) => item.id === activeServerId)
|
||||
if (!server || server.status !== "online") return
|
||||
void loadLatestAssistantResponse(server.url, activeSessionId)
|
||||
}, [activeServerId, activeSessionId, loadLatestAssistantResponse, serversRef, setAgentStateDismissed])
|
||||
void loadPendingPermissions(server.url, activeSessionId)
|
||||
}, [
|
||||
activeServerId,
|
||||
activeSessionId,
|
||||
loadLatestAssistantResponse,
|
||||
loadPendingPermissions,
|
||||
serversRef,
|
||||
setAgentStateDismissed,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -404,6 +475,7 @@ export function useMonitoring({
|
|||
|
||||
const runtimeStatus = await fetchSessionRuntimeStatus(server.url, input.sessionID)
|
||||
await loadLatestAssistantResponse(server.url, input.sessionID)
|
||||
await loadPendingPermissions(server.url, input.sessionID)
|
||||
|
||||
if (runtimeStatus === "busy" || runtimeStatus === "retry") {
|
||||
const nextJob: MonitorJob = {
|
||||
|
|
@ -433,6 +505,7 @@ export function useMonitoring({
|
|||
appState,
|
||||
fetchSessionRuntimeStatus,
|
||||
loadLatestAssistantResponse,
|
||||
loadPendingPermissions,
|
||||
refreshServerStatusAndSessions,
|
||||
serversRef,
|
||||
startForegroundMonitor,
|
||||
|
|
@ -548,6 +621,62 @@ export function useMonitoring({
|
|||
void syncSessionState({ serverID, sessionID })
|
||||
}, [activeServerIdRef, activeSessionIdRef, appState, syncSessionState])
|
||||
|
||||
const respondToPermission = useCallback(
|
||||
async (input: { serverID: string; sessionID: string; requestID: string; reply: PermissionDecision }) => {
|
||||
const server = serversRef.current.find((item) => item.id === input.serverID)
|
||||
if (!server) {
|
||||
throw new Error("Server unavailable")
|
||||
}
|
||||
|
||||
const base = server.url.replace(/\/+$/, "")
|
||||
setReplyingPermissionID(input.requestID)
|
||||
setMonitorStatus(input.reply === "reject" ? "Rejecting request…" : "Sending approval…")
|
||||
let removed: PendingPermissionRequest | undefined
|
||||
setPendingPermissions((current) => {
|
||||
removed = current.find((item) => item.id === input.requestID)
|
||||
return current.filter((item) => item.id !== input.requestID)
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(`${base}/permission/${input.requestID}/reply`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ reply: input.reply }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Permission reply failed (${response.status})`)
|
||||
}
|
||||
|
||||
await syncSessionState({
|
||||
serverID: input.serverID,
|
||||
sessionID: input.sessionID,
|
||||
})
|
||||
} catch (error) {
|
||||
if (removed) {
|
||||
setPendingPermissions((current) => {
|
||||
const restored = removed
|
||||
if (!restored) {
|
||||
return current
|
||||
}
|
||||
if (current.some((item) => item.id === restored.id)) {
|
||||
return current
|
||||
}
|
||||
return [restored, ...current]
|
||||
})
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
setReplyingPermissionID((current) => (current === input.requestID ? null : current))
|
||||
}
|
||||
},
|
||||
[serversRef, syncSessionState],
|
||||
)
|
||||
|
||||
const activePermissionRequest = pendingPermissions[0] ?? null
|
||||
|
||||
const relayServersKey = useMemo(
|
||||
() =>
|
||||
servers
|
||||
|
|
@ -658,6 +787,10 @@ export function useMonitoring({
|
|||
monitorStatus,
|
||||
setMonitorStatus,
|
||||
latestAssistantResponse,
|
||||
activePermissionRequest,
|
||||
pendingPermissionCount: pendingPermissions.length,
|
||||
respondingPermissionID: replyingPermissionID,
|
||||
respondToPermission,
|
||||
beginMonitoring,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,6 +320,113 @@ export function useServerSessions() {
|
|||
[refreshServerStatusAndSessions],
|
||||
)
|
||||
|
||||
const createSession = useCallback(
|
||||
async (
|
||||
serverID: string,
|
||||
options?: {
|
||||
directory?: string
|
||||
workspaceID?: string
|
||||
title?: string
|
||||
},
|
||||
) => {
|
||||
const server = serversRef.current.find((item) => item.id === serverID)
|
||||
if (!server) {
|
||||
return null
|
||||
}
|
||||
|
||||
const base = server.url.replace(/\/+$/, "")
|
||||
const params = new URLSearchParams()
|
||||
const directory = options?.directory?.trim()
|
||||
const workspaceID = options?.workspaceID?.trim()
|
||||
const title = options?.title?.trim()
|
||||
|
||||
if (directory) {
|
||||
params.set("directory", directory)
|
||||
}
|
||||
|
||||
const body: {
|
||||
workspaceID?: string
|
||||
title?: string
|
||||
} = {}
|
||||
|
||||
if (workspaceID) {
|
||||
body.workspaceID = workspaceID
|
||||
}
|
||||
|
||||
if (title) {
|
||||
body.title = title
|
||||
}
|
||||
|
||||
const query = params.toString()
|
||||
const endpoint = `${base}/session${query ? `?${query}` : ""}`
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.log("[Server] session:create:http-error", {
|
||||
id: server.id,
|
||||
endpoint,
|
||||
status: response.status,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as unknown
|
||||
const parsed = parseSessionItems([payload])[0]
|
||||
|
||||
if (!parsed) {
|
||||
void refreshServerStatusAndSessions(serverID)
|
||||
return null
|
||||
}
|
||||
|
||||
const created = parsed.updated > 0 ? parsed : { ...parsed, updated: Date.now() }
|
||||
|
||||
setServers((prev) =>
|
||||
prev.map((item) => {
|
||||
if (item.id !== serverID) return item
|
||||
|
||||
const sessions = [created, ...item.sessions.filter((session) => session.id !== created.id)].sort(
|
||||
(a, b) => b.updated - a.updated,
|
||||
)
|
||||
|
||||
return {
|
||||
...item,
|
||||
status: "online",
|
||||
sessionsLoading: false,
|
||||
sessions,
|
||||
}
|
||||
}),
|
||||
)
|
||||
setActiveServerId(serverID)
|
||||
setActiveSessionId(created.id)
|
||||
|
||||
console.log("[Server] session:create", {
|
||||
id: server.id,
|
||||
sessionID: created.id,
|
||||
hasDirectory: Boolean(created.directory),
|
||||
hasWorkspaceID: Boolean(created.workspaceID),
|
||||
})
|
||||
|
||||
return created
|
||||
} catch (err) {
|
||||
console.log("[Server] session:create:error", {
|
||||
id: server.id,
|
||||
endpoint,
|
||||
error: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
|
||||
})
|
||||
return null
|
||||
}
|
||||
},
|
||||
[refreshServerStatusAndSessions],
|
||||
)
|
||||
|
||||
const findServerForSession = useCallback(
|
||||
async (sessionID: string, preferredServerID?: string | null): Promise<ServerItem | null> => {
|
||||
if (!serversRef.current.length && !restoredRef.current) {
|
||||
|
|
@ -381,6 +488,7 @@ export function useServerSessions() {
|
|||
selectSession,
|
||||
removeServer,
|
||||
addServer,
|
||||
createSession,
|
||||
findServerForSession,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +1,81 @@
|
|||
export type OpenCodeEvent = {
|
||||
type: string;
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
type: string
|
||||
properties?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type MonitorEventType = 'complete' | 'permission' | 'error';
|
||||
export type MonitorEventType = "complete" | "permission" | "error"
|
||||
|
||||
export function extractSessionID(event: OpenCodeEvent): string | null {
|
||||
const props = event.properties ?? {};
|
||||
const props = event.properties ?? {}
|
||||
|
||||
const fromDirect = props.sessionID;
|
||||
if (typeof fromDirect === 'string' && fromDirect.length > 0) return fromDirect;
|
||||
const fromDirect = props.sessionID
|
||||
if (typeof fromDirect === "string" && fromDirect.length > 0) return fromDirect
|
||||
|
||||
const info = props.info;
|
||||
if (info && typeof info === 'object') {
|
||||
const infoSessionID = (info as Record<string, unknown>).sessionID;
|
||||
if (typeof infoSessionID === 'string' && infoSessionID.length > 0) return infoSessionID;
|
||||
const info = props.info
|
||||
if (info && typeof info === "object") {
|
||||
const infoSessionID = (info as Record<string, unknown>).sessionID
|
||||
if (typeof infoSessionID === "string" && infoSessionID.length > 0) return infoSessionID
|
||||
}
|
||||
|
||||
const part = props.part;
|
||||
if (part && typeof part === 'object') {
|
||||
const partSessionID = (part as Record<string, unknown>).sessionID;
|
||||
if (typeof partSessionID === 'string' && partSessionID.length > 0) return partSessionID;
|
||||
const part = props.part
|
||||
if (part && typeof part === "object") {
|
||||
const partSessionID = (part as Record<string, unknown>).sessionID
|
||||
if (typeof partSessionID === "string" && partSessionID.length > 0) return partSessionID
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
export function classifyMonitorEvent(event: OpenCodeEvent): MonitorEventType | null {
|
||||
const type = event.type;
|
||||
const lowerType = type.toLowerCase();
|
||||
const type = event.type
|
||||
const lowerType = type.toLowerCase()
|
||||
|
||||
if (lowerType.includes('permission')) {
|
||||
return 'permission';
|
||||
if (lowerType === "permission.asked" || lowerType === "permission") {
|
||||
return "permission"
|
||||
}
|
||||
|
||||
if (lowerType.includes('error')) {
|
||||
return 'error';
|
||||
if (lowerType.includes("error")) {
|
||||
return "error"
|
||||
}
|
||||
|
||||
if (type === 'session.status') {
|
||||
const status = event.properties?.status;
|
||||
if (status && typeof status === 'object') {
|
||||
const statusType = (status as Record<string, unknown>).type;
|
||||
if (statusType === 'idle') {
|
||||
return 'complete';
|
||||
if (type === "session.status") {
|
||||
const status = event.properties?.status
|
||||
if (status && typeof status === "object") {
|
||||
const statusType = (status as Record<string, unknown>).type
|
||||
if (statusType === "idle") {
|
||||
return "complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'message.updated') {
|
||||
const info = event.properties?.info;
|
||||
if (info && typeof info === 'object') {
|
||||
const role = (info as Record<string, unknown>).role;
|
||||
const time = (info as Record<string, unknown>).time;
|
||||
if (role === 'assistant' && time && typeof time === 'object' && 'completed' in (time as Record<string, unknown>)) {
|
||||
return 'complete';
|
||||
if (type === "message.updated") {
|
||||
const info = event.properties?.info
|
||||
if (info && typeof info === "object") {
|
||||
const role = (info as Record<string, unknown>).role
|
||||
const time = (info as Record<string, unknown>).time
|
||||
if (
|
||||
role === "assistant" &&
|
||||
time &&
|
||||
typeof time === "object" &&
|
||||
"completed" in (time as Record<string, unknown>)
|
||||
) {
|
||||
return "complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
export function formatMonitorEventLabel(eventType: MonitorEventType): string {
|
||||
switch (eventType) {
|
||||
case 'complete':
|
||||
return 'Session complete';
|
||||
case 'permission':
|
||||
return 'Action needed';
|
||||
case 'error':
|
||||
return 'Session error';
|
||||
case "complete":
|
||||
return "Session complete"
|
||||
case "permission":
|
||||
return "Action needed"
|
||||
case "error":
|
||||
return "Session error"
|
||||
default:
|
||||
return 'Session update';
|
||||
return "Session update"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
export type PendingPermissionRequest = {
|
||||
id: string
|
||||
sessionID: string
|
||||
permission: string
|
||||
patterns: string[]
|
||||
metadata: Record<string, unknown>
|
||||
always: string[]
|
||||
tool?: {
|
||||
messageID: string
|
||||
callID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PermissionCardSection = {
|
||||
label: string
|
||||
text: string
|
||||
mono?: boolean
|
||||
}
|
||||
|
||||
export type PermissionCardModel = {
|
||||
eyebrow: string
|
||||
title: string
|
||||
body: string
|
||||
sections: PermissionCardSection[]
|
||||
}
|
||||
|
||||
function record(input: unknown): Record<string, unknown> | null {
|
||||
if (!input || typeof input !== "object") return null
|
||||
return input as Record<string, unknown>
|
||||
}
|
||||
|
||||
function maybeString(input: unknown): string | undefined {
|
||||
return typeof input === "string" && input.trim().length > 0 ? input : undefined
|
||||
}
|
||||
|
||||
function stringList(input: unknown): string[] {
|
||||
if (!Array.isArray(input)) return []
|
||||
return input.filter((item): item is string => typeof item === "string" && item.length > 0)
|
||||
}
|
||||
|
||||
function previewText(input: string, options?: { maxLines?: number; maxChars?: number }): string {
|
||||
const maxLines = options?.maxLines ?? 18
|
||||
const maxChars = options?.maxChars ?? 1200
|
||||
const normalized = input.replace(/\r\n/g, "\n").trim()
|
||||
if (!normalized) return ""
|
||||
|
||||
const lines = normalized.split("\n")
|
||||
const sliced = lines.slice(0, maxLines)
|
||||
let text = sliced.join("\n")
|
||||
|
||||
if (text.length > maxChars) {
|
||||
text = `${text.slice(0, maxChars).trimEnd()}\n…`
|
||||
} else if (lines.length > maxLines) {
|
||||
text = `${text}\n…`
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function formatPath(input: string): string {
|
||||
return input.replace(/\\/g, "/")
|
||||
}
|
||||
|
||||
function permissionTool(input: unknown): PendingPermissionRequest["tool"] | undefined {
|
||||
const value = record(input)
|
||||
if (!value) return
|
||||
|
||||
const messageID = maybeString(value.messageID)
|
||||
const callID = maybeString(value.callID)
|
||||
if (!messageID || !callID) return
|
||||
|
||||
return {
|
||||
messageID,
|
||||
callID,
|
||||
}
|
||||
}
|
||||
|
||||
function parsePendingPermissionRequest(input: unknown): PendingPermissionRequest | null {
|
||||
const value = record(input)
|
||||
if (!value) return null
|
||||
|
||||
const id = maybeString(value.id)
|
||||
const sessionID = maybeString(value.sessionID)
|
||||
const permission = maybeString(value.permission)
|
||||
if (!id || !sessionID || !permission) return null
|
||||
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
permission,
|
||||
patterns: stringList(value.patterns),
|
||||
metadata: record(value.metadata) ?? {},
|
||||
always: stringList(value.always),
|
||||
tool: permissionTool(value.tool),
|
||||
}
|
||||
}
|
||||
|
||||
export { parsePendingPermissionRequest }
|
||||
|
||||
export function parsePendingPermissionRequests(payload: unknown): PendingPermissionRequest[] {
|
||||
if (!Array.isArray(payload)) return []
|
||||
|
||||
return payload
|
||||
.map((item) => parsePendingPermissionRequest(item))
|
||||
.filter((item): item is PendingPermissionRequest => item !== null)
|
||||
}
|
||||
|
||||
function firstPattern(request: PendingPermissionRequest): string | undefined {
|
||||
return request.patterns.find((item) => item.trim().length > 0)
|
||||
}
|
||||
|
||||
function externalDirectory(request: PendingPermissionRequest): string | undefined {
|
||||
const fromMetadata = maybeString(request.metadata.parentDir) ?? maybeString(request.metadata.filepath)
|
||||
if (fromMetadata) return fromMetadata
|
||||
|
||||
const pattern = firstPattern(request)
|
||||
if (!pattern) return
|
||||
return pattern.endsWith("/*") ? pattern.slice(0, -2) : pattern
|
||||
}
|
||||
|
||||
function allowScopeSection(request: PendingPermissionRequest): PermissionCardSection | null {
|
||||
if (request.always.length === 0) return null
|
||||
if (
|
||||
request.always.length === request.patterns.length &&
|
||||
request.always.every((item, index) => item === request.patterns[index])
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (request.always.length === 1 && request.always[0] === "*") {
|
||||
return {
|
||||
label: "Always allow",
|
||||
text: "Applies to all future requests of this permission until OpenCode restarts.",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: "Always allow scope",
|
||||
text: previewText(request.always.join("\n"), { maxLines: 8, maxChars: 600 }),
|
||||
mono: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPermissionCardModel(request: PendingPermissionRequest): PermissionCardModel {
|
||||
const filepath = maybeString(request.metadata.filepath)
|
||||
const diff = maybeString(request.metadata.diff)
|
||||
const commandText = previewText(request.patterns.join("\n"), { maxLines: 6, maxChars: 700 })
|
||||
const scope = allowScopeSection(request)
|
||||
|
||||
if (request.permission === "edit") {
|
||||
const sections: PermissionCardSection[] = []
|
||||
if (filepath) {
|
||||
sections.push({ label: "File", text: formatPath(filepath), mono: true })
|
||||
}
|
||||
if (diff) {
|
||||
sections.push({ label: "Diff preview", text: previewText(diff), mono: true })
|
||||
}
|
||||
if (scope) sections.push(scope)
|
||||
|
||||
return {
|
||||
eyebrow: "EDIT",
|
||||
title: "Allow file edit?",
|
||||
body: "OpenCode wants to change a file in this session.",
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
if (request.permission === "bash") {
|
||||
const sections: PermissionCardSection[] = []
|
||||
if (commandText) {
|
||||
sections.push({ label: "Command", text: commandText, mono: true })
|
||||
}
|
||||
if (scope) sections.push(scope)
|
||||
|
||||
return {
|
||||
eyebrow: "BASH",
|
||||
title: "Allow shell command?",
|
||||
body: "OpenCode wants to run a shell command for this session.",
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
if (request.permission === "read") {
|
||||
const sections: PermissionCardSection[] = []
|
||||
const path = firstPattern(request)
|
||||
if (path) {
|
||||
sections.push({ label: "Path", text: formatPath(path), mono: true })
|
||||
}
|
||||
if (scope) sections.push(scope)
|
||||
|
||||
return {
|
||||
eyebrow: "READ",
|
||||
title: "Allow file read?",
|
||||
body: "OpenCode wants to read a path from your machine.",
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
if (request.permission === "external_directory") {
|
||||
const sections: PermissionCardSection[] = []
|
||||
const dir = externalDirectory(request)
|
||||
if (dir) {
|
||||
sections.push({ label: "Directory", text: formatPath(dir), mono: true })
|
||||
}
|
||||
if (request.patterns.length > 0) {
|
||||
sections.push({
|
||||
label: "Patterns",
|
||||
text: previewText(request.patterns.join("\n"), { maxLines: 8, maxChars: 600 }),
|
||||
mono: true,
|
||||
})
|
||||
}
|
||||
if (scope) sections.push(scope)
|
||||
|
||||
return {
|
||||
eyebrow: "DIRECTORY",
|
||||
title: "Allow external access?",
|
||||
body: "OpenCode wants to work with files outside the current project directory.",
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
if (request.permission === "task") {
|
||||
const sections: PermissionCardSection[] = []
|
||||
if (request.patterns.length > 0) {
|
||||
sections.push({
|
||||
label: "Patterns",
|
||||
text: previewText(request.patterns.join("\n"), { maxLines: 8, maxChars: 600 }),
|
||||
mono: true,
|
||||
})
|
||||
}
|
||||
if (scope) sections.push(scope)
|
||||
|
||||
return {
|
||||
eyebrow: "TASK",
|
||||
title: "Allow delegated task?",
|
||||
body: "OpenCode wants to launch another task as part of this session.",
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
const sections: PermissionCardSection[] = []
|
||||
if (request.patterns.length > 0) {
|
||||
sections.push({
|
||||
label: "Patterns",
|
||||
text: previewText(request.patterns.join("\n"), { maxLines: 8, maxChars: 600 }),
|
||||
mono: true,
|
||||
})
|
||||
}
|
||||
if (scope) sections.push(scope)
|
||||
|
||||
return {
|
||||
eyebrow: request.permission.toUpperCase(),
|
||||
title: `Allow ${request.permission}?`,
|
||||
body: "OpenCode needs your permission before it can continue this session.",
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,17 @@ export type SessionItem = {
|
|||
id: string
|
||||
title: string
|
||||
updated: number
|
||||
directory?: string
|
||||
workspaceID?: string
|
||||
projectID?: string
|
||||
}
|
||||
|
||||
type ServerSessionPayload = {
|
||||
id?: unknown
|
||||
title?: unknown
|
||||
directory?: unknown
|
||||
workspaceID?: unknown
|
||||
projectID?: unknown
|
||||
time?: {
|
||||
updated?: unknown
|
||||
}
|
||||
|
|
@ -50,11 +56,21 @@ export function parseSessionItems(payload: unknown): SessionItem[] {
|
|||
|
||||
return payload
|
||||
.filter((item): item is ServerSessionPayload => !!item && typeof item === "object")
|
||||
.map((item) => ({
|
||||
.map((item) => {
|
||||
const directory = typeof item.directory === "string" && item.directory.length > 0 ? item.directory : undefined
|
||||
const workspaceID =
|
||||
typeof item.workspaceID === "string" && item.workspaceID.length > 0 ? item.workspaceID : undefined
|
||||
const projectID = typeof item.projectID === "string" && item.projectID.length > 0 ? item.projectID : undefined
|
||||
|
||||
return {
|
||||
id: String(item.id ?? ""),
|
||||
title: String(item.title ?? item.id ?? "Untitled session"),
|
||||
updated: Number(item.time?.updated ?? 0),
|
||||
}))
|
||||
directory,
|
||||
workspaceID,
|
||||
projectID,
|
||||
}
|
||||
})
|
||||
.filter((item) => item.id.length > 0)
|
||||
.sort((a, b) => b.updated - a.updated)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue