From f1c48588575a5d0c6c8eaf1a802e006fc838b871 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 26 Sep 2025 12:04:42 -0700 Subject: [PATCH 1/7] fix redirect to extensions page after deeplink install and show toast with success message --- ui/desktop/src/App.tsx | 3 ++- .../src/components/ExtensionInstallModal.tsx | 19 +++++++++++----- .../components/extensions/ExtensionsView.tsx | 22 +++++++++++++++++++ .../settings/extensions/deeplink.ts | 13 +++++++++-- .../subcomponents/ExtensionItem.tsx | 1 + 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 17a6f9bde9dd..13593bb580e8 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -316,6 +316,7 @@ export function AppInner() { const [didSelectProvider, setDidSelectProvider] = useState(false); const navigate = useNavigate(); + const setView = useMemo(() => createNavigationHandler(navigate), [navigate]); const location = useLocation(); const [_searchParams, setSearchParams] = useSearchParams(); @@ -534,7 +535,7 @@ export function AppInner() { closeOnClick pauseOnHover /> - +
diff --git a/ui/desktop/src/components/ExtensionInstallModal.tsx b/ui/desktop/src/components/ExtensionInstallModal.tsx index 2ac3bea07ca6..5037adaba916 100644 --- a/ui/desktop/src/components/ExtensionInstallModal.tsx +++ b/ui/desktop/src/components/ExtensionInstallModal.tsx @@ -12,6 +12,7 @@ import { Button } from './ui/button'; import { extractExtensionName } from './settings/extensions/utils'; import { addExtensionFromDeepLink } from './settings/extensions/deeplink'; import type { ExtensionConfig } from '../api/types.gen'; +import { View, ViewOptions } from '../utils/navigationUtils'; type ModalType = 'blocked' | 'untrusted' | 'trusted'; @@ -41,6 +42,7 @@ interface ExtensionModalConfig { interface ExtensionInstallModalProps { addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise; + setView?: (view: View, options?: ViewOptions) => void; } function extractCommand(link: string): string { @@ -55,7 +57,7 @@ function extractRemoteUrl(link: string): string | null { return url.searchParams.get('url'); } -export function ExtensionInstallModal({ addExtension }: ExtensionInstallModalProps) { +export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstallModalProps) { const [modalState, setModalState] = useState({ isOpen: false, modalType: 'trusted', @@ -197,9 +199,16 @@ export function ExtensionInstallModal({ addExtension }: ExtensionInstallModalPro console.log(`Confirming installation of extension from: ${pendingLink}`); if (addExtension) { - await addExtensionFromDeepLink(pendingLink, addExtension, () => { - console.log('Extension installation completed, navigating to extensions'); - }); + await addExtensionFromDeepLink( + pendingLink, + addExtension, + (view: string, options?: ViewOptions) => { + console.log('Extension installation completed, navigating to:', view, options); + if (setView) { + setView(view as View, options); + } + } + ); } else { throw new Error('addExtension function not provided to component'); } @@ -216,7 +225,7 @@ export function ExtensionInstallModal({ addExtension }: ExtensionInstallModalPro isPending: false, })); } - }, [pendingLink, dismissModal, addExtension]); + }, [pendingLink, dismissModal, addExtension, setView]); useEffect(() => { console.log('Setting up extension install modal handler'); diff --git a/ui/desktop/src/components/extensions/ExtensionsView.tsx b/ui/desktop/src/components/extensions/ExtensionsView.tsx index 8907435dc1d2..389d7c5641e8 100644 --- a/ui/desktop/src/components/extensions/ExtensionsView.tsx +++ b/ui/desktop/src/components/extensions/ExtensionsView.tsx @@ -19,6 +19,7 @@ import { useConfig } from '../ConfigContext'; export type ExtensionsViewOptions = { deepLinkConfig?: ExtensionConfig; showEnvVars?: boolean; + extensionId?: string; }; export default function ExtensionsView({ @@ -45,6 +46,27 @@ export default function ExtensionsView({ } }, [viewOptions.deepLinkConfig, viewOptions.showEnvVars]); + // Scroll to extension after refresh if extensionId is provided + useEffect(() => { + if (viewOptions.extensionId && refreshKey > 0) { + // Use setTimeout to ensure the DOM has been updated after the refresh + setTimeout(() => { + const element = document.getElementById(`extension-${viewOptions.extensionId}`); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + // Add a subtle highlight effect + element.style.boxShadow = '0 0 0 2px rgba(59, 130, 246, 0.5)'; + setTimeout(() => { + element.style.boxShadow = ''; + }, 2000); + } + }, 100); + } + }, [viewOptions.extensionId, refreshKey]); + const handleModalClose = () => { setIsAddModalOpen(false); }; diff --git a/ui/desktop/src/components/settings/extensions/deeplink.ts b/ui/desktop/src/components/settings/extensions/deeplink.ts index 308dbf2d342c..a62052d73073 100644 --- a/ui/desktop/src/components/settings/extensions/deeplink.ts +++ b/ui/desktop/src/components/settings/extensions/deeplink.ts @@ -180,9 +180,9 @@ export async function addExtensionFromDeepLink( config.type === 'streamable_http' && config.headers && Object.keys(config.headers).length > 0; if (hasEnvVars || hasHeaders) { - console.log('Environment variables or headers required, redirecting to settings'); + console.log('Environment variables or headers required, redirecting to extensions'); console.log('Calling setView with:', { deepLinkConfig: config, showEnvVars: true }); - setView('settings', { deepLinkConfig: config, showEnvVars: true }); + setView('extensions', { deepLinkConfig: config, showEnvVars: true }); return; } @@ -193,6 +193,15 @@ export async function addExtensionFromDeepLink( // It will be activated when the next session starts console.warn('Extension will be added to config but requires a session to activate'); await addExtensionFn(config.name, config, true); + + // Show success toast and navigate to extensions page + toastService.success({ + title: 'Extension Installed', + msg: `${config.name} extension has been installed successfully. Start a new chat session to use it.`, + }); + + // Navigate to extensions page to show the newly installed extension + setView('extensions', { deepLinkConfig: config, showEnvVars: false, extensionId: config.name }); } catch (error) { console.error('Failed to activate extension from deeplink:', error); throw error; diff --git a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx index 578a001b83de..d4f47dc4bee9 100644 --- a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx +++ b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx @@ -73,6 +73,7 @@ export default function ExtensionItem({ return ( handleToggle(extension)} > From 240dad1e27de7391c0fe75633633889c7e2e9c98 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 26 Sep 2025 14:12:55 -0700 Subject: [PATCH 2/7] remove comment --- ui/desktop/src/components/extensions/ExtensionsView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/desktop/src/components/extensions/ExtensionsView.tsx b/ui/desktop/src/components/extensions/ExtensionsView.tsx index 389d7c5641e8..a618e9722fad 100644 --- a/ui/desktop/src/components/extensions/ExtensionsView.tsx +++ b/ui/desktop/src/components/extensions/ExtensionsView.tsx @@ -49,7 +49,6 @@ export default function ExtensionsView({ // Scroll to extension after refresh if extensionId is provided useEffect(() => { if (viewOptions.extensionId && refreshKey > 0) { - // Use setTimeout to ensure the DOM has been updated after the refresh setTimeout(() => { const element = document.getElementById(`extension-${viewOptions.extensionId}`); if (element) { From e6cc7fb2c9d65f008986dc0972f7a35630f4d225 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 29 Sep 2025 09:26:16 -0700 Subject: [PATCH 3/7] reuse createNavigationHandler and remove unnecessary memoization --- ui/desktop/src/App.tsx | 21 +++++++++------------ ui/desktop/src/hooks/useNavigation.ts | 13 +++++++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 ui/desktop/src/hooks/useNavigation.ts diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 13593bb580e8..af9f71c2cff9 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { IpcRendererEvent } from 'electron'; import { HashRouter, @@ -38,13 +38,14 @@ import PermissionSettingsView from './components/settings/permission/PermissionS import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView'; import RecipesView from './components/recipes/RecipesView'; import RecipeEditor from './components/recipes/RecipeEditor'; -import { createNavigationHandler, View, ViewOptions } from './utils/navigationUtils'; +import { View, ViewOptions } from './utils/navigationUtils'; import { AgentState, InitializationContext, NoProviderOrModelError, useAgent, } from './hooks/useAgent'; +import { useNavigation } from './hooks/useNavigation'; // Route Components const HubRouteWrapper = ({ @@ -56,8 +57,7 @@ const HubRouteWrapper = ({ isExtensionsLoading: boolean; resetChat: () => void; }) => { - const navigate = useNavigate(); - const setView = useMemo(() => createNavigationHandler(navigate), [navigate]); + const setView = useNavigation(); return ( Promise; }) => { const location = useLocation(); - const navigate = useNavigate(); - const setView = useMemo(() => createNavigationHandler(navigate), [navigate]); + const setView = useNavigation(); const routeState = (location.state as PairRouteState) || (window.history.state as PairRouteState) || {}; const [searchParams] = useSearchParams(); @@ -115,7 +114,7 @@ const PairRouteWrapper = ({ const SettingsRoute = () => { const location = useLocation(); const navigate = useNavigate(); - const setView = useMemo(() => createNavigationHandler(navigate), [navigate]); + const setView = useNavigation(); // Get viewOptions from location.state or history.state const viewOptions = @@ -124,8 +123,7 @@ const SettingsRoute = () => { }; const SessionsRoute = () => { - const navigate = useNavigate(); - const setView = useMemo(() => createNavigationHandler(navigate), [navigate]); + const setView = useNavigation(); return ; }; @@ -242,8 +240,7 @@ const SharedSessionRouteWrapper = ({ sharedSessionError: string | null; }) => { const location = useLocation(); - const navigate = useNavigate(); - const setView = createNavigationHandler(navigate); + const setView = useNavigation(); const historyState = window.history.state; const sessionDetails = (location.state?.sessionDetails || @@ -316,7 +313,7 @@ export function AppInner() { const [didSelectProvider, setDidSelectProvider] = useState(false); const navigate = useNavigate(); - const setView = useMemo(() => createNavigationHandler(navigate), [navigate]); + const setView = useNavigation(); const location = useLocation(); const [_searchParams, setSearchParams] = useSearchParams(); diff --git a/ui/desktop/src/hooks/useNavigation.ts b/ui/desktop/src/hooks/useNavigation.ts new file mode 100644 index 000000000000..2dfefa3a418b --- /dev/null +++ b/ui/desktop/src/hooks/useNavigation.ts @@ -0,0 +1,13 @@ +import { useNavigate } from 'react-router-dom'; +import { createNavigationHandler } from '../utils/navigationUtils'; + +/** + * Custom hook that provides a navigation handler function. + * Eliminates the repetitive pattern of creating navigation handlers in components. + * + * @returns A navigation handler function + */ +export const useNavigation = () => { + const navigate = useNavigate(); + return createNavigationHandler(navigate); +}; From dc346a008fa5597856d72dc3e73b43972cbb9daf Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 29 Sep 2025 10:17:51 -0700 Subject: [PATCH 4/7] make setView required --- .../components/ExtensionInstallModal.test.tsx | 29 ++++++++++--------- .../src/components/ExtensionInstallModal.tsx | 6 ++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ui/desktop/src/components/ExtensionInstallModal.test.tsx b/ui/desktop/src/components/ExtensionInstallModal.test.tsx index 9d65635882cd..057451f56a90 100644 --- a/ui/desktop/src/components/ExtensionInstallModal.test.tsx +++ b/ui/desktop/src/components/ExtensionInstallModal.test.tsx @@ -21,6 +21,7 @@ const mockElectron = { describe('ExtensionInstallModal', () => { const mockAddExtension = vi.fn(); + const mockSetView = vi.fn(); const getAddExtensionEventHandler = () => { const addExtensionCall = mockElectron.on.mock.calls.find((call) => call[0] === 'add-extension'); @@ -43,7 +44,7 @@ describe('ExtensionInstallModal', () => { it('should handle trusted extension (default behaviour, no allowlist)', async () => { mockElectron.getAllowedExtensions.mockResolvedValue([]); - render(); + render(); const eventHandler = getAddExtensionEventHandler(); @@ -60,7 +61,7 @@ describe('ExtensionInstallModal', () => { it('should handle trusted extension (from allowlist)', async () => { mockElectron.getAllowedExtensions.mockResolvedValue(['npx test-extension']); - render(); + render(); const eventHandler = getAddExtensionEventHandler(); @@ -78,7 +79,7 @@ describe('ExtensionInstallModal', () => { }); mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']); - render(); + render(); const eventHandler = getAddExtensionEventHandler(); @@ -94,27 +95,29 @@ describe('ExtensionInstallModal', () => { expect(screen.getAllByRole('button')).toHaveLength(3); }); - - it("should handle i-ching-mcp-server as allowed command", async () => { + it('should handle i-ching-mcp-server as allowed command', async () => { mockElectron.getAllowedExtensions.mockResolvedValue([]); - render(); + render(); const eventHandler = getAddExtensionEventHandler(); await act(async () => { - await eventHandler({}, "goose://extension?cmd=i-ching-mcp-server&id=i-ching&name=I%20Ching&description=I%20Ching%20divination"); + await eventHandler( + {}, + 'goose://extension?cmd=i-ching-mcp-server&id=i-ching&name=I%20Ching&description=I%20Ching%20divination' + ); }); - expect(screen.getByRole("dialog")).toBeInTheDocument(); - expect(screen.getByText("Confirm Extension Installation")).toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText('Confirm Extension Installation')).toBeInTheDocument(); expect(screen.getByText(/I Ching extension/)).toBeInTheDocument(); - expect(screen.getAllByRole("button")).toHaveLength(3); + expect(screen.getAllByRole('button')).toHaveLength(3); }); it('should handle blocked extension', async () => { mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']); - render(); + render(); const eventHandler = getAddExtensionEventHandler(); @@ -133,7 +136,7 @@ describe('ExtensionInstallModal', () => { it('should dismiss modal correctly', async () => { mockElectron.getAllowedExtensions.mockResolvedValue([]); - render(); + render(); const eventHandler = getAddExtensionEventHandler(); @@ -154,7 +157,7 @@ describe('ExtensionInstallModal', () => { vi.mocked(addExtensionFromDeepLink).mockResolvedValue(undefined); mockElectron.getAllowedExtensions.mockResolvedValue([]); - render(); + render(); const eventHandler = getAddExtensionEventHandler(); diff --git a/ui/desktop/src/components/ExtensionInstallModal.tsx b/ui/desktop/src/components/ExtensionInstallModal.tsx index 5037adaba916..aceeb6be6254 100644 --- a/ui/desktop/src/components/ExtensionInstallModal.tsx +++ b/ui/desktop/src/components/ExtensionInstallModal.tsx @@ -42,7 +42,7 @@ interface ExtensionModalConfig { interface ExtensionInstallModalProps { addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise; - setView?: (view: View, options?: ViewOptions) => void; + setView: (view: View, options?: ViewOptions) => void; } function extractCommand(link: string): string { @@ -204,9 +204,7 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal addExtension, (view: string, options?: ViewOptions) => { console.log('Extension installation completed, navigating to:', view, options); - if (setView) { - setView(view as View, options); - } + setView(view as View, options); } ); } else { From 1da8ca1f63fc73cca8069896c8725d2922c37b2f Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 29 Sep 2025 10:22:10 -0700 Subject: [PATCH 5/7] remove unnecessary try catch and log --- .../settings/extensions/deeplink.ts | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/ui/desktop/src/components/settings/extensions/deeplink.ts b/ui/desktop/src/components/settings/extensions/deeplink.ts index a62052d73073..c09668ae8d49 100644 --- a/ui/desktop/src/components/settings/extensions/deeplink.ts +++ b/ui/desktop/src/components/settings/extensions/deeplink.ts @@ -119,9 +119,6 @@ export async function addExtensionFromDeepLink( | { deepLinkConfig: ExtensionConfig; showEnvVars: boolean } ) => void ) { - console.log('=== addExtensionFromDeepLink Debug ==='); - console.log('URL:', url); - const parsedUrl = new URL(url); if (parsedUrl.protocol !== 'goose:') { @@ -186,24 +183,18 @@ export async function addExtensionFromDeepLink( return; } - try { - console.log('No env vars required, activating extension directly'); - // Note: deeplink activation doesn't have access to sessionId - // The extension will be added to config but not activated in the current session - // It will be activated when the next session starts - console.warn('Extension will be added to config but requires a session to activate'); - await addExtensionFn(config.name, config, true); - - // Show success toast and navigate to extensions page - toastService.success({ - title: 'Extension Installed', - msg: `${config.name} extension has been installed successfully. Start a new chat session to use it.`, - }); - - // Navigate to extensions page to show the newly installed extension - setView('extensions', { deepLinkConfig: config, showEnvVars: false, extensionId: config.name }); - } catch (error) { - console.error('Failed to activate extension from deeplink:', error); - throw error; - } + console.log('No env vars required, activating extension directly'); + // Note: deeplink activation doesn't have access to sessionId + // The extension will be added to config but not activated in the current session + // It will be activated when the next session starts + await addExtensionFn(config.name, config, true); + + // Show success toast and navigate to extensions page + toastService.success({ + title: 'Extension Installed', + msg: `${config.name} extension has been installed successfully. Start a new chat session to use it.`, + }); + + // Navigate to extensions page to show the newly installed extension + setView('extensions', { deepLinkConfig: config, showEnvVars: false, extensionId: config.name }); } From 8cf7a93ddf9cff01e7efe6167cd49566e6d56aae Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Tue, 30 Sep 2025 14:54:16 -0700 Subject: [PATCH 6/7] remove extensionId and use config name instead --- .../components/extensions/ExtensionsView.tsx | 49 +++++++++++-------- .../settings/extensions/ExtensionsSection.tsx | 10 +++- .../settings/extensions/deeplink.ts | 11 ++--- .../subcomponents/ExtensionItem.tsx | 5 +- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/ui/desktop/src/components/extensions/ExtensionsView.tsx b/ui/desktop/src/components/extensions/ExtensionsView.tsx index a618e9722fad..b4af1adc23c0 100644 --- a/ui/desktop/src/components/extensions/ExtensionsView.tsx +++ b/ui/desktop/src/components/extensions/ExtensionsView.tsx @@ -7,19 +7,19 @@ import { Button } from '../ui/button'; import { Plus } from 'lucide-react'; import { GPSIcon } from '../ui/icons'; import { useState, useEffect } from 'react'; +import kebabCase from 'lodash/kebabCase'; import ExtensionModal from '../settings/extensions/modal/ExtensionModal'; import { getDefaultFormData, ExtensionFormData, createExtensionConfig, } from '../settings/extensions/utils'; -import { activateExtension } from '../settings/extensions/index'; +import { activateExtension } from '../settings/extensions'; import { useConfig } from '../ConfigContext'; export type ExtensionsViewOptions = { deepLinkConfig?: ExtensionConfig; showEnvVars?: boolean; - extensionId?: string; }; export default function ExtensionsView({ @@ -39,32 +39,36 @@ export default function ExtensionsView({ console.error('ExtensionsView: No session ID available'); } - // Trigger refresh when deep link config changes (i.e., when a deep link is processed) + // Only trigger refresh when deep link config changes AND we don't need to show env vars useEffect(() => { - if (viewOptions.deepLinkConfig) { + if (viewOptions.deepLinkConfig && !viewOptions.showEnvVars) { setRefreshKey((prevKey) => prevKey + 1); } }, [viewOptions.deepLinkConfig, viewOptions.showEnvVars]); - // Scroll to extension after refresh if extensionId is provided + const scrollToExtension = (extensionName: string) => { + setTimeout(() => { + const element = document.getElementById(`extension-${kebabCase(extensionName)}`); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + // Add a subtle highlight effect + element.style.boxShadow = '0 0 0 2px rgba(59, 130, 246, 0.5)'; + setTimeout(() => { + element.style.boxShadow = ''; + }, 2000); + } + }, 200); + }; + + // Scroll to extension whenever extensionId is provided (after refresh) useEffect(() => { - if (viewOptions.extensionId && refreshKey > 0) { - setTimeout(() => { - const element = document.getElementById(`extension-${viewOptions.extensionId}`); - if (element) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); - // Add a subtle highlight effect - element.style.boxShadow = '0 0 0 2px rgba(59, 130, 246, 0.5)'; - setTimeout(() => { - element.style.boxShadow = ''; - }, 2000); - } - }, 100); + if (viewOptions.deepLinkConfig?.name && refreshKey > 0) { + scrollToExtension(viewOptions.deepLinkConfig?.name); } - }, [viewOptions.extensionId, refreshKey]); + }, [viewOptions.deepLinkConfig?.name, refreshKey]); const handleModalClose = () => { setIsAddModalOpen(false); @@ -140,6 +144,9 @@ export default function ExtensionsView({ deepLinkConfig={viewOptions.deepLinkConfig} showEnvVars={viewOptions.showEnvVars} hideButtons={true} + onModalClose={(extensionName: string) => { + scrollToExtension(extensionName); + }} />
diff --git a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx index c51a46b7cf5a..c225212700e4 100644 --- a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx @@ -24,6 +24,7 @@ interface ExtensionSectionProps { disableConfiguration?: boolean; customToggle?: (extension: FixedExtensionEntry) => Promise; selectedExtensions?: string[]; // Add controlled state + onModalClose?: (extensionName: string) => void; } export default function ExtensionsSection({ @@ -34,6 +35,7 @@ export default function ExtensionsSection({ disableConfiguration, customToggle, selectedExtensions = [], + onModalClose, }: ExtensionSectionProps) { const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig(); const [selectedExtension, setSelectedExtension] = useState(null); @@ -127,11 +129,15 @@ export default function ExtensionsSection({ extensionConfig: extensionConfig, sessionId: sessionId, }); - // Immediately refresh the extensions list after successful activation - await fetchExtensions(); } catch (error) { console.error('Failed to activate extension:', error); + } finally { await fetchExtensions(); + if (onModalClose) { + setTimeout(() => { + onModalClose(formData.name); + }, 200); + } } }; diff --git a/ui/desktop/src/components/settings/extensions/deeplink.ts b/ui/desktop/src/components/settings/extensions/deeplink.ts index c09668ae8d49..8666707050ae 100644 --- a/ui/desktop/src/components/settings/extensions/deeplink.ts +++ b/ui/desktop/src/components/settings/extensions/deeplink.ts @@ -114,9 +114,7 @@ export async function addExtensionFromDeepLink( ) => Promise, setView: ( view: string, - options: - | { extensionId: string; showEnvVars: boolean } - | { deepLinkConfig: ExtensionConfig; showEnvVars: boolean } + options: { showEnvVars: boolean; deepLinkConfig?: ExtensionConfig } ) => void ) { const parsedUrl = new URL(url); @@ -177,8 +175,9 @@ export async function addExtensionFromDeepLink( config.type === 'streamable_http' && config.headers && Object.keys(config.headers).length > 0; if (hasEnvVars || hasHeaders) { - console.log('Environment variables or headers required, redirecting to extensions'); - console.log('Calling setView with:', { deepLinkConfig: config, showEnvVars: true }); + console.log( + 'Environment variables or headers required, redirecting to extensions with env variables modal showing' + ); setView('extensions', { deepLinkConfig: config, showEnvVars: true }); return; } @@ -196,5 +195,5 @@ export async function addExtensionFromDeepLink( }); // Navigate to extensions page to show the newly installed extension - setView('extensions', { deepLinkConfig: config, showEnvVars: false, extensionId: config.name }); + setView('extensions', { deepLinkConfig: config, showEnvVars: false }); } diff --git a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx index d4f47dc4bee9..872142dfbce9 100644 --- a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx +++ b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; +import kebabCase from 'lodash/kebabCase'; import { Switch } from '../../../ui/switch'; -import { Gear } from '../../../icons/Gear'; +import { Gear } from '../../../icons'; import { FixedExtensionEntry } from '../../../ConfigContext'; import { getSubtitle, getFriendlyTitle } from './ExtensionList'; import { Card, CardHeader, CardTitle, CardContent, CardAction } from '../../../ui/card'; @@ -73,7 +74,7 @@ export default function ExtensionItem({ return ( handleToggle(extension)} > From d0578db4d9a2838d281d0535120805aadbfa35a2 Mon Sep 17 00:00:00 2001 From: Amed Rodriguez Date: Thu, 2 Oct 2025 10:45:46 -0700 Subject: [PATCH 7/7] Fix Streaming HTTP extension parsing --- .../src/components/ExtensionInstallModal.tsx | 8 ++++++++ .../components/settings/extensions/deeplink.ts | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/ExtensionInstallModal.tsx b/ui/desktop/src/components/ExtensionInstallModal.tsx index aceeb6be6254..0478730c53c2 100644 --- a/ui/desktop/src/components/ExtensionInstallModal.tsx +++ b/ui/desktop/src/components/ExtensionInstallModal.tsx @@ -47,6 +47,14 @@ interface ExtensionInstallModalProps { function extractCommand(link: string): string { const url = new URL(link); + + // For remote extensions (SSE or Streaming HTTP), return the URL + const remoteUrl = url.searchParams.get('url'); + if (remoteUrl) { + return remoteUrl; + } + + // For stdio extensions, return the command const cmd = url.searchParams.get('cmd') || 'Unknown Command'; const args = url.searchParams.getAll('arg').map(decodeURIComponent); return `${cmd} ${args.join(' ')}`.trim(); diff --git a/ui/desktop/src/components/settings/extensions/deeplink.ts b/ui/desktop/src/components/settings/extensions/deeplink.ts index 8666707050ae..8620acedbe4c 100644 --- a/ui/desktop/src/components/settings/extensions/deeplink.ts +++ b/ui/desktop/src/components/settings/extensions/deeplink.ts @@ -88,7 +88,8 @@ function getStreamableHttpConfig( name: string, description: string, timeout: number, - headers?: { [key: string]: string } + headers?: { [key: string]: string }, + envs?: { [key: string]: string } ) { const config: ExtensionConfig = { name, @@ -97,6 +98,7 @@ function getStreamableHttpConfig( description, timeout: timeout, headers: headers, + envs: envs, }; return config; @@ -163,9 +165,21 @@ export async function addExtensionFromDeepLink( ) : undefined; + // Parse env vars for remote extensions (same logic as stdio) + const envList = parsedUrl.searchParams.getAll('env'); + const envs = + envList.length > 0 + ? Object.fromEntries( + envList.map((env) => { + const [key] = env.split('='); + return [key, '']; + }) + ) + : undefined; + const config = remoteUrl ? transportType === 'streamable_http' - ? getStreamableHttpConfig(remoteUrl, name, description || '', timeout, headers) + ? getStreamableHttpConfig(remoteUrl, name, description || '', timeout, headers, envs) : getSseConfig(remoteUrl, name, description || '', timeout) : getStdioConfig(cmd!, parsedUrl, name, description || '', timeout);