Skip to content

Commit 031b8b7

Browse files
zanesqamed-xyz
andauthored
fix redirect to extensions page after deeplink install and show toast with success message (block#4863)
Co-authored-by: Amed Rodriguez <[email protected]>
1 parent 610967a commit 031b8b7

File tree

8 files changed

+128
-52
lines changed

8 files changed

+128
-52
lines changed

ui/desktop/src/App.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useState } from 'react';
1+
import { useCallback, useEffect, useState } from 'react';
22
import { IpcRendererEvent } from 'electron';
33
import {
44
HashRouter,
@@ -37,13 +37,14 @@ import PermissionSettingsView from './components/settings/permission/PermissionS
3737
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
3838
import RecipesView from './components/recipes/RecipesView';
3939
import RecipeEditor from './components/recipes/RecipeEditor';
40-
import { createNavigationHandler, View, ViewOptions } from './utils/navigationUtils';
40+
import { View, ViewOptions } from './utils/navigationUtils';
4141
import {
4242
AgentState,
4343
InitializationContext,
4444
NoProviderOrModelError,
4545
useAgent,
4646
} from './hooks/useAgent';
47+
import { useNavigation } from './hooks/useNavigation';
4748

4849
// Route Components
4950
const HubRouteWrapper = ({
@@ -55,8 +56,7 @@ const HubRouteWrapper = ({
5556
isExtensionsLoading: boolean;
5657
resetChat: () => void;
5758
}) => {
58-
const navigate = useNavigate();
59-
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
59+
const setView = useNavigation();
6060

6161
return (
6262
<Hub
@@ -86,8 +86,7 @@ const PairRouteWrapper = ({
8686
loadCurrentChat: (context: InitializationContext) => Promise<ChatType>;
8787
}) => {
8888
const location = useLocation();
89-
const navigate = useNavigate();
90-
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
89+
const setView = useNavigation();
9190
const routeState =
9291
(location.state as PairRouteState) || (window.history.state as PairRouteState) || {};
9392
const [searchParams] = useSearchParams();
@@ -114,7 +113,7 @@ const PairRouteWrapper = ({
114113
const SettingsRoute = () => {
115114
const location = useLocation();
116115
const navigate = useNavigate();
117-
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
116+
const setView = useNavigation();
118117

119118
// Get viewOptions from location.state or history.state
120119
const viewOptions =
@@ -123,8 +122,7 @@ const SettingsRoute = () => {
123122
};
124123

125124
const SessionsRoute = () => {
126-
const navigate = useNavigate();
127-
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
125+
const setView = useNavigation();
128126

129127
return <SessionsView setView={setView} />;
130128
};
@@ -241,8 +239,7 @@ const SharedSessionRouteWrapper = ({
241239
sharedSessionError: string | null;
242240
}) => {
243241
const location = useLocation();
244-
const navigate = useNavigate();
245-
const setView = createNavigationHandler(navigate);
242+
const setView = useNavigation();
246243

247244
const historyState = window.history.state;
248245
const sessionDetails = (location.state?.sessionDetails ||
@@ -315,6 +312,7 @@ export function AppInner() {
315312
const [didSelectProvider, setDidSelectProvider] = useState<boolean>(false);
316313

317314
const navigate = useNavigate();
315+
const setView = useNavigation();
318316

319317
const location = useLocation();
320318
const [_searchParams, setSearchParams] = useSearchParams();
@@ -535,7 +533,7 @@ export function AppInner() {
535533
closeOnClick
536534
pauseOnHover
537535
/>
538-
<ExtensionInstallModal addExtension={addExtension} />
536+
<ExtensionInstallModal addExtension={addExtension} setView={setView} />
539537
<div className="relative w-screen h-screen overflow-hidden bg-background-muted flex flex-col">
540538
<div className="titlebar-drag-region" />
541539
<Routes>

ui/desktop/src/components/ExtensionInstallModal.test.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const mockElectron = {
2121

2222
describe('ExtensionInstallModal', () => {
2323
const mockAddExtension = vi.fn();
24+
const mockSetView = vi.fn();
2425

2526
const getAddExtensionEventHandler = () => {
2627
const addExtensionCall = mockElectron.on.mock.calls.find((call) => call[0] === 'add-extension');
@@ -43,7 +44,7 @@ describe('ExtensionInstallModal', () => {
4344
it('should handle trusted extension (default behaviour, no allowlist)', async () => {
4445
mockElectron.getAllowedExtensions.mockResolvedValue([]);
4546

46-
render(<ExtensionInstallModal addExtension={mockAddExtension} />);
47+
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
4748

4849
const eventHandler = getAddExtensionEventHandler();
4950

@@ -60,7 +61,7 @@ describe('ExtensionInstallModal', () => {
6061
it('should handle trusted extension (from allowlist)', async () => {
6162
mockElectron.getAllowedExtensions.mockResolvedValue(['npx test-extension']);
6263

63-
render(<ExtensionInstallModal addExtension={mockAddExtension} />);
64+
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
6465

6566
const eventHandler = getAddExtensionEventHandler();
6667

@@ -78,7 +79,7 @@ describe('ExtensionInstallModal', () => {
7879
});
7980
mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']);
8081

81-
render(<ExtensionInstallModal addExtension={mockAddExtension} />);
82+
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
8283

8384
const eventHandler = getAddExtensionEventHandler();
8485

@@ -97,7 +98,7 @@ describe('ExtensionInstallModal', () => {
9798
it('should handle i-ching-mcp-server as allowed command', async () => {
9899
mockElectron.getAllowedExtensions.mockResolvedValue([]);
99100

100-
render(<ExtensionInstallModal addExtension={mockAddExtension} />);
101+
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
101102

102103
const eventHandler = getAddExtensionEventHandler();
103104

@@ -116,7 +117,7 @@ describe('ExtensionInstallModal', () => {
116117
it('should handle blocked extension', async () => {
117118
mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']);
118119

119-
render(<ExtensionInstallModal addExtension={mockAddExtension} />);
120+
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
120121

121122
const eventHandler = getAddExtensionEventHandler();
122123

@@ -135,7 +136,7 @@ describe('ExtensionInstallModal', () => {
135136
it('should dismiss modal correctly', async () => {
136137
mockElectron.getAllowedExtensions.mockResolvedValue([]);
137138

138-
render(<ExtensionInstallModal addExtension={mockAddExtension} />);
139+
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
139140

140141
const eventHandler = getAddExtensionEventHandler();
141142

@@ -156,7 +157,7 @@ describe('ExtensionInstallModal', () => {
156157
vi.mocked(addExtensionFromDeepLink).mockResolvedValue(undefined);
157158
mockElectron.getAllowedExtensions.mockResolvedValue([]);
158159

159-
render(<ExtensionInstallModal addExtension={mockAddExtension} />);
160+
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
160161

161162
const eventHandler = getAddExtensionEventHandler();
162163

ui/desktop/src/components/ExtensionInstallModal.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Button } from './ui/button';
1212
import { extractExtensionName } from './settings/extensions/utils';
1313
import { addExtensionFromDeepLink } from './settings/extensions/deeplink';
1414
import type { ExtensionConfig } from '../api/types.gen';
15+
import { View, ViewOptions } from '../utils/navigationUtils';
1516

1617
type ModalType = 'blocked' | 'untrusted' | 'trusted';
1718

@@ -41,10 +42,19 @@ interface ExtensionModalConfig {
4142

4243
interface ExtensionInstallModalProps {
4344
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
45+
setView: (view: View, options?: ViewOptions) => void;
4446
}
4547

4648
function extractCommand(link: string): string {
4749
const url = new URL(link);
50+
51+
// For remote extensions (SSE or Streaming HTTP), return the URL
52+
const remoteUrl = url.searchParams.get('url');
53+
if (remoteUrl) {
54+
return remoteUrl;
55+
}
56+
57+
// For stdio extensions, return the command
4858
const cmd = url.searchParams.get('cmd') || 'Unknown Command';
4959
const args = url.searchParams.getAll('arg').map(decodeURIComponent);
5060
return `${cmd} ${args.join(' ')}`.trim();
@@ -55,7 +65,7 @@ function extractRemoteUrl(link: string): string | null {
5565
return url.searchParams.get('url');
5666
}
5767

58-
export function ExtensionInstallModal({ addExtension }: ExtensionInstallModalProps) {
68+
export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstallModalProps) {
5969
const [modalState, setModalState] = useState<ExtensionModalState>({
6070
isOpen: false,
6171
modalType: 'trusted',
@@ -197,9 +207,14 @@ export function ExtensionInstallModal({ addExtension }: ExtensionInstallModalPro
197207
console.log(`Confirming installation of extension from: ${pendingLink}`);
198208

199209
if (addExtension) {
200-
await addExtensionFromDeepLink(pendingLink, addExtension, () => {
201-
console.log('Extension installation completed, navigating to extensions');
202-
});
210+
await addExtensionFromDeepLink(
211+
pendingLink,
212+
addExtension,
213+
(view: string, options?: ViewOptions) => {
214+
console.log('Extension installation completed, navigating to:', view, options);
215+
setView(view as View, options);
216+
}
217+
);
203218
} else {
204219
throw new Error('addExtension function not provided to component');
205220
}
@@ -216,7 +231,7 @@ export function ExtensionInstallModal({ addExtension }: ExtensionInstallModalPro
216231
isPending: false,
217232
}));
218233
}
219-
}, [pendingLink, dismissModal, addExtension]);
234+
}, [pendingLink, dismissModal, addExtension, setView]);
220235

221236
useEffect(() => {
222237
console.log('Setting up extension install modal handler');

ui/desktop/src/components/extensions/ExtensionsView.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { Button } from '../ui/button';
77
import { Plus } from 'lucide-react';
88
import { GPSIcon } from '../ui/icons';
99
import { useState, useEffect } from 'react';
10+
import kebabCase from 'lodash/kebabCase';
1011
import ExtensionModal from '../settings/extensions/modal/ExtensionModal';
1112
import {
1213
getDefaultFormData,
1314
ExtensionFormData,
1415
createExtensionConfig,
1516
} from '../settings/extensions/utils';
16-
import { activateExtension } from '../settings/extensions/index';
17+
import { activateExtension } from '../settings/extensions';
1718
import { useConfig } from '../ConfigContext';
1819

1920
export type ExtensionsViewOptions = {
@@ -38,13 +39,37 @@ export default function ExtensionsView({
3839
console.error('ExtensionsView: No session ID available');
3940
}
4041

41-
// Trigger refresh when deep link config changes (i.e., when a deep link is processed)
42+
// Only trigger refresh when deep link config changes AND we don't need to show env vars
4243
useEffect(() => {
43-
if (viewOptions.deepLinkConfig) {
44+
if (viewOptions.deepLinkConfig && !viewOptions.showEnvVars) {
4445
setRefreshKey((prevKey) => prevKey + 1);
4546
}
4647
}, [viewOptions.deepLinkConfig, viewOptions.showEnvVars]);
4748

49+
const scrollToExtension = (extensionName: string) => {
50+
setTimeout(() => {
51+
const element = document.getElementById(`extension-${kebabCase(extensionName)}`);
52+
if (element) {
53+
element.scrollIntoView({
54+
behavior: 'smooth',
55+
block: 'center',
56+
});
57+
// Add a subtle highlight effect
58+
element.style.boxShadow = '0 0 0 2px rgba(59, 130, 246, 0.5)';
59+
setTimeout(() => {
60+
element.style.boxShadow = '';
61+
}, 2000);
62+
}
63+
}, 200);
64+
};
65+
66+
// Scroll to extension whenever extensionId is provided (after refresh)
67+
useEffect(() => {
68+
if (viewOptions.deepLinkConfig?.name && refreshKey > 0) {
69+
scrollToExtension(viewOptions.deepLinkConfig?.name);
70+
}
71+
}, [viewOptions.deepLinkConfig?.name, refreshKey]);
72+
4873
const handleModalClose = () => {
4974
setIsAddModalOpen(false);
5075
};
@@ -119,6 +144,9 @@ export default function ExtensionsView({
119144
deepLinkConfig={viewOptions.deepLinkConfig}
120145
showEnvVars={viewOptions.showEnvVars}
121146
hideButtons={true}
147+
onModalClose={(extensionName: string) => {
148+
scrollToExtension(extensionName);
149+
}}
122150
/>
123151
</div>
124152

ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface ExtensionSectionProps {
2424
disableConfiguration?: boolean;
2525
customToggle?: (extension: FixedExtensionEntry) => Promise<boolean | void>;
2626
selectedExtensions?: string[]; // Add controlled state
27+
onModalClose?: (extensionName: string) => void;
2728
}
2829

2930
export default function ExtensionsSection({
@@ -34,6 +35,7 @@ export default function ExtensionsSection({
3435
disableConfiguration,
3536
customToggle,
3637
selectedExtensions = [],
38+
onModalClose,
3739
}: ExtensionSectionProps) {
3840
const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig();
3941
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
@@ -127,11 +129,15 @@ export default function ExtensionsSection({
127129
extensionConfig: extensionConfig,
128130
sessionId: sessionId,
129131
});
130-
// Immediately refresh the extensions list after successful activation
131-
await fetchExtensions();
132132
} catch (error) {
133133
console.error('Failed to activate extension:', error);
134+
} finally {
134135
await fetchExtensions();
136+
if (onModalClose) {
137+
setTimeout(() => {
138+
onModalClose(formData.name);
139+
}, 200);
140+
}
135141
}
136142
};
137143

0 commit comments

Comments
 (0)