diff --git a/playwright/e2e/integration-manager/utils.ts b/playwright/e2e/integration-manager/utils.ts
index c6a2fb998e..0ea59e6ff7 100644
--- a/playwright/e2e/integration-manager/utils.ts
+++ b/playwright/e2e/integration-manager/utils.ts
@@ -19,8 +19,6 @@ import type { ElementAppPage } from "../../pages/ElementAppPage";
 export async function openIntegrationManager(app: ElementAppPage) {
     const { page } = app;
     await app.toggleRoomInfoPanel();
-    await page
-        .locator(".mx_RoomSummaryCard_appsGroup")
-        .getByRole("button", { name: "Add widgets, bridges & bots" })
-        .click();
+    await page.getByRole("tab", { name: "Extensions" }).click();
+    await page.getByRole("button", { name: "Add extensions" }).click();
 }
diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts
index f282d83d62..f7b2958509 100644
--- a/playwright/e2e/right-panel/right-panel.spec.ts
+++ b/playwright/e2e/right-panel/right-panel.spec.ts
@@ -73,7 +73,8 @@ test.describe("RightPanel", () => {
         test("should handle clicking add widgets", async ({ page, app }) => {
             await viewRoomSummaryByName(page, app, ROOM_NAME);
 
-            await page.getByRole("button", { name: "Add widgets, bridges & bots" }).click();
+            await page.getByRole("tab", { name: "Extensions" }).click();
+            await page.getByRole("button", { name: "Add extensions" }).click();
             await expect(page.locator(".mx_IntegrationManager")).toBeVisible();
         });
 
diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png
index 2c6160f2a1..d7c90ce5fe 100644
Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ
diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png
index 943cc9dfc8..0501086f9e 100644
Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ
diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 20a6d2fe54..f1509c58e6 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -260,6 +260,7 @@
 @import "./views/right_panel/_BaseCard.pcss";
 @import "./views/right_panel/_EmptyState.pcss";
 @import "./views/right_panel/_EncryptionInfo.pcss";
+@import "./views/right_panel/_ExtensionsCard.pcss";
 @import "./views/right_panel/_PinnedMessagesCard.pcss";
 @import "./views/right_panel/_RightPanelTabs.pcss";
 @import "./views/right_panel/_RoomSummaryCard.pcss";
diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss
index 692f7d23b3..47092c124f 100644
--- a/res/css/views/right_panel/_BaseCard.pcss
+++ b/res/css/views/right_panel/_BaseCard.pcss
@@ -98,50 +98,6 @@ limitations under the License.
         scrollbar-gutter: stable;
     }
 
-    .mx_BaseCard_Group {
-        margin: $spacing-20 0 $spacing-16;
-
-        & > * {
-            margin-left: $spacing-12;
-            margin-right: $spacing-12;
-        }
-
-        > h2 {
-            color: $tertiary-content;
-            font: var(--cpd-font-body-sm-medium);
-            margin: $spacing-12;
-        }
-
-        .mx_BaseCard_Button {
-            padding: 10px;
-            padding-inline-start: $spacing-12;
-            margin: 0;
-            position: relative;
-            font: var(--cpd-font-heading-sm-medium);
-            height: 20px;
-            overflow: hidden;
-            white-space: nowrap;
-            text-overflow: ellipsis;
-            display: flex;
-
-            .mx_BaseCard_Button_sublabel {
-                color: $tertiary-content;
-                margin-left: auto;
-            }
-
-            &:hover {
-                background-color: rgba(141, 151, 165, 0.1);
-            }
-
-            &.mx_AccessibleButton_disabled {
-                padding-right: $spacing-12;
-                &::after {
-                    content: unset;
-                }
-            }
-        }
-    }
-
     .mx_BaseCard_footer {
         padding-top: $spacing-4;
         text-align: center;
diff --git a/res/css/views/right_panel/_ExtensionsCard.pcss b/res/css/views/right_panel/_ExtensionsCard.pcss
new file mode 100644
index 0000000000..ea5431fb36
--- /dev/null
+++ b/res/css/views/right_panel/_ExtensionsCard.pcss
@@ -0,0 +1,145 @@
+/*
+Copyright 2024 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_ExtensionsCard {
+    --cpd-separator-inset: var(--cpd-space-4x);
+    --cpd-separator-spacing: var(--cpd-space-4x);
+
+    .mx_BaseCard_header {
+        /* Hide the line between the header and the body of the card */
+        border-block-end: none;
+
+        /* Styling for the "Add extensions" button */
+        button {
+            width: 100%;
+        }
+    }
+
+    .mx_AutoHideScrollbar {
+        padding: 0 var(--cpd-space-4x);
+        box-sizing: border-box;
+    }
+
+    .mx_ExtensionsCard_container {
+        text-align: center;
+        margin: $spacing-20 var(--cpd-space-4x) 0;
+    }
+
+    .mx_ExtensionsCard_Button {
+        /* this button is special so we have to override some of the original styling */
+        /* as we will be applying it in its children */
+        padding: 0;
+        height: auto;
+        color: $tertiary-content;
+        position: relative;
+
+        .mx_WidgetAvatar {
+            flex-shrink: 0;
+        }
+
+        .mx_ExtensionsCard_icon_app {
+            padding: var(--cpd-space-2x) var(--cpd-space-12x) var(--cpd-space-2x) var(--cpd-space-3x);
+            text-overflow: ellipsis;
+            overflow: hidden;
+            display: flex;
+            align-items: center;
+
+            p {
+                margin: 0 var(--cpd-space-3x);
+                color: $primary-content;
+            }
+        }
+
+        .mx_ExtensionsCard_app_pinToggle,
+        .mx_ExtensionsCard_app_options {
+            position: absolute;
+            top: 0;
+            height: 100%; /* to give bigger interactive zone */
+            width: 24px;
+            padding: var(--cpd-space-3x) var(--cpd-space-1x);
+            box-sizing: border-box;
+            min-width: 24px; /* prevent flexbox crushing */
+
+            &:hover {
+                &::after {
+                    content: "";
+                    position: absolute;
+                    height: 24px;
+                    width: 24px;
+                    top: var(--cpd-space-2x); /* equal to padding-top of parent */
+                    left: 0;
+                    border-radius: 12px;
+                    background-color: rgba(141, 151, 165, 0.1);
+                }
+            }
+
+            &::before {
+                content: "";
+                position: absolute;
+                height: 16px;
+                width: 16px;
+                mask-repeat: no-repeat;
+                mask-position: center;
+                mask-size: 16px;
+                background-color: $icon-button-color;
+            }
+        }
+
+        .mx_ExtensionsCard_app_pinToggle {
+            right: 8px;
+
+            &::before {
+                mask-image: url("$(res)/img/element-icons/room/pin-upright.svg");
+            }
+        }
+
+        .mx_ExtensionsCard_app_options {
+            right: 32px; /* 24 + 8 */
+            &::before {
+                mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
+            }
+        }
+
+        &.mx_ExtensionsCard_Button_pinned {
+            &::after {
+                opacity: 0.2;
+            }
+
+            .mx_ExtensionsCard_app_pinToggle::before {
+                background-color: $accent;
+            }
+        }
+
+        &::before {
+            content: unset;
+        }
+
+        &::after {
+            top: var(--cpd-space-2x); /* re-align based on the height change */
+            pointer-events: none; /* pass through to the real button */
+        }
+    }
+
+    /* Set layout for everyone button */
+    a[data-kind="primary"] {
+        margin-top: var(--cpd-space-10x);
+    }
+
+    .mx_EmptyState::before {
+        /* Overlap the Add extensions button */
+        top: -76px;
+    }
+}
diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss
index 75f0178cdd..5c3cab320c 100644
--- a/res/css/views/right_panel/_RoomSummaryCard.pcss
+++ b/res/css/views/right_panel/_RoomSummaryCard.pcss
@@ -33,24 +33,6 @@ limitations under the License.
         text-overflow: ellipsis;
     }
 
-    .mx_RoomSummaryCard_aboutGroup {
-        .mx_RoomSummaryCard_Button {
-            padding-left: 44px;
-
-            &::before {
-                content: "";
-                position: absolute;
-                top: 8px;
-                left: 10px;
-                height: 24px;
-                width: 24px;
-                mask-repeat: no-repeat;
-                mask-position: center;
-                background-color: $icon-button-color;
-            }
-        }
-    }
-
     .mx_RoomSummaryCard_topic {
         padding: 0 12px;
         color: var(--cpd-color-text-secondary);
@@ -99,131 +81,6 @@ limitations under the License.
         }
     }
 
-    .mx_RoomSummaryCard_appsGroup {
-        .mx_RoomSummaryCard_Button {
-            /* this button is special so we have to override some of the original styling */
-            /* as we will be applying it in its children */
-            padding: 0;
-            height: auto;
-            color: $tertiary-content;
-
-            .mx_RoomSummaryCard_icon_app {
-                padding: 10px 48px 10px 12px; /* based on typical mx_RoomSummaryCard_Button padding */
-                text-overflow: ellipsis;
-                overflow: hidden;
-                display: flex;
-                justify-content: center;
-                span {
-                    /* Center aligned and Spacing matched with the About section above the Widgets section */
-                    margin-right: 10px;
-                    display: flex;
-                    justify-content: center;
-                    align-items: center;
-                    color: $primary-content;
-                }
-            }
-
-            .mx_RoomSummaryCard_app_pinToggle,
-            .mx_RoomSummaryCard_app_maximiseToggle,
-            .mx_RoomSummaryCard_app_options {
-                position: absolute;
-                top: 0;
-                height: 100%; /* to give bigger interactive zone */
-                width: 24px;
-                padding: 12px 4px;
-                box-sizing: border-box;
-                min-width: 24px; /* prevent flexbox crushing */
-
-                &:hover {
-                    &::after {
-                        content: "";
-                        position: absolute;
-                        height: 24px;
-                        width: 24px;
-                        top: 8px; /* equal to padding-top of parent */
-                        left: 0;
-                        border-radius: 12px;
-                        background-color: rgba(141, 151, 165, 0.1);
-                    }
-                }
-
-                &::before {
-                    content: "";
-                    position: absolute;
-                    height: 16px;
-                    width: 16px;
-                    mask-repeat: no-repeat;
-                    mask-position: center;
-                    mask-size: 16px;
-                    background-color: $icon-button-color;
-                }
-            }
-
-            .mx_RoomSummaryCard_app_pinToggle {
-                right: 8px;
-
-                &::before {
-                    mask-image: url("$(res)/img/element-icons/room/pin-upright.svg");
-                }
-            }
-            .mx_RoomSummaryCard_app_maximiseToggle {
-                right: 32px; /* 24 + 8 */
-
-                &::before {
-                    mask-size: 14px;
-                    mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
-                }
-            }
-
-            .mx_RoomSummaryCard_app_options {
-                right: 56px; /* 2*24 + 8 */
-                display: none;
-                &::before {
-                    mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
-                }
-            }
-
-            &.mx_RoomSummaryCard_Button_pinned {
-                &::after {
-                    opacity: 0.2;
-                }
-
-                .mx_RoomSummaryCard_app_pinToggle::before {
-                    background-color: $accent;
-                }
-            }
-
-            &.mx_RoomSummaryCard_Button_maximised {
-                &::after {
-                    opacity: 0.2;
-                }
-
-                .mx_RoomSummaryCard_app_maximiseToggle::before {
-                    background-color: $accent;
-                }
-            }
-
-            &:hover {
-                .mx_RoomSummaryCard_icon_app {
-                    padding-right: 72px;
-                }
-
-                .mx_RoomSummaryCard_app_options {
-                    display: unset;
-                }
-            }
-
-            &::before {
-                content: unset;
-            }
-
-            &::after {
-                top: 8px; /* re-align based on the height change */
-                pointer-events: none; /* pass through to the real button */
-            }
-        }
-    }
-
     .mx_AccessibleButton_kind_link {
         margin-top: 12px;
         margin-bottom: 12px;
diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss
index a306e769b0..213c641440 100644
--- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss
+++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss
@@ -64,14 +64,6 @@ $accent-1400: var(--cpd-color-green-1400);
     outline-offset: 2px;
 }
 
-/* Add padding, so the outline is not chopped off on the left */
-.mx_BaseCard {
-    padding-left: 4px !important; /* Remove 4 to allow 4 in mx_BaseCard_Group */
-}
-.mx_BaseCard_Group {
-    padding-left: 4px !important;
-}
-
 .mx_BasicMessageComposer .mx_BasicMessageComposer_inputEmpty > :first-child::before {
     color: $secondary-content;
     opacity: 1 !important;
diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx
index bc80692459..1d6bf101b6 100644
--- a/src/components/structures/RightPanel.tsx
+++ b/src/components/structures/RightPanel.tsx
@@ -43,6 +43,7 @@ import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/
 import { Action } from "../../dispatcher/actions";
 import { XOR } from "../../@types/common";
 import { RightPanelTabs } from "../views/right_panel/RightPanelTabs";
+import ExtensionsCard from "../views/right_panel/ExtensionsCard";
 
 interface BaseProps {
     overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
@@ -306,6 +307,12 @@ export default class RightPanel extends React.Component<Props, IState> {
                 }
                 break;
 
+            case RightPanelPhases.Extensions:
+                if (!!this.props.room) {
+                    card = <ExtensionsCard room={this.props.room} onClose={this.onClose} />;
+                }
+                break;
+
             case RightPanelPhases.Widget:
                 if (!!this.props.room && !!cardState?.widgetId) {
                     card = <WidgetCard room={this.props.room} widgetId={cardState.widgetId} onClose={this.onClose} />;
@@ -315,7 +322,7 @@ export default class RightPanel extends React.Component<Props, IState> {
 
         return (
             <aside className="mx_RightPanel" id="mx_RightPanel">
-                {phase && <RightPanelTabs phase={phase} />}
+                {phase && <RightPanelTabs room={this.props.room} phase={phase} />}
                 {card}
             </aside>
         );
diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx
index fc7b1bcf69..8443da220e 100644
--- a/src/components/views/right_panel/BaseCard.tsx
+++ b/src/components/views/right_panel/BaseCard.tsx
@@ -41,26 +41,11 @@ interface IProps {
     onKeyDown?(ev: KeyboardEvent): void;
     cardState?: any;
     ref?: Ref<HTMLDivElement>;
-    // Ref for the 'close' button the the card
+    // Ref for the 'close' button the card
     closeButtonRef?: Ref<HTMLButtonElement>;
     children: ReactNode;
 }
 
-interface IGroupProps {
-    className?: string;
-    title: string;
-    children: ReactNode;
-}
-
-export const Group: React.FC<IGroupProps> = ({ className, title, children }) => {
-    return (
-        <div className={classNames("mx_BaseCard_Group", className)}>
-            <h2>{title}</h2>
-            {children}
-        </div>
-    );
-};
-
 const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
     (
         {
diff --git a/src/components/views/right_panel/ExtensionsCard.tsx b/src/components/views/right_panel/ExtensionsCard.tsx
new file mode 100644
index 0000000000..22ea8bad99
--- /dev/null
+++ b/src/components/views/right_panel/ExtensionsCard.tsx
@@ -0,0 +1,214 @@
+/*
+Copyright 2024 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useEffect, useMemo, useState } from "react";
+import { Room } from "matrix-js-sdk/src/matrix";
+import classNames from "classnames";
+import { Button, Link, Separator, Text } from "@vector-im/compound-web";
+import { Icon as PlusIcon } from "@vector-im/compound-design-tokens/icons/plus.svg";
+import { Icon as ExtensionsIcon } from "@vector-im/compound-design-tokens/icons/extensions.svg";
+
+import BaseCard from "./BaseCard";
+import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
+import { _t } from "../../../languageHandler";
+import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
+import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
+import UIStore from "../../../stores/UIStore";
+import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
+import { IApp } from "../../../stores/WidgetStore";
+import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
+import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
+import AccessibleButton from "../elements/AccessibleButton";
+import WidgetAvatar from "../avatars/WidgetAvatar";
+import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
+import EmptyState from "./EmptyState";
+
+interface Props {
+    room: Room;
+    onClose(): void;
+}
+
+interface IAppRowProps {
+    app: IApp;
+    room: Room;
+}
+
+const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
+    const name = WidgetUtils.getWidgetName(app);
+    const [canModifyWidget, setCanModifyWidget] = useState<boolean>();
+
+    useEffect(() => {
+        setCanModifyWidget(WidgetUtils.canUserModifyWidgets(room.client, room.roomId));
+    }, [room.client, room.roomId]);
+
+    const onOpenWidgetClick = (): void => {
+        RightPanelStore.instance.pushCard({
+            phase: RightPanelPhases.Widget,
+            state: { widgetId: app.id },
+        });
+    };
+
+    const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
+    const togglePin = isPinned
+        ? () => {
+              WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
+          }
+        : () => {
+              WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
+          };
+
+    const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
+    let contextMenu;
+    if (menuDisplayed) {
+        const rect = handle.current?.getBoundingClientRect();
+        const rightMargin = rect?.right ?? 0;
+        const topMargin = rect?.top ?? 0;
+        contextMenu = (
+            <WidgetContextMenu
+                chevronFace={ChevronFace.None}
+                right={UIStore.instance.windowWidth - rightMargin}
+                bottom={UIStore.instance.windowHeight - topMargin}
+                onFinished={closeMenu}
+                app={app}
+            />
+        );
+    }
+
+    const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
+
+    let pinTitle: string;
+    if (cannotPin) {
+        pinTitle = _t("right_panel|pinned_messages|limits", { count: MAX_PINNED });
+    } else {
+        pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
+    }
+
+    const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
+
+    let openTitle = "";
+    if (isPinned) {
+        openTitle = _t("widget|unpin_to_view_right_panel");
+    } else if (isMaximised) {
+        openTitle = _t("widget|close_to_view_right_panel");
+    }
+
+    const classes = classNames("mx_BaseCard_Button mx_ExtensionsCard_Button", {
+        mx_ExtensionsCard_Button_pinned: isPinned,
+    });
+
+    return (
+        <div className={classes} ref={handle}>
+            <AccessibleButton
+                className="mx_ExtensionsCard_icon_app"
+                onClick={onOpenWidgetClick}
+                // only show a tooltip if the widget is pinned
+                title={!(isPinned || isMaximised) ? undefined : openTitle}
+                disabled={isPinned || isMaximised}
+            >
+                <WidgetAvatar app={app} size="24px" />
+                <Text size="md" weight="medium" className="mx_lineClamp">
+                    {name}
+                </Text>
+            </AccessibleButton>
+
+            {canModifyWidget && (
+                <ContextMenuTooltipButton
+                    className="mx_ExtensionsCard_app_options"
+                    isExpanded={menuDisplayed}
+                    onClick={openMenu}
+                    title={_t("common|options")}
+                />
+            )}
+
+            <AccessibleButton
+                className="mx_ExtensionsCard_app_pinToggle"
+                onClick={togglePin}
+                title={pinTitle}
+                disabled={cannotPin}
+            />
+
+            {contextMenu}
+        </div>
+    );
+};
+
+/**
+ * A right panel card displaying a list of widgets in the room and allowing the user to manage them.
+ * @param room the room to manage widgets for
+ * @param onClose callback when the card is closed
+ */
+const ExtensionsCard: React.FC<Props> = ({ room, onClose }) => {
+    const apps = useWidgets(room);
+    // Filter out virtual widgets
+    const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]);
+
+    const onManageIntegrations = (): void => {
+        const managers = IntegrationManagers.sharedInstance();
+        if (!managers.hasManager()) {
+            managers.openNoManagerDialog();
+        } else {
+            // noinspection JSIgnoredPromiseFromCall
+            managers.getPrimaryManager()?.open(room);
+        }
+    };
+
+    // The button is in the header to keep it outside the scrollable region
+    const header = (
+        <Button size="sm" onClick={onManageIntegrations} kind="secondary" Icon={PlusIcon}>
+            {_t("right_panel|add_integrations")}
+        </Button>
+    );
+
+    let body: JSX.Element;
+    if (realApps.length < 1) {
+        body = (
+            <EmptyState
+                Icon={ExtensionsIcon}
+                title={_t("right_panel|extensions_empty_title")}
+                description={_t("right_panel|extensions_empty_description", {
+                    addIntegrations: _t("right_panel|add_integrations"),
+                })}
+            />
+        );
+    } else {
+        let copyLayoutBtn: JSX.Element | null = null;
+        if (WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
+            copyLayoutBtn = (
+                <Link onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
+                    {_t("widget|set_room_layout")}
+                </Link>
+            );
+        }
+
+        body = (
+            <>
+                <Separator />
+                {realApps.map((app) => (
+                    <AppRow key={app.id} app={app} room={room} />
+                ))}
+                {copyLayoutBtn}
+            </>
+        );
+    }
+
+    return (
+        <BaseCard header={header} className="mx_ExtensionsCard" onClose={onClose} hideHeaderButtons>
+            {body}
+        </BaseCard>
+    );
+};
+
+export default ExtensionsCard;
diff --git a/src/components/views/right_panel/RightPanelTabs.tsx b/src/components/views/right_panel/RightPanelTabs.tsx
index fc2eeb17fa..300856e28f 100644
--- a/src/components/views/right_panel/RightPanelTabs.tsx
+++ b/src/components/views/right_panel/RightPanelTabs.tsx
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React, { useRef } from "react";
 import { NavBar, NavItem } from "@vector-im/compound-web";
+import { Room } from "matrix-js-sdk/src/matrix";
 
 import { _t } from "../../../languageHandler";
 import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
@@ -24,17 +25,27 @@ import PosthogTrackers from "../../../PosthogTrackers";
 import { useDispatcher } from "../../../hooks/useDispatcher";
 import dispatcher from "../../../dispatcher/dispatcher";
 import { Action } from "../../../dispatcher/actions";
+import SettingsStore from "../../../settings/SettingsStore";
+import { UIComponent, UIFeature } from "../../../settings/UIFeature";
+import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
+import { useIsVideoRoom } from "../../../utils/video-rooms";
 
 function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean {
-    const tabs = [RightPanelPhases.RoomSummary, RightPanelPhases.RoomMemberList, RightPanelPhases.ThreadPanel];
+    const tabs = [
+        RightPanelPhases.RoomSummary,
+        RightPanelPhases.RoomMemberList,
+        RightPanelPhases.ThreadPanel,
+        RightPanelPhases.Extensions,
+    ];
     return !!phase && tabs.includes(phase);
 }
 
 type Props = {
+    room?: Room;
     phase: RightPanelPhases;
 };
 
-export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null => {
+export const RightPanelTabs: React.FC<Props> = ({ phase, room }): JSX.Element | null => {
     const threadsTabRef = useRef<HTMLButtonElement | null>(null);
 
     useDispatcher(dispatcher, (payload) => {
@@ -45,6 +56,8 @@ export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null =
         }
     });
 
+    const isVideoRoom = useIsVideoRoom(room);
+
     if (!shouldShowTabsForPhase(phase)) return null;
 
     return (
@@ -81,6 +94,20 @@ export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null =
             >
                 {_t("common|threads")}
             </NavItem>
+            {SettingsStore.getValue(UIFeature.Widgets) &&
+                !isVideoRoom &&
+                shouldShowComponent(UIComponent.AddIntegrations) && (
+                    <NavItem
+                        aria-controls="thread-panel"
+                        id="extensions-panel-tab"
+                        onClick={() => {
+                            RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true);
+                        }}
+                        active={phase === RightPanelPhases.Extensions}
+                    >
+                        {_t("common|extensions")}
+                    </NavItem>
+                )}
         </NavBar>
     );
 };
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 9896289e25..bb35242bc7 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -14,16 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {
-    ChangeEvent,
-    SyntheticEvent,
-    useCallback,
-    useContext,
-    useEffect,
-    useMemo,
-    useRef,
-    useState,
-} from "react";
+import React, { ChangeEvent, SyntheticEvent, useContext, useEffect, useRef, useState } from "react";
 import classNames from "classnames";
 import {
     MenuItem,
@@ -55,35 +46,23 @@ import { EventType, JoinRule, Room, RoomStateEvent } from "matrix-js-sdk/src/mat
 
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
-import BaseCard, { Group } from "./BaseCard";
+import BaseCard from "./BaseCard";
 import { _t } from "../../../languageHandler";
 import RoomAvatar from "../avatars/RoomAvatar";
-import AccessibleButton from "../elements/AccessibleButton";
 import defaultDispatcher from "../../../dispatcher/dispatcher";
 import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
 import Modal from "../../../Modal";
 import ShareDialog from "../dialogs/ShareDialog";
-import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
-import WidgetUtils from "../../../utils/WidgetUtils";
-import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
-import SettingsStore from "../../../settings/SettingsStore";
-import WidgetAvatar from "../avatars/WidgetAvatar";
-import WidgetStore, { IApp } from "../../../stores/WidgetStore";
+import { useEventEmitterState } from "../../../hooks/useEventEmitter";
 import { E2EStatus } from "../../../utils/ShieldUtils";
 import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
 import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
-import { UIComponent, UIFeature } from "../../../settings/UIFeature";
-import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
-import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
 import { useFeatureEnabled } from "../../../hooks/useSettings";
 import { usePinnedEvents } from "./PinnedMessagesCard";
-import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
 import RoomName from "../elements/RoomName";
-import UIStore from "../../../stores/UIStore";
 import ExportDialog from "../dialogs/ExportDialog";
 import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
 import PosthogTrackers from "../../../PosthogTrackers";
-import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
 import { PollHistoryDialog } from "../dialogs/PollHistoryDialog";
 import { Flex } from "../../utils/Flex";
 import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
@@ -111,182 +90,6 @@ interface IProps {
     focusRoomSearch?: boolean;
 }
 
-interface IAppsSectionProps {
-    room: Room;
-}
-
-export const useWidgets = (room: Room): IApp[] => {
-    const [apps, setApps] = useState<IApp[]>(() => WidgetStore.instance.getApps(room.roomId));
-
-    const updateApps = useCallback(() => {
-        // Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
-        setApps([...WidgetStore.instance.getApps(room.roomId)]);
-    }, [room]);
-
-    useEffect(updateApps, [room, updateApps]);
-    useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
-    useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
-
-    return apps;
-};
-
-interface IAppRowProps {
-    app: IApp;
-    room: Room;
-}
-
-const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
-    const name = WidgetUtils.getWidgetName(app);
-    const dataTitle = WidgetUtils.getWidgetDataTitle(app);
-    const subtitle = dataTitle && " - " + dataTitle;
-    const [canModifyWidget, setCanModifyWidget] = useState<boolean>();
-
-    useEffect(() => {
-        setCanModifyWidget(WidgetUtils.canUserModifyWidgets(room.client, room.roomId));
-    }, [room.client, room.roomId]);
-
-    const onOpenWidgetClick = (): void => {
-        RightPanelStore.instance.pushCard({
-            phase: RightPanelPhases.Widget,
-            state: { widgetId: app.id },
-        });
-    };
-
-    const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
-    const togglePin = isPinned
-        ? () => {
-              WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
-          }
-        : () => {
-              WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
-          };
-
-    const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
-    let contextMenu;
-    if (menuDisplayed) {
-        const rect = handle.current?.getBoundingClientRect();
-        const rightMargin = rect?.right ?? 0;
-        const topMargin = rect?.top ?? 0;
-        contextMenu = (
-            <WidgetContextMenu
-                chevronFace={ChevronFace.None}
-                right={UIStore.instance.windowWidth - rightMargin}
-                bottom={UIStore.instance.windowHeight - topMargin}
-                onFinished={closeMenu}
-                app={app}
-            />
-        );
-    }
-
-    const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
-
-    let pinTitle: string;
-    if (cannotPin) {
-        pinTitle = _t("right_panel|pinned_messages|limits", { count: MAX_PINNED });
-    } else {
-        pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
-    }
-
-    const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
-    const toggleMaximised = isMaximised
-        ? () => {
-              WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
-          }
-        : () => {
-              WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
-          };
-
-    const maximiseTitle = isMaximised ? _t("action|close") : _t("action|maximise");
-
-    let openTitle = "";
-    if (isPinned) {
-        openTitle = _t("widget|unpin_to_view_right_panel");
-    } else if (isMaximised) {
-        openTitle = _t("widget|close_to_view_right_panel");
-    }
-
-    const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
-        mx_RoomSummaryCard_Button_pinned: isPinned,
-        mx_RoomSummaryCard_Button_maximised: isMaximised,
-    });
-
-    return (
-        <div className={classes} ref={handle}>
-            <AccessibleButton
-                className="mx_RoomSummaryCard_icon_app"
-                onClick={onOpenWidgetClick}
-                // only show a tooltip if the widget is pinned
-                title={!(isPinned || isMaximised) ? undefined : openTitle}
-                disabled={isPinned || isMaximised}
-            >
-                <WidgetAvatar app={app} size="20px" />
-                <span>{name}</span>
-                {subtitle}
-            </AccessibleButton>
-
-            {canModifyWidget && (
-                <ContextMenuTooltipButton
-                    className="mx_RoomSummaryCard_app_options"
-                    isExpanded={menuDisplayed}
-                    onClick={openMenu}
-                    title={_t("common|options")}
-                />
-            )}
-
-            <AccessibleButton
-                className="mx_RoomSummaryCard_app_pinToggle"
-                onClick={togglePin}
-                title={pinTitle}
-                disabled={cannotPin}
-            />
-            <AccessibleButton
-                className="mx_RoomSummaryCard_app_maximiseToggle"
-                onClick={toggleMaximised}
-                title={maximiseTitle}
-            />
-
-            {contextMenu}
-        </div>
-    );
-};
-
-const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
-    const apps = useWidgets(room);
-    // Filter out virtual widgets
-    const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]);
-
-    const onManageIntegrations = (): void => {
-        const managers = IntegrationManagers.sharedInstance();
-        if (!managers.hasManager()) {
-            managers.openNoManagerDialog();
-        } else {
-            // noinspection JSIgnoredPromiseFromCall
-            managers.getPrimaryManager()?.open(room);
-        }
-    };
-
-    let copyLayoutBtn: JSX.Element | null = null;
-    if (realApps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
-        copyLayoutBtn = (
-            <AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
-                {_t("widget|set_room_layout")}
-            </AccessibleButton>
-        );
-    }
-
-    return (
-        <Group className="mx_RoomSummaryCard_appsGroup" title={_t("right_panel|widgets_section")}>
-            {realApps.map((app) => (
-                <AppRow key={app.id} app={app} room={room} />
-            ))}
-            {copyLayoutBtn}
-            <AccessibleButton kind="link" onClick={onManageIntegrations}>
-                {realApps.length > 0 ? _t("right_panel|edit_integrations") : _t("right_panel|add_integrations")}
-            </AccessibleButton>
-        </Group>
-    );
-};
-
 const onRoomFilesClick = (): void => {
     RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
 };
@@ -622,10 +425,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
                     onSelect={onLeaveRoomClick}
                 />
             </div>
-
-            {SettingsStore.getValue(UIFeature.Widgets) &&
-                !isVideoRoom &&
-                shouldShowComponent(UIComponent.AddIntegrations) && <AppsSection room={room} />}
         </BaseCard>
     );
 };
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index ca7cdebb21..210e673556 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -19,10 +19,9 @@ import { Room } from "matrix-js-sdk/src/matrix";
 
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import BaseCard from "./BaseCard";
-import WidgetUtils from "../../../utils/WidgetUtils";
+import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
 import AppTile from "../elements/AppTile";
 import { _t } from "../../../languageHandler";
-import { useWidgets } from "./RoomSummaryCard";
 import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
 import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
 import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx
index c6fa28fc7c..b3f76e980f 100644
--- a/src/components/views/rooms/LegacyRoomHeader.tsx
+++ b/src/components/views/rooms/LegacyRoomHeader.tsx
@@ -54,7 +54,7 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHa
 import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
 import SdkConfig from "../../../SdkConfig";
 import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
-import { useWidgets } from "../right_panel/RoomSummaryCard";
+import { useWidgets } from "../../../utils/WidgetUtils";
 import { WidgetType } from "../../../widgets/WidgetType";
 import { useCall, useLayout } from "../../../hooks/useCall";
 import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts
index 8dc18040a1..65be125bf3 100644
--- a/src/hooks/room/useRoomCall.ts
+++ b/src/hooks/room/useRoomCall.ts
@@ -22,7 +22,7 @@ import { useFeatureEnabled } from "../useSettings";
 import SdkConfig from "../../SdkConfig";
 import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
 import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
-import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard";
+import { useWidgets } from "../../utils/WidgetUtils";
 import { WidgetType } from "../../widgets/WidgetType";
 import { useCall, useConnectionState, useParticipantCount } from "../useCall";
 import { useRoomMemberCount } from "../useRoomMembers";
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a542fc7c60..a17b04a9f6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -474,6 +474,7 @@
         "encrypted": "Encrypted",
         "encryption_enabled": "Encryption enabled",
         "error": "Error",
+        "extensions": "Extensions",
         "faq": "FAQ",
         "favourites": "Favourites",
         "feedback": "Feedback",
@@ -1830,10 +1831,11 @@
         "restore_failed_error": "Unable to restore backup"
     },
     "right_panel": {
-        "add_integrations": "Add widgets, bridges & bots",
+        "add_integrations": "Add extensions",
         "add_topic": "Add topic",
-        "edit_integrations": "Edit widgets, bridges & bots",
         "export_chat_button": "Export chat",
+        "extensions_empty_description": "Select “%(addIntegrations)s” to browse and add extensions to this room",
+        "extensions_empty_title": "Boost productivity with more tools, widgets and bots",
         "files_button": "Files",
         "info": "Info",
         "pinned_messages": {
@@ -4067,7 +4069,7 @@
             "title": "Allow this widget to verify your identity"
         },
         "popout": "Popout widget",
-        "set_room_layout": "Set my room layout for everyone",
+        "set_room_layout": "Set layout for everyone",
         "shared_data_avatar": "Your profile picture URL",
         "shared_data_device_id": "Your device ID",
         "shared_data_lang": "Your language",
diff --git a/src/stores/right-panel/RightPanelStorePhases.ts b/src/stores/right-panel/RightPanelStorePhases.ts
index 2353d3fe43..4cae676830 100644
--- a/src/stores/right-panel/RightPanelStorePhases.ts
+++ b/src/stores/right-panel/RightPanelStorePhases.ts
@@ -28,6 +28,7 @@ export enum RightPanelPhases {
     Widget = "Widget",
     PinnedMessages = "PinnedMessages",
     Timeline = "Timeline",
+    Extensions = "Extensions",
 
     Room3pidMemberInfo = "Room3pidMemberInfo",
 
diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts
index 3272a14e4e..40712f633a 100644
--- a/src/utils/WidgetUtils.ts
+++ b/src/utils/WidgetUtils.ts
@@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import { useCallback, useEffect, useState } from "react";
 import { base32 } from "rfc4648";
 import { IWidget, IWidgetData } from "matrix-widget-api";
 import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
@@ -32,8 +33,10 @@ import { WidgetType } from "../widgets/WidgetType";
 import { Jitsi } from "../widgets/Jitsi";
 import { objectClone } from "./objects";
 import { _t } from "../languageHandler";
-import { IApp, isAppWidget } from "../stores/WidgetStore";
+import WidgetStore, { IApp, isAppWidget } from "../stores/WidgetStore";
 import { parseUrl } from "./UrlUtils";
+import { useEventEmitter } from "../hooks/useEventEmitter";
+import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
 
 // How long we wait for the state event echo to come back from the server
 // before waitFor[Room/User]Widget rejects its promise
@@ -562,3 +565,22 @@ export default class WidgetUtils {
         return false;
     }
 }
+
+/**
+ * Hook to get the widgets for a room and update when they change
+ * @param room the room to get widgets for
+ */
+export const useWidgets = (room: Room): IApp[] => {
+    const [apps, setApps] = useState<IApp[]>(() => WidgetStore.instance.getApps(room.roomId));
+
+    const updateApps = useCallback(() => {
+        // Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
+        setApps([...WidgetStore.instance.getApps(room.roomId)]);
+    }, [room]);
+
+    useEffect(updateApps, [room, updateApps]);
+    useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
+    useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
+
+    return apps;
+};
diff --git a/test/components/views/right_panel/ExtensionsCard-test.tsx b/test/components/views/right_panel/ExtensionsCard-test.tsx
new file mode 100644
index 0000000000..b34db13b7a
--- /dev/null
+++ b/test/components/views/right_panel/ExtensionsCard-test.tsx
@@ -0,0 +1,159 @@
+/*
+Copyright 2024 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { mocked, Mocked } from "jest-mock";
+import { render, screen } from "@testing-library/react";
+import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
+import { MatrixWidgetType } from "matrix-widget-api";
+import userEvent from "@testing-library/user-event";
+
+import ExtensionsCard from "../../../../src/components/views/right_panel/ExtensionsCard";
+import { stubClient } from "../../../test-utils";
+import { IApp } from "../../../../src/stores/WidgetStore";
+import WidgetUtils, { useWidgets } from "../../../../src/utils/WidgetUtils";
+import { WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
+import { IntegrationManagers } from "../../../../src/integrations/IntegrationManagers";
+
+jest.mock("../../../../src/utils/WidgetUtils");
+
+describe("<ExtensionsCard />", () => {
+    let client: Mocked<MatrixClient>;
+    let room: Room;
+
+    beforeEach(() => {
+        client = mocked(stubClient());
+        room = new Room("!room:server", client, client.getSafeUserId());
+        mocked(WidgetUtils.getWidgetName).mockImplementation((app) => app?.name ?? "No Name");
+    });
+
+    it("should render empty state", () => {
+        mocked(useWidgets).mockReturnValue([]);
+        const { asFragment } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
+        expect(screen.getByText("Boost productivity with more tools, widgets and bots")).toBeInTheDocument();
+        expect(asFragment()).toMatchSnapshot();
+    });
+
+    it("should render widgets", async () => {
+        mocked(useWidgets).mockReturnValue([
+            {
+                id: "id",
+                roomId: room.roomId,
+                eventId: "$event1",
+                creatorUserId: client.getSafeUserId(),
+                type: MatrixWidgetType.Custom,
+                name: "Custom Widget",
+                url: "http://url1",
+            },
+            {
+                id: "jitsi",
+                roomId: room.roomId,
+                eventId: "$event2",
+                creatorUserId: client.getSafeUserId(),
+                type: MatrixWidgetType.JitsiMeet,
+                name: "Jitsi",
+                url: "http://jitsi",
+            },
+        ] satisfies IApp[]);
+
+        const { asFragment } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
+        expect(screen.getByText("Custom Widget")).toBeInTheDocument();
+        expect(screen.getByText("Jitsi")).toBeInTheDocument();
+        expect(asFragment()).toMatchSnapshot();
+    });
+
+    it("should show context menu on widget row", async () => {
+        jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
+        mocked(useWidgets).mockReturnValue([
+            {
+                id: "id",
+                roomId: room.roomId,
+                eventId: "$event1",
+                creatorUserId: client.getSafeUserId(),
+                type: MatrixWidgetType.Custom,
+                name: "Custom Widget",
+                url: "http://url1",
+            },
+        ] satisfies IApp[]);
+
+        const { container } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
+        await userEvent.click(container.querySelector(".mx_ExtensionsCard_app_options")!);
+        expect(document.querySelector(".mx_IconizedContextMenu")).toMatchSnapshot();
+    });
+
+    it("should show set room layout button", async () => {
+        jest.spyOn(WidgetLayoutStore.instance, "canCopyLayoutToRoom").mockReturnValue(true);
+        mocked(useWidgets).mockReturnValue([
+            {
+                id: "id",
+                roomId: room.roomId,
+                eventId: "$event1",
+                creatorUserId: client.getSafeUserId(),
+                type: MatrixWidgetType.Custom,
+                name: "Custom Widget",
+                url: "http://url1",
+            },
+        ] satisfies IApp[]);
+
+        render(<ExtensionsCard room={room} onClose={jest.fn()} />);
+        expect(screen.getByText("Set layout for everyone")).toBeInTheDocument();
+    });
+
+    it("should show widget as pinned", async () => {
+        jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
+        mocked(useWidgets).mockReturnValue([
+            {
+                id: "id",
+                roomId: room.roomId,
+                eventId: "$event1",
+                creatorUserId: client.getSafeUserId(),
+                type: MatrixWidgetType.Custom,
+                name: "Custom Widget",
+                url: "http://url1",
+            },
+        ] satisfies IApp[]);
+
+        render(<ExtensionsCard room={room} onClose={jest.fn()} />);
+        expect(screen.getByText("Custom Widget").closest(".mx_ExtensionsCard_Button_pinned")).toBeInTheDocument();
+    });
+
+    it("should show cannot pin warning", async () => {
+        jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
+        jest.spyOn(WidgetLayoutStore.instance, "canAddToContainer").mockReturnValue(false);
+        mocked(useWidgets).mockReturnValue([
+            {
+                id: "id",
+                roomId: room.roomId,
+                eventId: "$event1",
+                creatorUserId: client.getSafeUserId(),
+                type: MatrixWidgetType.Custom,
+                name: "Custom Widget",
+                url: "http://url1",
+            },
+        ] satisfies IApp[]);
+
+        render(<ExtensionsCard room={room} onClose={jest.fn()} />);
+        expect(screen.getByLabelText("You can only pin up to 3 widgets")).toBeInTheDocument();
+    });
+
+    it("should should open integration manager on click", async () => {
+        jest.spyOn(IntegrationManagers.sharedInstance(), "hasManager").mockReturnValue(false);
+        const spy = jest.spyOn(IntegrationManagers.sharedInstance(), "openNoManagerDialog");
+        render(<ExtensionsCard room={room} onClose={jest.fn()} />);
+        await userEvent.click(screen.getByText("Add extensions"));
+        expect(spy).toHaveBeenCalled();
+    });
+});
diff --git a/test/components/views/right_panel/RightPanelTabs-test.tsx b/test/components/views/right_panel/RightPanelTabs-test.tsx
index dae7b1a79a..4f702a46ae 100644
--- a/test/components/views/right_panel/RightPanelTabs-test.tsx
+++ b/test/components/views/right_panel/RightPanelTabs-test.tsx
@@ -38,8 +38,8 @@ describe("<RightPanelTabs />", () => {
         const { container } = render(<RightPanelTabs phase={RightPanelPhases.RoomMemberList} />);
         expect(container).toMatchSnapshot();
         // Assert that the active tab is Info
-        expect(container.querySelectorAll("[aria-selected='true'").length).toEqual(1);
-        expect(container.querySelector("[aria-selected='true'")).toHaveAccessibleName("People");
+        expect(container.querySelectorAll("[aria-selected='true']").length).toEqual(1);
+        expect(container.querySelector("[aria-selected='true']")).toHaveAccessibleName("People");
     });
 
     it("Renders nothing for some phases, eg: FilePanel", () => {
diff --git a/test/components/views/right_panel/__snapshots__/ExtensionsCard-test.tsx.snap b/test/components/views/right_panel/__snapshots__/ExtensionsCard-test.tsx.snap
new file mode 100644
index 0000000000..2cff7803ac
--- /dev/null
+++ b/test/components/views/right_panel/__snapshots__/ExtensionsCard-test.tsx.snap
@@ -0,0 +1,194 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<ExtensionsCard /> should render empty state 1`] = `
+<DocumentFragment>
+  <div
+    class="mx_BaseCard mx_ExtensionsCard"
+  >
+    <div
+      class="mx_BaseCard_header"
+    >
+      <button
+        class="_button_zt6rp_17 _has-icon_zt6rp_61"
+        data-kind="secondary"
+        data-size="sm"
+        role="button"
+        tabindex="0"
+      >
+        <div
+          aria-hidden="true"
+          height="20"
+          width="20"
+        />
+        Add extensions
+      </button>
+    </div>
+    <div
+      class="mx_AutoHideScrollbar"
+      tabindex="-1"
+    >
+      <div
+        class="mx_Flex mx_EmptyState"
+        style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
+      >
+        <div
+          height="32px"
+          width="32px"
+        />
+        <p
+          class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83"
+        >
+          Boost productivity with more tools, widgets and bots
+        </p>
+        <p
+          class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
+        >
+          Select “Add extensions” to browse and add extensions to this room
+        </p>
+      </div>
+    </div>
+  </div>
+</DocumentFragment>
+`;
+
+exports[`<ExtensionsCard /> should render widgets 1`] = `
+<DocumentFragment>
+  <div
+    class="mx_BaseCard mx_ExtensionsCard"
+  >
+    <div
+      class="mx_BaseCard_header"
+    >
+      <button
+        class="_button_zt6rp_17 _has-icon_zt6rp_61"
+        data-kind="secondary"
+        data-size="sm"
+        role="button"
+        tabindex="0"
+      >
+        <div
+          aria-hidden="true"
+          height="20"
+          width="20"
+        />
+        Add extensions
+      </button>
+    </div>
+    <div
+      class="mx_AutoHideScrollbar"
+      tabindex="-1"
+    >
+      <div
+        class="_separator_144s5_17"
+        data-kind="primary"
+        data-orientation="horizontal"
+        role="separator"
+      />
+      <div
+        class="mx_BaseCard_Button mx_ExtensionsCard_Button"
+      >
+        <div
+          class="mx_AccessibleButton mx_ExtensionsCard_icon_app"
+          role="button"
+          tabindex="0"
+        >
+          <span
+            aria-label="Avatar"
+            class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
+            data-color="1"
+            data-testid="avatar-img"
+            data-type="round"
+            style="--cpd-avatar-size: 24px;"
+          >
+            <img
+              alt=""
+              class="_image_mcap2_50"
+              data-type="round"
+              height="24px"
+              loading="lazy"
+              referrerpolicy="no-referrer"
+              src="image-file-stub"
+              width="24px"
+            />
+          </span>
+          <p
+            class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 mx_lineClamp"
+          >
+            Custom Widget
+          </p>
+        </div>
+        <div
+          aria-label="Pin"
+          class="mx_AccessibleButton mx_ExtensionsCard_app_pinToggle"
+          role="button"
+          tabindex="0"
+        />
+      </div>
+      <div
+        class="mx_BaseCard_Button mx_ExtensionsCard_Button"
+      >
+        <div
+          class="mx_AccessibleButton mx_ExtensionsCard_icon_app"
+          role="button"
+          tabindex="0"
+        >
+          <span
+            aria-label="Avatar"
+            class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
+            data-color="1"
+            data-testid="avatar-img"
+            data-type="round"
+            style="--cpd-avatar-size: 24px;"
+          >
+            <img
+              alt=""
+              class="_image_mcap2_50"
+              data-type="round"
+              height="24px"
+              loading="lazy"
+              referrerpolicy="no-referrer"
+              src="image-file-stub"
+              width="24px"
+            />
+          </span>
+          <p
+            class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 mx_lineClamp"
+          >
+            Jitsi
+          </p>
+        </div>
+        <div
+          aria-label="Pin"
+          class="mx_AccessibleButton mx_ExtensionsCard_app_pinToggle"
+          role="button"
+          tabindex="0"
+        />
+      </div>
+    </div>
+  </div>
+</DocumentFragment>
+`;
+
+exports[`<ExtensionsCard /> should show context menu on widget row 1`] = `
+<ul
+  class="mx_IconizedContextMenu"
+  role="none"
+>
+  <div
+    class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
+  >
+    <li
+      aria-label="Remove for everyone"
+      class="mx_AccessibleButton mx_IconizedContextMenu_item"
+      role="menuitem"
+      tabindex="0"
+    >
+      <span
+        class="mx_IconizedContextMenu_label"
+      >
+        Remove for everyone
+      </span>
+    </li>
+  </div>
+</ul>
+`;
diff --git a/test/components/views/right_panel/__snapshots__/RightPanelTabs-test.tsx.snap b/test/components/views/right_panel/__snapshots__/RightPanelTabs-test.tsx.snap
index 36c3ccf6b5..f95c1ed556 100644
--- a/test/components/views/right_panel/__snapshots__/RightPanelTabs-test.tsx.snap
+++ b/test/components/views/right_panel/__snapshots__/RightPanelTabs-test.tsx.snap
@@ -54,6 +54,20 @@ exports[`<RightPanelTabs /> Component renders the correct tabs 1`] = `
           Threads
         </button>
       </li>
+      <li
+        class="_nav-tab_135dy_33"
+        role="presentation"
+      >
+        <button
+          aria-controls="thread-panel"
+          aria-selected="false"
+          class="_nav-item_135dy_55"
+          id="extensions-panel-tab"
+          role="tab"
+        >
+          Extensions
+        </button>
+      </li>
     </ul>
   </nav>
 </div>
@@ -113,6 +127,20 @@ exports[`<RightPanelTabs /> Correct tab is active 1`] = `
           Threads
         </button>
       </li>
+      <li
+        class="_nav-tab_135dy_33"
+        role="presentation"
+      >
+        <button
+          aria-controls="thread-panel"
+          aria-selected="false"
+          class="_nav-item_135dy_55"
+          id="extensions-panel-tab"
+          role="tab"
+        >
+          Extensions
+        </button>
+      </li>
     </ul>
   </nav>
 </div>
diff --git a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap
index e2bee3ac7c..1986c2c036 100644
--- a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap
+++ b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap
@@ -414,20 +414,6 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
           </svg>
         </button>
       </div>
-      <div
-        class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
-      >
-        <h2>
-          Widgets
-        </h2>
-        <div
-          class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
-          role="button"
-          tabindex="0"
-        >
-          Add widgets, bridges & bots
-        </div>
-      </div>
     </div>
   </div>
 </div>
@@ -820,20 +806,6 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
           </svg>
         </button>
       </div>
-      <div
-        class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
-      >
-        <h2>
-          Widgets
-        </h2>
-        <div
-          class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
-          role="button"
-          tabindex="0"
-        >
-          Add widgets, bridges & bots
-        </div>
-      </div>
     </div>
   </div>
 </div>
@@ -1253,20 +1225,6 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
           </svg>
         </button>
       </div>
-      <div
-        class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
-      >
-        <h2>
-          Widgets
-        </h2>
-        <div
-          class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
-          role="button"
-          tabindex="0"
-        >
-          Add widgets, bridges & bots
-        </div>
-      </div>
     </div>
   </div>
 </div>