diff --git a/src/app/App.tsx b/src/app/App.tsx
index f30f32a26..84993787a 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -33,7 +33,7 @@ import { TrialExpiredModal } from "@/components/shared/TrialExpiredModal";
import CloudAuthModal from "@/components/layout/CloudAuthModal";
import WhatsNewModal from "@/components/changelog/WhatsNewModal";
import { EmailVerificationRequiredModal } from "@/components/notifications/EmailVerificationRequiredModal";
-import { DeepLinkJoinModal } from "@/components/terminal/DeepLinkJoinModal";
+import { DeepLinkConfirmModal } from "@/components/terminal/DeepLinkConfirmModal";
import { useDeepLinkStore } from "@/stores/deepLinkStore";
import { GlobalTransferQueue } from "@/components/filetransfer/GlobalTransferQueue";
@@ -85,7 +85,7 @@ function App() {
-
+
{/* Global snippet variable modal — triggered from OmniSearch, the
diff --git a/src/components/settings/sections/PluginPermissionList.tsx b/src/components/settings/sections/PluginPermissionList.tsx
new file mode 100644
index 000000000..4c0af2d5e
--- /dev/null
+++ b/src/components/settings/sections/PluginPermissionList.tsx
@@ -0,0 +1,104 @@
+import { Icon } from "@iconify/react";
+import { useTranslation } from "react-i18next";
+import { describePermissions, type PermissionDescriptor } from "@/plugins/gatedPermissions";
+
+interface Props {
+ /** All permissions the plugin will hold after this action. */
+ permissions: string[];
+ /** For updates: permissions newly requested by this version (subset of `permissions`). */
+ addedPermissions?: string[];
+ /** Labels the block as "new permissions" — an update gate, not a first install. */
+ showNewHeading?: boolean;
+}
+
+/**
+ * Discloses what a plugin's code will be allowed to do. Shared by the settings
+ * install/update gate and the deep-link install sheet, so a link-driven install
+ * cannot end up disclosing less than a click-driven one.
+ */
+export function PluginPermissionList({ permissions, addedPermissions = [], showNewHeading = false }: Props) {
+ const { t } = useTranslation();
+ const added = new Set(addedPermissions);
+
+ const descriptors = describePermissions(permissions);
+ const ordinary = descriptors.filter((d) => !d.gated);
+ const readOnly = descriptors.filter((d) => d.gated && !d.danger);
+ const danger = descriptors.filter((d) => d.danger);
+
+ const renderRow = (d: PermissionDescriptor) => {
+ const isNew = added.has(d.perm);
+ // Three tones, not two: a gated read-only perm is neither ordinary (it still
+ // needs consent) nor destructive. Accent-tinted, no warning triangle.
+ const elevated = d.gated && !d.danger;
+ return (
+
+
+ {d.danger && }
+ {elevated && }
+ {isNew && }
+ {d.known ? t(d.labelKey) : d.perm}
+
+ {d.known && (
+
+ {t(d.descriptionKey)}
+
+ )}
+
+ );
+ };
+
+ if (descriptors.length === 0) {
+ return (
+
+ {t("settings.plugins.permissionModal.noPermissions")}
+
+ );
+ }
+
+ return (
+
+ {showNewHeading && (
+
+ {t("settings.plugins.permissionModal.newPermissions")}
+
+ )}
+ {ordinary.length > 0 && (
+
{ordinary.map(renderRow)}
+ )}
+ {readOnly.length > 0 && (
+
+
+
+ {t("settings.plugins.permissionModal.permissions.readOnlyHeading")}
+
+
+ {t("settings.plugins.permissionModal.permissions.readOnlyWarning")}
+
+ {readOnly.map(renderRow)}
+
+ )}
+ {danger.length > 0 && (
+
+
+
+ {t("settings.plugins.permissionModal.permissions.dangerHeading")}
+
+
+ {t("settings.plugins.permissionModal.permissions.dangerWarning")}
+
+ {danger.map(renderRow)}
+
+ )}
+
+ );
+}
diff --git a/src/components/settings/sections/PluginPermissionModal.tsx b/src/components/settings/sections/PluginPermissionModal.tsx
index a5234d0b6..0624c7ea3 100644
--- a/src/components/settings/sections/PluginPermissionModal.tsx
+++ b/src/components/settings/sections/PluginPermissionModal.tsx
@@ -1,7 +1,7 @@
import { Icon } from "@iconify/react";
import { useTranslation } from "react-i18next";
import { Modal, ModalCard } from "@/components/shared/Modal";
-import { describePermissions, type PermissionDescriptor } from "@/plugins/gatedPermissions";
+import { PluginPermissionList } from "@/components/settings/sections/PluginPermissionList";
interface Props {
mode: "install" | "update";
@@ -29,46 +29,8 @@ export function PluginPermissionModal({
onCancel,
}: Props) {
const { t } = useTranslation();
- const added = new Set(addedPermissions);
const isUpdate = mode === "update";
- const descriptors = describePermissions(permissions);
- const ordinary = descriptors.filter((d) => !d.gated);
- const readOnly = descriptors.filter((d) => d.gated && !d.danger);
- const danger = descriptors.filter((d) => d.danger);
-
- const renderRow = (d: PermissionDescriptor) => {
- const isNew = added.has(d.perm);
- // Three tones, not two: a gated read-only perm is neither ordinary (it still
- // needs consent) nor destructive. Accent-tinted, no warning triangle.
- const elevated = d.gated && !d.danger;
- return (
-
-
- {d.danger && }
- {elevated && }
- {isNew && }
- {d.known ? t(d.labelKey) : d.perm}
-
- {d.known && (
-
- {t(d.descriptionKey)}
-
- )}
-
- );
- };
-
return (
@@ -92,46 +54,11 @@ export function PluginPermissionModal({
: t("settings.plugins.permissionModal.installBody")}
- {descriptors.length === 0 ? (
-
- {t("settings.plugins.permissionModal.noPermissions")}
-
- ) : (
-
- {isUpdate && (
-
- {t("settings.plugins.permissionModal.newPermissions")}
-
- )}
- {ordinary.length > 0 && (
-
{ordinary.map(renderRow)}
- )}
- {readOnly.length > 0 && (
-
-
-
- {t("settings.plugins.permissionModal.permissions.readOnlyHeading")}
-
-
- {t("settings.plugins.permissionModal.permissions.readOnlyWarning")}
-
- {readOnly.map(renderRow)}
-
- )}
- {danger.length > 0 && (
-
-
-
- {t("settings.plugins.permissionModal.permissions.dangerHeading")}
-
-
- {t("settings.plugins.permissionModal.permissions.dangerWarning")}
-
- {danger.map(renderRow)}
-
- )}
-
- )}
+