@@ -54,6 +54,8 @@ import {
5454 MenuSubTrigger ,
5555 MenuTrigger ,
5656} from "../ui/menu" ;
57+ import { readLocalApi } from "~/localApi" ;
58+
5759import { toastManager } from "../ui/toast" ;
5860import {
5961 AlertDialog ,
@@ -117,7 +119,7 @@ const zoomLabel = (zoomFactor: number) => `${Math.round(zoomFactor * 100)}%`;
117119 * it. Anything unrecognised reads as a plain read failure rather than leaking
118120 * the raw message into a toast.
119121 */
120- const importFailureReason = ( cause : unknown ) : BrowserImportFailureReason => {
122+ export const importFailureReason = ( cause : unknown ) : BrowserImportFailureReason => {
121123 const message = String ( ( cause as { message ?: unknown } | undefined ) ?. message ?? "" ) ;
122124 return (
123125 BrowserImportFailureReason . literals . find ( ( reason ) => message . includes ( `failed: ${ reason } .` ) ) ??
@@ -520,13 +522,25 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
520522 * files, and the answer changes while the app is running (quitting the browser
521523 * clears `browserRunning`), so a value cached at mount would go stale.
522524 */
525+ /**
526+ * Opens System Settings → Privacy & Security → Full Disk Access. The scheme is
527+ * unchanged from the old System Preferences and still resolves on Ventura and
528+ * later.
529+ */
530+ const FULL_DISK_ACCESS_SETTINGS_URL =
531+ "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFilesAccess" ;
532+
523533function BrowserProfilesSetting ( { disabled } : { readonly disabled : boolean } ) {
524534 const userProfiles = useClientSettings ( ( settings ) => settings . browserProfiles ) ;
525535 const defaultProfileId = useClientSettings ( ( settings ) => settings . browserDefaultProfileId ) ;
526536 const updateSettings = useUpdatePrimarySettings ( ) ;
527537 const environmentId = usePrimaryEnvironment ( ) ?. environmentId ;
528538 const [ sources , setSources ] = useState < ReadonlyArray < BrowserImportSource > | null > ( null ) ;
529539 const [ busy , setBusy ] = useState ( false ) ;
540+ // Safari's cookies sit behind Full Disk Access, which macOS never prompts
541+ // for — the app is added by hand. A dialog with a link into the right pane
542+ // is more use than a toast that names a setting the user then has to find.
543+ const [ fullDiskAccessSource , setFullDiskAccessSource ] = useState < string | null > ( null ) ;
530544 const [ profilePendingRemoval , setProfilePendingRemoval ] = useState < BrowserProfile | null > ( null ) ;
531545
532546 const profiles = resolveBrowserProfiles ( userProfiles ) ;
@@ -653,10 +667,15 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
653667 } ) ;
654668 } )
655669 . catch ( ( cause : unknown ) => {
670+ const reason = importFailureReason ( cause ) ;
671+ if ( reason === "needsFullDiskAccess" ) {
672+ setFullDiskAccessSource ( source . name ) ;
673+ return ;
674+ }
656675 toastManager . add ( {
657676 type : "error" ,
658677 title : `Could not import from ${ source . name } ` ,
659- description : BROWSER_IMPORT_FAILURE_COPY [ importFailureReason ( cause ) ] ,
678+ description : BROWSER_IMPORT_FAILURE_COPY [ reason ] ,
660679 } ) ;
661680 } )
662681 . finally ( ( ) => setBusy ( false ) ) ;
@@ -882,6 +901,36 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
882901 </ AlertDialogFooter >
883902 </ AlertDialogPopup >
884903 </ AlertDialog >
904+ < AlertDialog
905+ open = { fullDiskAccessSource !== null }
906+ onOpenChange = { ( open ) => {
907+ if ( ! open ) setFullDiskAccessSource ( null ) ;
908+ } }
909+ >
910+ < AlertDialogPopup >
911+ < AlertDialogHeader >
912+ < AlertDialogTitle > T3 Code needs Full Disk Access</ AlertDialogTitle >
913+ < AlertDialogDescription >
914+ { fullDiskAccessSource ?? "Safari" } keeps its cookies in a protected folder. Turn on
915+ Full Disk Access for T3 Code in System Settings, then run the import again. macOS
916+ doesn’t ask for this permission — you add the app yourself.
917+ </ AlertDialogDescription >
918+ </ AlertDialogHeader >
919+ < AlertDialogFooter >
920+ < AlertDialogClose render = { < Button variant = "outline" /> } > Not now</ AlertDialogClose >
921+ < AlertDialogClose
922+ render = { < Button /> }
923+ onClick = { ( ) => {
924+ void readLocalApi ( )
925+ ?. shell . openExternal ( FULL_DISK_ACCESS_SETTINGS_URL )
926+ . catch ( ( ) => undefined ) ;
927+ } }
928+ >
929+ Open System Settings
930+ </ AlertDialogClose >
931+ </ AlertDialogFooter >
932+ </ AlertDialogPopup >
933+ </ AlertDialog >
885934 </ SettingsRow >
886935 ) ;
887936}
0 commit comments