Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { IpcRendererEvent } from 'electron';
import {
HashRouter,
Expand Down Expand Up @@ -37,13 +37,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 = ({
Expand All @@ -55,8 +56,7 @@ const HubRouteWrapper = ({
isExtensionsLoading: boolean;
resetChat: () => void;
}) => {
const navigate = useNavigate();
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
const setView = useNavigation();

return (
<Hub
Expand Down Expand Up @@ -86,8 +86,7 @@ const PairRouteWrapper = ({
loadCurrentChat: (context: InitializationContext) => Promise<ChatType>;
}) => {
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();
Expand All @@ -114,7 +113,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 =
Expand All @@ -123,8 +122,7 @@ const SettingsRoute = () => {
};

const SessionsRoute = () => {
const navigate = useNavigate();
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
const setView = useNavigation();

return <SessionsView setView={setView} />;
};
Expand Down Expand Up @@ -241,8 +239,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 ||
Expand Down Expand Up @@ -315,6 +312,7 @@ export function AppInner() {
const [didSelectProvider, setDidSelectProvider] = useState<boolean>(false);

const navigate = useNavigate();
const setView = useNavigation();

const location = useLocation();
const [_searchParams, setSearchParams] = useSearchParams();
Expand Down Expand Up @@ -535,7 +533,7 @@ export function AppInner() {
closeOnClick
pauseOnHover
/>
<ExtensionInstallModal addExtension={addExtension} />
<ExtensionInstallModal addExtension={addExtension} setView={setView} />
<div className="relative w-screen h-screen overflow-hidden bg-background-muted flex flex-col">
<div className="titlebar-drag-region" />
<Routes>
Expand Down
15 changes: 8 additions & 7 deletions ui/desktop/src/components/ExtensionInstallModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -43,7 +44,7 @@ describe('ExtensionInstallModal', () => {
it('should handle trusted extension (default behaviour, no allowlist)', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue([]);

render(<ExtensionInstallModal addExtension={mockAddExtension} />);
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);

const eventHandler = getAddExtensionEventHandler();

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

render(<ExtensionInstallModal addExtension={mockAddExtension} />);
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);

const eventHandler = getAddExtensionEventHandler();

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

render(<ExtensionInstallModal addExtension={mockAddExtension} />);
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);

const eventHandler = getAddExtensionEventHandler();

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

render(<ExtensionInstallModal addExtension={mockAddExtension} />);
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);

const eventHandler = getAddExtensionEventHandler();

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

render(<ExtensionInstallModal addExtension={mockAddExtension} />);
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);

const eventHandler = getAddExtensionEventHandler();

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

render(<ExtensionInstallModal addExtension={mockAddExtension} />);
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);

const eventHandler = getAddExtensionEventHandler();

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

render(<ExtensionInstallModal addExtension={mockAddExtension} />);
render(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);

const eventHandler = getAddExtensionEventHandler();

Expand Down
25 changes: 20 additions & 5 deletions ui/desktop/src/components/ExtensionInstallModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -41,10 +42,19 @@ interface ExtensionModalConfig {

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

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();
Expand All @@ -55,7 +65,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<ExtensionModalState>({
isOpen: false,
modalType: 'trusted',
Expand Down Expand Up @@ -197,9 +207,14 @@ 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);
setView(view as View, options);
}
);
} else {
throw new Error('addExtension function not provided to component');
}
Expand All @@ -216,7 +231,7 @@ export function ExtensionInstallModal({ addExtension }: ExtensionInstallModalPro
isPending: false,
}));
}
}, [pendingLink, dismissModal, addExtension]);
}, [pendingLink, dismissModal, addExtension, setView]);

useEffect(() => {
console.log('Setting up extension install modal handler');
Expand Down
34 changes: 31 additions & 3 deletions ui/desktop/src/components/extensions/ExtensionsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ 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 = {
Expand All @@ -38,13 +39,37 @@ 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]);

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.deepLinkConfig?.name && refreshKey > 0) {
scrollToExtension(viewOptions.deepLinkConfig?.name);
}
}, [viewOptions.deepLinkConfig?.name, refreshKey]);

const handleModalClose = () => {
setIsAddModalOpen(false);
};
Expand Down Expand Up @@ -119,6 +144,9 @@ export default function ExtensionsView({
deepLinkConfig={viewOptions.deepLinkConfig}
showEnvVars={viewOptions.showEnvVars}
hideButtons={true}
onModalClose={(extensionName: string) => {
scrollToExtension(extensionName);
}}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface ExtensionSectionProps {
disableConfiguration?: boolean;
customToggle?: (extension: FixedExtensionEntry) => Promise<boolean | void>;
selectedExtensions?: string[]; // Add controlled state
onModalClose?: (extensionName: string) => void;
}

export default function ExtensionsSection({
Expand All @@ -34,6 +35,7 @@ export default function ExtensionsSection({
disableConfiguration,
customToggle,
selectedExtensions = [],
onModalClose,
}: ExtensionSectionProps) {
const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig();
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
Expand Down Expand Up @@ -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);
}
}
};

Expand Down
Loading
Loading