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,
|
LayoutChangeEvent,
|
||||||
Linking,
|
Linking,
|
||||||
Platform,
|
Platform,
|
||||||
|
Switch,
|
||||||
} from "react-native"
|
} from "react-native"
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
|
|
@ -34,8 +35,9 @@ import { AudioPcmStreamAdapter } from "whisper.rn/src/realtime-transcription/ada
|
||||||
import { AudioManager } from "react-native-audio-api"
|
import { AudioManager } from "react-native-audio-api"
|
||||||
import * as FileSystem from "expo-file-system/legacy"
|
import * as FileSystem from "expo-file-system/legacy"
|
||||||
import { fetch as expoFetch } from "expo/fetch"
|
import { fetch as expoFetch } from "expo/fetch"
|
||||||
|
import { buildPermissionCardModel } from "@/lib/pending-permissions"
|
||||||
import { unregisterRelayDevice } from "@/lib/relay-client"
|
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 { looksLikeLocalHost, useServerSessions } from "@/hooks/use-server-sessions"
|
||||||
import { ensureNotificationPermissions, getDevicePushToken } from "@/notifications/monitoring-notifications"
|
import { ensureNotificationPermissions, getDevicePushToken } from "@/notifications/monitoring-notifications"
|
||||||
|
|
||||||
|
|
@ -77,15 +79,6 @@ const WHISPER_MODELS = [
|
||||||
"ggml-medium-q5_0.bin",
|
"ggml-medium-q5_0.bin",
|
||||||
"ggml-medium-q8_0.bin",
|
"ggml-medium-q8_0.bin",
|
||||||
"ggml-medium.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
|
] as const
|
||||||
|
|
||||||
type WhisperModelID = (typeof WHISPER_MODELS)[number]
|
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-q5_0.bin": "medium q5_0",
|
||||||
"ggml-medium-q8_0.bin": "medium q8_0",
|
"ggml-medium-q8_0.bin": "medium q8_0",
|
||||||
"ggml-medium.bin": "medium",
|
"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> = {
|
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-q5_0.bin": 539212467,
|
||||||
"ggml-medium-q8_0.bin": 823369779,
|
"ggml-medium-q8_0.bin": 823369779,
|
||||||
"ggml-medium.bin": 1533763059,
|
"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 {
|
function isWhisperModelID(value: unknown): value is WhisperModelID {
|
||||||
|
|
@ -271,6 +246,7 @@ type Scan = {
|
||||||
type WhisperSavedState = {
|
type WhisperSavedState = {
|
||||||
defaultModel: WhisperModelID
|
defaultModel: WhisperModelID
|
||||||
mode: TranscriptionMode
|
mode: TranscriptionMode
|
||||||
|
autoSendOnDictationEnd: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnboardingSavedState = {
|
type OnboardingSavedState = {
|
||||||
|
|
@ -402,6 +378,7 @@ export default function DictationScreen() {
|
||||||
const [downloadProgress, setDownloadProgress] = useState(0)
|
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||||
const [isPreparingWhisperModel, setIsPreparingWhisperModel] = useState(true)
|
const [isPreparingWhisperModel, setIsPreparingWhisperModel] = useState(true)
|
||||||
const [transcriptionMode, setTranscriptionMode] = useState<TranscriptionMode>(DEFAULT_TRANSCRIPTION_MODE)
|
const [transcriptionMode, setTranscriptionMode] = useState<TranscriptionMode>(DEFAULT_TRANSCRIPTION_MODE)
|
||||||
|
const [autoSendOnDictationEnd, setAutoSendOnDictationEnd] = useState(false)
|
||||||
const [isTranscribingBulk, setIsTranscribingBulk] = useState(false)
|
const [isTranscribingBulk, setIsTranscribingBulk] = useState(false)
|
||||||
const [whisperError, setWhisperError] = useState("")
|
const [whisperError, setWhisperError] = useState("")
|
||||||
const [transcribedText, setTranscribedText] = useState("")
|
const [transcribedText, setTranscribedText] = useState("")
|
||||||
|
|
@ -413,6 +390,7 @@ export default function DictationScreen() {
|
||||||
const [agentStateDismissed, setAgentStateDismissed] = useState(false)
|
const [agentStateDismissed, setAgentStateDismissed] = useState(false)
|
||||||
const [dropdownMode, setDropdownMode] = useState<DropdownMode>("none")
|
const [dropdownMode, setDropdownMode] = useState<DropdownMode>("none")
|
||||||
const [dropdownRenderMode, setDropdownRenderMode] = useState<Exclude<DropdownMode, "none">>("server")
|
const [dropdownRenderMode, setDropdownRenderMode] = useState<Exclude<DropdownMode, "none">>("server")
|
||||||
|
const [sessionCreateMode, setSessionCreateMode] = useState<"same" | "root" | null>(null)
|
||||||
const [scanOpen, setScanOpen] = useState(false)
|
const [scanOpen, setScanOpen] = useState(false)
|
||||||
const [camGranted, setCamGranted] = useState(false)
|
const [camGranted, setCamGranted] = useState(false)
|
||||||
const [waveformLevels, setWaveformLevels] = useState<number[]>(Array.from({ length: 24 }, () => 0))
|
const [waveformLevels, setWaveformLevels] = useState<number[]>(Array.from({ length: 24 }, () => 0))
|
||||||
|
|
@ -437,6 +415,7 @@ export default function DictationScreen() {
|
||||||
const bulkAudioChunksRef = useRef<Uint8Array[]>([])
|
const bulkAudioChunksRef = useRef<Uint8Array[]>([])
|
||||||
const bulkTranscriptionJobRef = useRef(0)
|
const bulkTranscriptionJobRef = useRef(0)
|
||||||
const downloadProgressRef = useRef(0)
|
const downloadProgressRef = useRef(0)
|
||||||
|
const autoSendSignatureRef = useRef("")
|
||||||
const waveformPulseIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
const waveformPulseIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
const sendSettleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const sendSettleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const scanLockRef = useRef(false)
|
const scanLockRef = useRef(false)
|
||||||
|
|
@ -462,15 +441,20 @@ export default function DictationScreen() {
|
||||||
selectSession,
|
selectSession,
|
||||||
removeServer,
|
removeServer,
|
||||||
addServer,
|
addServer,
|
||||||
|
createSession,
|
||||||
findServerForSession,
|
findServerForSession,
|
||||||
} = useServerSessions()
|
} = useServerSessions()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
beginMonitoring,
|
beginMonitoring,
|
||||||
|
activePermissionRequest,
|
||||||
devicePushToken,
|
devicePushToken,
|
||||||
latestAssistantResponse,
|
latestAssistantResponse,
|
||||||
monitorJob,
|
monitorJob,
|
||||||
monitorStatus,
|
monitorStatus,
|
||||||
|
pendingPermissionCount,
|
||||||
|
respondingPermissionID,
|
||||||
|
respondToPermission,
|
||||||
setDevicePushToken,
|
setDevicePushToken,
|
||||||
setMonitorStatus,
|
setMonitorStatus,
|
||||||
} = useMonitoring({
|
} = useMonitoring({
|
||||||
|
|
@ -727,6 +711,7 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
let nextDefaultModel: WhisperModelID = DEFAULT_WHISPER_MODEL
|
let nextDefaultModel: WhisperModelID = DEFAULT_WHISPER_MODEL
|
||||||
let nextMode: TranscriptionMode = DEFAULT_TRANSCRIPTION_MODE
|
let nextMode: TranscriptionMode = DEFAULT_TRANSCRIPTION_MODE
|
||||||
|
let nextAutoSendOnDictationEnd = false
|
||||||
try {
|
try {
|
||||||
const data = await FileSystem.readAsStringAsync(WHISPER_SETTINGS_FILE)
|
const data = await FileSystem.readAsStringAsync(WHISPER_SETTINGS_FILE)
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|
@ -737,6 +722,9 @@ export default function DictationScreen() {
|
||||||
if (isTranscriptionMode(parsed.mode)) {
|
if (isTranscriptionMode(parsed.mode)) {
|
||||||
nextMode = parsed.mode
|
nextMode = parsed.mode
|
||||||
}
|
}
|
||||||
|
if (parsed.autoSendOnDictationEnd === true) {
|
||||||
|
nextAutoSendOnDictationEnd = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Use default settings if state file is missing or invalid.
|
// Use default settings if state file is missing or invalid.
|
||||||
|
|
@ -747,6 +735,7 @@ export default function DictationScreen() {
|
||||||
whisperRestoredRef.current = true
|
whisperRestoredRef.current = true
|
||||||
setDefaultWhisperModel(nextDefaultModel)
|
setDefaultWhisperModel(nextDefaultModel)
|
||||||
setTranscriptionMode(nextMode)
|
setTranscriptionMode(nextMode)
|
||||||
|
setAutoSendOnDictationEnd(nextAutoSendOnDictationEnd)
|
||||||
|
|
||||||
await refreshInstalledWhisperModels()
|
await refreshInstalledWhisperModels()
|
||||||
|
|
||||||
|
|
@ -768,9 +757,13 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!whisperRestoredRef.current) return
|
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(() => {})
|
void FileSystem.writeAsStringAsync(WHISPER_SETTINGS_FILE, JSON.stringify(payload)).catch(() => {})
|
||||||
}, [defaultWhisperModel, transcriptionMode])
|
}, [autoSendOnDictationEnd, defaultWhisperModel, transcriptionMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -1140,6 +1133,26 @@ export default function DictationScreen() {
|
||||||
setAgentStateDismissed(true)
|
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(() => {
|
const resetTranscriptState = useCallback(() => {
|
||||||
if (isRecordingRef.current) {
|
if (isRecordingRef.current) {
|
||||||
stopRecording()
|
stopRecording()
|
||||||
|
|
@ -1454,6 +1467,7 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
const modelDownloading = downloadingModelID !== null
|
const modelDownloading = downloadingModelID !== null
|
||||||
const modelLoading = isPreparingWhisperModel || activeWhisperModel == null || modelDownloading || isTranscribingBulk
|
const modelLoading = isPreparingWhisperModel || activeWhisperModel == null || modelDownloading || isTranscribingBulk
|
||||||
|
const dictationSettingsLocked = isRecording || isTranscribingBulk || isSending
|
||||||
let modelLoadingState: "downloading" | "loading" | "ready" = "ready"
|
let modelLoadingState: "downloading" | "loading" | "ready" = "ready"
|
||||||
if (modelDownloading) {
|
if (modelDownloading) {
|
||||||
modelLoadingState = "downloading"
|
modelLoadingState = "downloading"
|
||||||
|
|
@ -1466,20 +1480,29 @@ export default function DictationScreen() {
|
||||||
: WHISPER_MODEL_LABELS[defaultWhisperModel]
|
: WHISPER_MODEL_LABELS[defaultWhisperModel]
|
||||||
const hasTranscript = transcribedText.trim().length > 0
|
const hasTranscript = transcribedText.trim().length > 0
|
||||||
const hasAssistantResponse = latestAssistantResponse.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 hasAgentActivity = hasAssistantResponse || monitorStatus.trim().length > 0 || monitorJob !== null
|
||||||
const shouldShowAgentStateCard = hasAgentActivity && !agentStateDismissed
|
const shouldShowAgentStateCard = !hasPendingPermission && hasAgentActivity && !agentStateDismissed
|
||||||
const showsCompleteState = monitorStatus.toLowerCase().includes("complete")
|
const showsCompleteState = monitorStatus.toLowerCase().includes("complete")
|
||||||
let agentStateIcon: "loading" | "done" = "loading"
|
let agentStateIcon: "loading" | "done" = "loading"
|
||||||
if (monitorJob === null && (hasAssistantResponse || showsCompleteState)) {
|
if (monitorJob === null && (hasAssistantResponse || showsCompleteState)) {
|
||||||
agentStateIcon = "done"
|
agentStateIcon = "done"
|
||||||
}
|
}
|
||||||
const agentStateText = hasAssistantResponse ? latestAssistantResponse : "Waiting for agent…"
|
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 activeServer = servers.find((s) => s.id === activeServerId) ?? null
|
||||||
const activeSession = activeServer?.sessions.find((s) => s.id === activeSessionId) ?? null
|
const activeSession = activeServer?.sessions.find((s) => s.id === activeSessionId) ?? null
|
||||||
const canSendToSession = !!activeServer && activeServer.status === "online" && !!activeSession
|
const canSendToSession = !!activeServer && activeServer.status === "online" && !!activeSession
|
||||||
|
const isReplyingToActivePermission =
|
||||||
|
activePermissionRequest !== null && respondingPermissionID === activePermissionRequest.id
|
||||||
|
const displayedTranscript = isSending ? "" : transcribedText
|
||||||
const isDropdownOpen = dropdownMode !== "none"
|
const isDropdownOpen = dropdownMode !== "none"
|
||||||
const effectiveDropdownMode = isDropdownOpen ? dropdownMode : dropdownRenderMode
|
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"
|
const headerTitle = activeServer?.name ?? "No server configured"
|
||||||
let headerDotStyle = styles.serverStatusOffline
|
let headerDotStyle = styles.serverStatusOffline
|
||||||
if (activeServer?.status === "online") {
|
if (activeServer?.status === "online") {
|
||||||
|
|
@ -1534,6 +1557,46 @@ export default function DictationScreen() {
|
||||||
})
|
})
|
||||||
}, [shouldShowSend, sendVisibility])
|
}, [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.
|
// Parent clips outer half of center-stroke, so only inner half is visible.
|
||||||
// borderWidth 6 → 3px visible inward, borderWidth 12 → 6px visible inward.
|
// borderWidth 6 → 3px visible inward, borderWidth 12 → 6px visible inward.
|
||||||
const animatedBorderStyle = useAnimatedStyle(() => {
|
const animatedBorderStyle = useAnimatedStyle(() => {
|
||||||
|
|
@ -1590,8 +1653,15 @@ export default function DictationScreen() {
|
||||||
const menuRows =
|
const menuRows =
|
||||||
effectiveDropdownMode === "server" ? Math.max(servers.length, 1) : Math.max(activeServer?.sessions.length ?? 0, 1)
|
effectiveDropdownMode === "server" ? Math.max(servers.length, 1) : Math.max(activeServer?.sessions.length ?? 0, 1)
|
||||||
const expandedRowsHeight = Math.min(menuRows, DROPDOWN_VISIBLE_ROWS) * 42
|
const expandedRowsHeight = Math.min(menuRows, DROPDOWN_VISIBLE_ROWS) * 42
|
||||||
const addServerExtraHeight = effectiveDropdownMode === "server" ? 38 : 8
|
const dropdownFooterExtraHeight =
|
||||||
const expandedHeaderHeight = 51 + 12 + expandedRowsHeight + addServerExtraHeight
|
effectiveDropdownMode === "server"
|
||||||
|
? 38
|
||||||
|
: sessionCreationChoiceCount === 2
|
||||||
|
? 72
|
||||||
|
: sessionCreationChoiceCount === 1
|
||||||
|
? 38
|
||||||
|
: 8
|
||||||
|
const expandedHeaderHeight = 51 + 12 + expandedRowsHeight + dropdownFooterExtraHeight
|
||||||
|
|
||||||
const animatedHeaderStyle = useAnimatedStyle(() => ({
|
const animatedHeaderStyle = useAnimatedStyle(() => ({
|
||||||
height: interpolate(serverMenuProgress.value, [0, 1], [51, expandedHeaderHeight], Extrapolation.CLAMP),
|
height: interpolate(serverMenuProgress.value, [0, 1], [51, expandedHeaderHeight], Extrapolation.CLAMP),
|
||||||
|
|
@ -1711,6 +1781,49 @@ export default function DictationScreen() {
|
||||||
[selectSession],
|
[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(
|
const handleDeleteServer = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const server = serversRef.current.find((s) => s.id === id)
|
const server = serversRef.current.find((s) => s.id === id)
|
||||||
|
|
@ -2212,6 +2325,55 @@ export default function DictationScreen() {
|
||||||
<Pressable onPress={() => void handleStartScan()} style={styles.addServerButton}>
|
<Pressable onPress={() => void handleStartScan()} style={styles.addServerButton}>
|
||||||
<Text style={styles.addServerButtonText}>Add server by scanning QR code</Text>
|
<Text style={styles.addServerButtonText}>Add server by scanning QR code</Text>
|
||||||
</Pressable>
|
</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}
|
) : null}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -2219,7 +2381,91 @@ export default function DictationScreen() {
|
||||||
|
|
||||||
{/* Transcription area */}
|
{/* Transcription area */}
|
||||||
<View style={styles.transcriptionArea}>
|
<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.splitCardStack}>
|
||||||
<View style={[styles.splitCard, styles.replyCard]}>
|
<View style={[styles.splitCard, styles.replyCard]}>
|
||||||
<View style={styles.agentStateHeaderRow}>
|
<View style={styles.agentStateHeaderRow}>
|
||||||
|
|
@ -2282,9 +2528,9 @@ export default function DictationScreen() {
|
||||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||||
>
|
>
|
||||||
<Animated.View style={animatedTranscriptSendStyle}>
|
<Animated.View style={animatedTranscriptSendStyle}>
|
||||||
{transcribedText ? (
|
{displayedTranscript ? (
|
||||||
<Text style={styles.transcriptionText}>{transcribedText}</Text>
|
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||||
) : (
|
) : isSending ? null : (
|
||||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -2342,9 +2588,9 @@ export default function DictationScreen() {
|
||||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||||
>
|
>
|
||||||
<Animated.View style={animatedTranscriptSendStyle}>
|
<Animated.View style={animatedTranscriptSendStyle}>
|
||||||
{transcribedText ? (
|
{displayedTranscript ? (
|
||||||
<Text style={styles.transcriptionText}>{transcribedText}</Text>
|
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||||
) : (
|
) : isSending ? null : (
|
||||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -2367,60 +2613,61 @@ export default function DictationScreen() {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Record button */}
|
{hasPendingPermission ? null : (
|
||||||
<View style={styles.controlsRow} onLayout={handleControlsLayout}>
|
<View style={styles.controlsRow} onLayout={handleControlsLayout}>
|
||||||
<Pressable
|
|
||||||
onPressIn={handlePressIn}
|
|
||||||
onPressOut={handlePressOut}
|
|
||||||
disabled={!permissionGranted || modelLoading}
|
|
||||||
style={[styles.recordPressable, !permissionGranted && styles.recordButtonDisabled]}
|
|
||||||
>
|
|
||||||
<View style={styles.recordButton}>
|
|
||||||
{isTranscribingBulk ? (
|
|
||||||
<View style={styles.recordBusyCenter}>
|
|
||||||
<ActivityIndicator color="#FF2E3F" size="small" />
|
|
||||||
</View>
|
|
||||||
) : modelLoadingState !== "ready" ? (
|
|
||||||
<>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.loadFill,
|
|
||||||
modelLoadingState === "loading" && styles.loadFillPending,
|
|
||||||
{ width: modelLoadingState === "downloading" ? `${Math.max(pct, 3)}%` : "100%" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<View style={styles.loadOverlay} pointerEvents="none">
|
|
||||||
<Text style={styles.loadText}>
|
|
||||||
{modelLoadingState === "downloading"
|
|
||||||
? `Downloading ${loadingModelLabel} ${pct}%`
|
|
||||||
: `Loading ${loadingModelLabel}`}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Animated.View style={[styles.recordBorder, animatedBorderStyle]} pointerEvents="none" />
|
|
||||||
<Animated.View style={[styles.recordDot, animatedDotStyle]} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Animated.View style={[styles.sendSlot, animatedSendStyle]} pointerEvents={shouldShowSend ? "auto" : "none"}>
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleSendTranscript}
|
onPressIn={handlePressIn}
|
||||||
style={({ pressed }) => [
|
onPressOut={handlePressOut}
|
||||||
styles.sendButton,
|
disabled={!permissionGranted || modelLoading}
|
||||||
(isSending || !hasTranscript || !canSendToSession) && styles.sendButtonDisabled,
|
style={[styles.recordPressable, !permissionGranted && styles.recordButtonDisabled]}
|
||||||
pressed && styles.clearButtonPressed,
|
|
||||||
]}
|
|
||||||
disabled={isSending || !hasTranscript || !canSendToSession}
|
|
||||||
hitSlop={8}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.sendIcon}>↑</Text>
|
<View style={styles.recordButton}>
|
||||||
|
{isTranscribingBulk ? (
|
||||||
|
<View style={styles.recordBusyCenter}>
|
||||||
|
<ActivityIndicator color="#FF2E3F" size="small" />
|
||||||
|
</View>
|
||||||
|
) : modelLoadingState !== "ready" ? (
|
||||||
|
<>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.loadFill,
|
||||||
|
modelLoadingState === "loading" && styles.loadFillPending,
|
||||||
|
{ width: modelLoadingState === "downloading" ? `${Math.max(pct, 3)}%` : "100%" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View style={styles.loadOverlay} pointerEvents="none">
|
||||||
|
<Text style={styles.loadText}>
|
||||||
|
{modelLoadingState === "downloading"
|
||||||
|
? `Downloading ${loadingModelLabel} ${pct}%`
|
||||||
|
: `Loading ${loadingModelLabel}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Animated.View style={[styles.recordBorder, animatedBorderStyle]} pointerEvents="none" />
|
||||||
|
<Animated.View style={[styles.recordDot, animatedDotStyle]} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
|
||||||
</View>
|
<Animated.View style={[styles.sendSlot, animatedSendStyle]} pointerEvents={shouldShowSend ? "auto" : "none"}>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleSendTranscript}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
styles.sendButton,
|
||||||
|
(isSending || !hasTranscript || !canSendToSession) && styles.sendButtonDisabled,
|
||||||
|
pressed && styles.clearButtonPressed,
|
||||||
|
]}
|
||||||
|
disabled={isSending || !hasTranscript || !canSendToSession}
|
||||||
|
hitSlop={8}
|
||||||
|
>
|
||||||
|
<Text style={styles.sendIcon}>↑</Text>
|
||||||
|
</Pressable>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={whisperSettingsOpen}
|
visible={whisperSettingsOpen}
|
||||||
|
|
@ -2464,55 +2711,42 @@ export default function DictationScreen() {
|
||||||
<Text style={styles.settingsTextRowValue}>{WHISPER_MODEL_LABELS[defaultWhisperModel]}</Text>
|
<Text style={styles.settingsTextRowValue}>{WHISPER_MODEL_LABELS[defaultWhisperModel]}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Pressable
|
<View style={styles.settingsTextRow}>
|
||||||
onPress={() => setTranscriptionMode("bulk")}
|
|
||||||
disabled={isRecording || isTranscribingBulk}
|
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.settingsTextRow,
|
|
||||||
(isRecording || isTranscribingBulk) && styles.settingsInlinePressableDisabled,
|
|
||||||
pressed && styles.clearButtonPressed,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.settingsOptionCopy}>
|
<View style={styles.settingsOptionCopy}>
|
||||||
<Text style={styles.settingsTextRowTitle}>On Release</Text>
|
<Text style={styles.settingsTextRowTitle}>Realtime dictation</Text>
|
||||||
<Text style={styles.settingsTextRowMeta}>Transcribe after release</Text>
|
<Text style={styles.settingsTextRowMeta}>Turn off to transcribe after release</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Switch
|
||||||
style={[
|
value={transcriptionMode === "realtime"}
|
||||||
styles.settingsTextRowAction,
|
onValueChange={(enabled) => setTranscriptionMode(enabled ? "realtime" : "bulk")}
|
||||||
transcriptionMode === "bulk" && styles.settingsTextRowActionActive,
|
disabled={dictationSettingsLocked}
|
||||||
]}
|
trackColor={{ false: "#2D2D31", true: "#6A3A33" }}
|
||||||
>
|
thumbColor={transcriptionMode === "realtime" ? "#FF6B56" : "#F2F2F2"}
|
||||||
{transcriptionMode === "bulk" ? "Selected" : "Use"}
|
ios_backgroundColor="#2D2D31"
|
||||||
</Text>
|
/>
|
||||||
</Pressable>
|
</View>
|
||||||
|
|
||||||
<Pressable
|
<View style={styles.settingsTextRow}>
|
||||||
onPress={() => setTranscriptionMode("realtime")}
|
|
||||||
disabled={isRecording || isTranscribingBulk}
|
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.settingsTextRow,
|
|
||||||
(isRecording || isTranscribingBulk) && styles.settingsInlinePressableDisabled,
|
|
||||||
pressed && styles.clearButtonPressed,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.settingsOptionCopy}>
|
<View style={styles.settingsOptionCopy}>
|
||||||
<Text style={styles.settingsTextRowTitle}>Realtime</Text>
|
<Text style={styles.settingsTextRowTitle}>Auto send on dictation end</Text>
|
||||||
<Text style={styles.settingsTextRowMeta}>Transcribe while you speak</Text>
|
<Text style={styles.settingsTextRowMeta}>Send the transcript as soon as recording finishes</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Switch
|
||||||
style={[
|
value={autoSendOnDictationEnd}
|
||||||
styles.settingsTextRowAction,
|
onValueChange={setAutoSendOnDictationEnd}
|
||||||
transcriptionMode === "realtime" && styles.settingsTextRowActionActive,
|
disabled={dictationSettingsLocked}
|
||||||
]}
|
trackColor={{ false: "#2D2D31", true: "#6A3A33" }}
|
||||||
>
|
thumbColor={autoSendOnDictationEnd ? "#FF6B56" : "#F2F2F2"}
|
||||||
{transcriptionMode === "realtime" ? "Selected" : "Use"}
|
ios_backgroundColor="#2D2D31"
|
||||||
</Text>
|
/>
|
||||||
</Pressable>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.settingsSection}>
|
<View style={styles.settingsSection}>
|
||||||
<Text style={styles.settingsSectionLabel}>MODELS:</Text>
|
<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) => {
|
{WHISPER_MODELS.map((modelID) => {
|
||||||
const installed = installedWhisperModels.includes(modelID)
|
const installed = installedWhisperModels.includes(modelID)
|
||||||
const isDefault = defaultWhisperModel === modelID
|
const isDefault = defaultWhisperModel === modelID
|
||||||
|
|
@ -2995,6 +3229,35 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
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: {
|
statusLeft: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -3060,6 +3323,152 @@ const styles = StyleSheet.create({
|
||||||
replyCard: {
|
replyCard: {
|
||||||
paddingTop: 16,
|
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: {
|
transcriptionPanel: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
|
@ -3282,6 +3691,9 @@ const styles = StyleSheet.create({
|
||||||
borderBottomColor: "#242424",
|
borderBottomColor: "#242424",
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
},
|
},
|
||||||
|
settingsToggleRow: {
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
settingsMutedText: {
|
settingsMutedText: {
|
||||||
color: "#868686",
|
color: "#868686",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -3318,6 +3730,38 @@ const styles = StyleSheet.create({
|
||||||
settingsTextRowActionActive: {
|
settingsTextRowActionActive: {
|
||||||
color: "#FFD8D2",
|
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: {
|
settingsInlineRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ import {
|
||||||
type OpenCodeEvent,
|
type OpenCodeEvent,
|
||||||
type MonitorEventType,
|
type MonitorEventType,
|
||||||
} from "@/lib/opencode-events"
|
} from "@/lib/opencode-events"
|
||||||
|
import {
|
||||||
|
parsePendingPermissionRequest,
|
||||||
|
parsePendingPermissionRequests,
|
||||||
|
type PendingPermissionRequest,
|
||||||
|
} from "@/lib/pending-permissions"
|
||||||
import { registerRelayDevice, unregisterRelayDevice } from "@/lib/relay-client"
|
import { registerRelayDevice, unregisterRelayDevice } from "@/lib/relay-client"
|
||||||
import { parseSSEStream } from "@/lib/sse"
|
import { parseSSEStream } from "@/lib/sse"
|
||||||
import { getDevicePushToken, onPushTokenChange } from "@/notifications/monitoring-notifications"
|
import { getDevicePushToken, onPushTokenChange } from "@/notifications/monitoring-notifications"
|
||||||
|
|
@ -33,6 +38,8 @@ export type MonitorJob = {
|
||||||
startedAt: number
|
startedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PermissionDecision = "once" | "always" | "reject"
|
||||||
|
|
||||||
type SessionRuntimeStatus = "idle" | "busy" | "retry"
|
type SessionRuntimeStatus = "idle" | "busy" | "retry"
|
||||||
|
|
||||||
type PermissionPromptState = "idle" | "pending" | "granted" | "denied"
|
type PermissionPromptState = "idle" | "pending" | "granted" | "denied"
|
||||||
|
|
@ -114,6 +121,8 @@ export function useMonitoring({
|
||||||
const [monitorJob, setMonitorJob] = useState<MonitorJob | null>(null)
|
const [monitorJob, setMonitorJob] = useState<MonitorJob | null>(null)
|
||||||
const [monitorStatus, setMonitorStatus] = useState("")
|
const [monitorStatus, setMonitorStatus] = useState("")
|
||||||
const [latestAssistantResponse, setLatestAssistantResponse] = 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 [appState, setAppState] = useState<AppStateStatus>(AppState.currentState)
|
||||||
|
|
||||||
const foregroundMonitorAbortRef = useRef<AbortController | null>(null)
|
const foregroundMonitorAbortRef = useRef<AbortController | null>(null)
|
||||||
|
|
@ -127,6 +136,19 @@ export function useMonitoring({
|
||||||
const previousPushTokenRef = useRef<string | null>(null)
|
const previousPushTokenRef = useRef<string | null>(null)
|
||||||
const previousAppStateRef = useRef<AppStateStatus>(AppState.currentState)
|
const previousAppStateRef = useRef<AppStateStatus>(AppState.currentState)
|
||||||
const latestAssistantRequestRef = useRef(0)
|
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(() => {
|
useEffect(() => {
|
||||||
monitorJobRef.current = monitorJob
|
monitorJobRef.current = monitorJob
|
||||||
|
|
@ -243,6 +265,38 @@ export function useMonitoring({
|
||||||
[activeSessionIdRef, setAgentStateDismissed],
|
[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(
|
const fetchSessionRuntimeStatus = useCallback(
|
||||||
async (baseURL: string, sessionID: string): Promise<SessionRuntimeStatus | null> => {
|
async (baseURL: string, sessionID: string): Promise<SessionRuntimeStatus | null> => {
|
||||||
const base = baseURL.replace(/\/+$/, "")
|
const base = baseURL.replace(/\/+$/, "")
|
||||||
|
|
@ -278,6 +332,7 @@ export function useMonitoring({
|
||||||
|
|
||||||
if (eventType === "permission") {
|
if (eventType === "permission") {
|
||||||
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning).catch(() => {})
|
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning).catch(() => {})
|
||||||
|
void loadPendingPermissions(job.opencodeBaseURL, job.sessionID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,7 +350,7 @@ export function useMonitoring({
|
||||||
stopForegroundMonitor()
|
stopForegroundMonitor()
|
||||||
setMonitorJob(null)
|
setMonitorJob(null)
|
||||||
},
|
},
|
||||||
[completePlayer, loadLatestAssistantResponse, stopForegroundMonitor],
|
[completePlayer, loadLatestAssistantResponse, loadPendingPermissions, stopForegroundMonitor],
|
||||||
)
|
)
|
||||||
|
|
||||||
const startForegroundMonitor = useCallback(
|
const startForegroundMonitor = useCallback(
|
||||||
|
|
@ -333,6 +388,13 @@ export function useMonitoring({
|
||||||
const sessionID = extractSessionID(parsed)
|
const sessionID = extractSessionID(parsed)
|
||||||
if (sessionID !== job.sessionID) continue
|
if (sessionID !== job.sessionID) continue
|
||||||
|
|
||||||
|
if (parsed.type === "permission.asked") {
|
||||||
|
const request = parsePendingPermissionRequest(parsed.properties)
|
||||||
|
if (request) {
|
||||||
|
upsertPendingPermission(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const eventType = classifyMonitorEvent(parsed)
|
const eventType = classifyMonitorEvent(parsed)
|
||||||
if (!eventType) continue
|
if (!eventType) continue
|
||||||
|
|
||||||
|
|
@ -345,7 +407,7 @@ export function useMonitoring({
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
},
|
},
|
||||||
[handleMonitorEvent, stopForegroundMonitor],
|
[handleMonitorEvent, stopForegroundMonitor, upsertPendingPermission],
|
||||||
)
|
)
|
||||||
|
|
||||||
const beginMonitoring = useCallback(
|
const beginMonitoring = useCallback(
|
||||||
|
|
@ -381,13 +443,22 @@ export function useMonitoring({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLatestAssistantResponse("")
|
setLatestAssistantResponse("")
|
||||||
|
setPendingPermissions([])
|
||||||
setAgentStateDismissed(false)
|
setAgentStateDismissed(false)
|
||||||
if (!activeServerId || !activeSessionId) return
|
if (!activeServerId || !activeSessionId) return
|
||||||
|
|
||||||
const server = serversRef.current.find((item) => item.id === activeServerId)
|
const server = serversRef.current.find((item) => item.id === activeServerId)
|
||||||
if (!server || server.status !== "online") return
|
if (!server || server.status !== "online") return
|
||||||
void loadLatestAssistantResponse(server.url, activeSessionId)
|
void loadLatestAssistantResponse(server.url, activeSessionId)
|
||||||
}, [activeServerId, activeSessionId, loadLatestAssistantResponse, serversRef, setAgentStateDismissed])
|
void loadPendingPermissions(server.url, activeSessionId)
|
||||||
|
}, [
|
||||||
|
activeServerId,
|
||||||
|
activeSessionId,
|
||||||
|
loadLatestAssistantResponse,
|
||||||
|
loadPendingPermissions,
|
||||||
|
serversRef,
|
||||||
|
setAgentStateDismissed,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -404,6 +475,7 @@ export function useMonitoring({
|
||||||
|
|
||||||
const runtimeStatus = await fetchSessionRuntimeStatus(server.url, input.sessionID)
|
const runtimeStatus = await fetchSessionRuntimeStatus(server.url, input.sessionID)
|
||||||
await loadLatestAssistantResponse(server.url, input.sessionID)
|
await loadLatestAssistantResponse(server.url, input.sessionID)
|
||||||
|
await loadPendingPermissions(server.url, input.sessionID)
|
||||||
|
|
||||||
if (runtimeStatus === "busy" || runtimeStatus === "retry") {
|
if (runtimeStatus === "busy" || runtimeStatus === "retry") {
|
||||||
const nextJob: MonitorJob = {
|
const nextJob: MonitorJob = {
|
||||||
|
|
@ -433,6 +505,7 @@ export function useMonitoring({
|
||||||
appState,
|
appState,
|
||||||
fetchSessionRuntimeStatus,
|
fetchSessionRuntimeStatus,
|
||||||
loadLatestAssistantResponse,
|
loadLatestAssistantResponse,
|
||||||
|
loadPendingPermissions,
|
||||||
refreshServerStatusAndSessions,
|
refreshServerStatusAndSessions,
|
||||||
serversRef,
|
serversRef,
|
||||||
startForegroundMonitor,
|
startForegroundMonitor,
|
||||||
|
|
@ -548,6 +621,62 @@ export function useMonitoring({
|
||||||
void syncSessionState({ serverID, sessionID })
|
void syncSessionState({ serverID, sessionID })
|
||||||
}, [activeServerIdRef, activeSessionIdRef, appState, syncSessionState])
|
}, [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(
|
const relayServersKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
servers
|
servers
|
||||||
|
|
@ -658,6 +787,10 @@ export function useMonitoring({
|
||||||
monitorStatus,
|
monitorStatus,
|
||||||
setMonitorStatus,
|
setMonitorStatus,
|
||||||
latestAssistantResponse,
|
latestAssistantResponse,
|
||||||
|
activePermissionRequest,
|
||||||
|
pendingPermissionCount: pendingPermissions.length,
|
||||||
|
respondingPermissionID: replyingPermissionID,
|
||||||
|
respondToPermission,
|
||||||
beginMonitoring,
|
beginMonitoring,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,113 @@ export function useServerSessions() {
|
||||||
[refreshServerStatusAndSessions],
|
[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(
|
const findServerForSession = useCallback(
|
||||||
async (sessionID: string, preferredServerID?: string | null): Promise<ServerItem | null> => {
|
async (sessionID: string, preferredServerID?: string | null): Promise<ServerItem | null> => {
|
||||||
if (!serversRef.current.length && !restoredRef.current) {
|
if (!serversRef.current.length && !restoredRef.current) {
|
||||||
|
|
@ -381,6 +488,7 @@ export function useServerSessions() {
|
||||||
selectSession,
|
selectSession,
|
||||||
removeServer,
|
removeServer,
|
||||||
addServer,
|
addServer,
|
||||||
|
createSession,
|
||||||
findServerForSession,
|
findServerForSession,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,81 @@
|
||||||
export type OpenCodeEvent = {
|
export type OpenCodeEvent = {
|
||||||
type: string;
|
type: string
|
||||||
properties?: Record<string, unknown>;
|
properties?: Record<string, unknown>
|
||||||
};
|
}
|
||||||
|
|
||||||
export type MonitorEventType = 'complete' | 'permission' | 'error';
|
export type MonitorEventType = "complete" | "permission" | "error"
|
||||||
|
|
||||||
export function extractSessionID(event: OpenCodeEvent): string | null {
|
export function extractSessionID(event: OpenCodeEvent): string | null {
|
||||||
const props = event.properties ?? {};
|
const props = event.properties ?? {}
|
||||||
|
|
||||||
const fromDirect = props.sessionID;
|
const fromDirect = props.sessionID
|
||||||
if (typeof fromDirect === 'string' && fromDirect.length > 0) return fromDirect;
|
if (typeof fromDirect === "string" && fromDirect.length > 0) return fromDirect
|
||||||
|
|
||||||
const info = props.info;
|
const info = props.info
|
||||||
if (info && typeof info === 'object') {
|
if (info && typeof info === "object") {
|
||||||
const infoSessionID = (info as Record<string, unknown>).sessionID;
|
const infoSessionID = (info as Record<string, unknown>).sessionID
|
||||||
if (typeof infoSessionID === 'string' && infoSessionID.length > 0) return infoSessionID;
|
if (typeof infoSessionID === "string" && infoSessionID.length > 0) return infoSessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
const part = props.part;
|
const part = props.part
|
||||||
if (part && typeof part === 'object') {
|
if (part && typeof part === "object") {
|
||||||
const partSessionID = (part as Record<string, unknown>).sessionID;
|
const partSessionID = (part as Record<string, unknown>).sessionID
|
||||||
if (typeof partSessionID === 'string' && partSessionID.length > 0) return partSessionID;
|
if (typeof partSessionID === "string" && partSessionID.length > 0) return partSessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function classifyMonitorEvent(event: OpenCodeEvent): MonitorEventType | null {
|
export function classifyMonitorEvent(event: OpenCodeEvent): MonitorEventType | null {
|
||||||
const type = event.type;
|
const type = event.type
|
||||||
const lowerType = type.toLowerCase();
|
const lowerType = type.toLowerCase()
|
||||||
|
|
||||||
if (lowerType.includes('permission')) {
|
if (lowerType === "permission.asked" || lowerType === "permission") {
|
||||||
return 'permission';
|
return "permission"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lowerType.includes('error')) {
|
if (lowerType.includes("error")) {
|
||||||
return 'error';
|
return "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'session.status') {
|
if (type === "session.status") {
|
||||||
const status = event.properties?.status;
|
const status = event.properties?.status
|
||||||
if (status && typeof status === 'object') {
|
if (status && typeof status === "object") {
|
||||||
const statusType = (status as Record<string, unknown>).type;
|
const statusType = (status as Record<string, unknown>).type
|
||||||
if (statusType === 'idle') {
|
if (statusType === "idle") {
|
||||||
return 'complete';
|
return "complete"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'message.updated') {
|
if (type === "message.updated") {
|
||||||
const info = event.properties?.info;
|
const info = event.properties?.info
|
||||||
if (info && typeof info === 'object') {
|
if (info && typeof info === "object") {
|
||||||
const role = (info as Record<string, unknown>).role;
|
const role = (info as Record<string, unknown>).role
|
||||||
const time = (info as Record<string, unknown>).time;
|
const time = (info as Record<string, unknown>).time
|
||||||
if (role === 'assistant' && time && typeof time === 'object' && 'completed' in (time as Record<string, unknown>)) {
|
if (
|
||||||
return 'complete';
|
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 {
|
export function formatMonitorEventLabel(eventType: MonitorEventType): string {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'complete':
|
case "complete":
|
||||||
return 'Session complete';
|
return "Session complete"
|
||||||
case 'permission':
|
case "permission":
|
||||||
return 'Action needed';
|
return "Action needed"
|
||||||
case 'error':
|
case "error":
|
||||||
return 'Session error';
|
return "Session error"
|
||||||
default:
|
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
|
id: string
|
||||||
title: string
|
title: string
|
||||||
updated: number
|
updated: number
|
||||||
|
directory?: string
|
||||||
|
workspaceID?: string
|
||||||
|
projectID?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerSessionPayload = {
|
type ServerSessionPayload = {
|
||||||
id?: unknown
|
id?: unknown
|
||||||
title?: unknown
|
title?: unknown
|
||||||
|
directory?: unknown
|
||||||
|
workspaceID?: unknown
|
||||||
|
projectID?: unknown
|
||||||
time?: {
|
time?: {
|
||||||
updated?: unknown
|
updated?: unknown
|
||||||
}
|
}
|
||||||
|
|
@ -50,11 +56,21 @@ export function parseSessionItems(payload: unknown): SessionItem[] {
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
.filter((item): item is ServerSessionPayload => !!item && typeof item === "object")
|
.filter((item): item is ServerSessionPayload => !!item && typeof item === "object")
|
||||||
.map((item) => ({
|
.map((item) => {
|
||||||
id: String(item.id ?? ""),
|
const directory = typeof item.directory === "string" && item.directory.length > 0 ? item.directory : undefined
|
||||||
title: String(item.title ?? item.id ?? "Untitled session"),
|
const workspaceID =
|
||||||
updated: Number(item.time?.updated ?? 0),
|
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)
|
.filter((item) => item.id.length > 0)
|
||||||
.sort((a, b) => b.updated - a.updated)
|
.sort((a, b) => b.updated - a.updated)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue