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
Original file line number Diff line number Diff line change
Expand Up @@ -65,44 +65,36 @@ describe('UnifiedSidebar', () => {
it('renders for agent root route', () => {
renderSidebar('/agents/my-agent');
expect(screen.getByTestId('agentBuilderSidebar-conversation')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-agent')).not.toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
});

it('renders for conversation route', () => {
renderSidebar('/agents/my-agent/conversations/abc-123');
expect(screen.getByTestId('agentBuilderSidebar-conversation')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-agent')).not.toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
});
});

describe('agent settings sidebar', () => {
it('renders for overview route', () => {
renderSidebar('/agents/my-agent/overview');
expect(screen.getByTestId('agentBuilderSidebar-agent')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.getByTestId('agentBuilderSidebar-conversation')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
});

it('renders for skills route', () => {
renderSidebar('/agents/my-agent/skills');
expect(screen.getByTestId('agentBuilderSidebar-agent')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.getByTestId('agentBuilderSidebar-conversation')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
});

it('renders for plugins route', () => {
renderSidebar('/agents/my-agent/plugins');
expect(screen.getByTestId('agentBuilderSidebar-agent')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.getByTestId('agentBuilderSidebar-conversation')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
});

it('renders for connectors route', () => {
renderSidebar('/agents/my-agent/connectors');
expect(screen.getByTestId('agentBuilderSidebar-agent')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.getByTestId('agentBuilderSidebar-conversation')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
});
});
Expand All @@ -112,35 +104,30 @@ describe('UnifiedSidebar', () => {
renderSidebar('/manage/agents');
expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-agent')).not.toBeInTheDocument();
});

it('renders for manage tools route', () => {
renderSidebar('/manage/tools');
expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-agent')).not.toBeInTheDocument();
});

it('renders for manage skills route', () => {
renderSidebar('/manage/skills');
expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-agent')).not.toBeInTheDocument();
});

it('renders for manage plugins route', () => {
renderSidebar('/manage/plugins');
expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-agent')).not.toBeInTheDocument();
});

it('renders for manage connectors route', () => {
renderSidebar('/manage/connectors');
expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
expect(screen.queryByTestId('agentBuilderSidebar-agent')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,17 @@ import { css } from '@emotion/react';

import { i18n } from '@kbn/i18n';
import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common';
import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids';
import { appPaths } from '../../../../../utils/app_paths';
import {
getAgentIdFromPath,
getAgentSettingsNavItems,
getConversationIdFromPath,
} from '../../../../../route_config';
import { useFeatureFlags } from '../../../../../hooks/use_feature_flags';
import { useNavigation } from '../../../../../hooks/use_navigation';
import { useValidateAgentId } from '../../../../../hooks/agents/use_validate_agent_id';
import { useAgentBuilderAgents } from '../../../../../hooks/agents/use_agents';
import { useLastAgentId } from '../../../../../hooks/use_last_agent_id';
import { useKibana } from '../../../../../hooks/use_kibana';
import { useConversationList } from '../../../../../hooks/use_conversation_list';
import { SidebarNavList } from '../../shared/sidebar_nav_list';

Expand Down Expand Up @@ -94,29 +93,18 @@ export const ConversationSidebarView: React.FC = () => {
const validateAgentId = useValidateAgentId();
const { isFetched: isAgentsFetched } = useAgentBuilderAgents();
const lastAgentId = useLastAgentId();
const featureFlags = useFeatureFlags();

const hasSetCustomiseAccordionFirstTime = useRef(false);

const { conversations = [] } = useConversationList({ agentId });
const hasConversations = conversations.length > 0;

const {
services: { uiSettings },
} = useKibana();
const isConnectorsEnabled = uiSettings.get<boolean>(
AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID,
false
const navItems = useMemo(
() => getAgentSettingsNavItems(agentId, featureFlags),
[agentId, featureFlags]
);

const navItems = useMemo(() => {
return getAgentSettingsNavItems(agentId).filter((item) => {
if (item.isConnectors && !isConnectorsEnabled) {
return false;
}
return true;
});
}, [agentId, isConnectorsEnabled]);

const isActive = (path: string) => pathname === path;

const isAnyNavItemActive = navItems.some((item) => isActive(item.path));
Expand Down Expand Up @@ -256,6 +244,7 @@ export const ConversationSidebarView: React.FC = () => {
arrowDisplay="left"
forceState={isChatsOpen ? 'open' : 'closed'}
onToggle={() => setIsChatsOpen((prev) => !prev)}
buttonProps={{ 'data-test-subj': 'agentBuilderSidebarChatsToggle' }}
paddingSize="none"
css={[accordionButtonStyles, chatsAccordionStyles]}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import React, { useMemo } from 'react';

import { EuiFlexGroup, EuiHorizontalRule, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids';
import { useExperimentalFeatures } from '../../../../hooks/use_experimental_features';
import { useKibana } from '../../../../hooks/use_kibana';
import { useFeatureFlags } from '../../../../hooks/use_feature_flags';
import { getManageNavItems } from '../../../../route_config';
import { SidebarNavList } from '../shared/sidebar_nav_list';

Expand All @@ -20,27 +18,10 @@ interface ManageSidebarViewProps {
}

export const ManageSidebarView: React.FC<ManageSidebarViewProps> = ({ pathname }) => {
const isExperimentalFeaturesEnabled = useExperimentalFeatures();
const featureFlags = useFeatureFlags();
const { euiTheme } = useEuiTheme();
const {
services: { uiSettings },
} = useKibana();
const isConnectorsEnabled = uiSettings.get<boolean>(
AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID,
false
);

const navItems = useMemo(() => {
return getManageNavItems().filter((item) => {
if (item.isExperimental && !isExperimentalFeaturesEnabled) {
return false;
}
if (item.isConnectors && !isConnectorsEnabled) {
return false;
}
return true;
});
}, [isExperimentalFeaturesEnabled, isConnectorsEnabled]);
const navItems = useMemo(() => getManageNavItems(featureFlags), [featureFlags]);

const isActive = (path: string) => pathname.startsWith(path);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { useMutation } from '@kbn/react-query';
import { useRef, useState, useMemo, useCallback, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { toToolMetadata } from '@kbn/agent-builder-browser/tools/browser_api_tool';
import { firstValueFrom } from 'rxjs';
import { isEqual } from 'lodash';
Expand Down Expand Up @@ -117,15 +118,26 @@ export const useSendMessageMutation = ({ connectorId }: UseSendMessageMutationPr

const removeError = useCallback(() => {
setError(null);
setErrorSteps([]);
setErrorSteps((prev) => (prev.length === 0 ? prev : []));
}, []);

const { key: locationKey } = useLocation();

useEffect(() => {
// Clear errors any time conversation id changes - we do not persist it.
if (conversationId) {
removeError();
}
}, [conversationId, removeError]);
// Clear error state on every navigation. location.key is updated on every history push,
// including re-navigation to the same URL (e.g. clicking "New" while already on /new
// with an errored conversation). A conversationId-based effect was used previously but
// missed both that case and navigating from an errored conversation back to /new, since
// conversationId stays undefined for the /new route.
//
// Known gap: the old NewConversationButton (which lived inside SendMessageProvider) also
// cancelled any in-flight stream via cleanConversation() when clicking "New" on /new.
// That button was removed during the sidebar refactor and the sidebar replacement lives
// outside SendMessageProvider, so it cannot call cleanConversation() directly. Fixing the
// streaming-cancellation case properly requires a larger provider refactor —
// and is left as a known limitation for now.
removeError();
}, [locationKey, removeError]);

const browserApiToolsMetadata = useMemo(() => {
if (!browserApiTools) return undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useMemo } from 'react';
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids';
import type { FeatureFlags } from '../route_config';
import { useExperimentalFeatures } from './use_experimental_features';

export const useFeatureFlags = (): FeatureFlags => {
const experimental = useExperimentalFeatures();
const connectors = useUiSetting<boolean>(AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID);
return useMemo(() => ({ experimental, connectors }), [experimental, connectors]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import { AgentBuilderConnectorsPage } from './pages/connectors';

export type SidebarView = 'conversation' | 'manage';

export interface FeatureFlags {
experimental: boolean;
connectors: boolean;
}

export interface RouteDefinition {
path: string;
element: React.ReactNode;
Expand Down Expand Up @@ -79,13 +84,15 @@ export const agentRoutes: RouteDefinition[] = [
{
path: '/agents/:agentId/skills',
sidebarView: 'conversation',
isExperimental: true,
navLabel: navLabels.skills,
navIcon: 'bolt',
element: <AgentBuilderAgentSkillsPage />,
},
{
path: '/agents/:agentId/plugins',
sidebarView: 'conversation',
isExperimental: true,
navLabel: navLabels.plugins,
navIcon: 'package',
element: <AgentBuilderAgentPluginsPage />,
Expand Down Expand Up @@ -154,13 +161,15 @@ export const manageRoutes: RouteDefinition[] = [
{
path: '/manage/plugins',
sidebarView: 'manage',
isExperimental: true,
navLabel: navLabels.plugins,
navIcon: 'package',
element: <AgentBuilderPluginsPage />,
},
{
path: '/manage/plugins/:pluginId',
sidebarView: 'manage',
isExperimental: true,
element: <AgentBuilderPluginDetailsPage />,
},
{
Expand Down Expand Up @@ -220,29 +229,37 @@ export interface SidebarNavItem {
label: string;
path: string;
icon?: string;
isExperimental?: boolean;
isConnectors?: boolean;
}

export const getAgentSettingsNavItems = (agentId: string): SidebarNavItem[] => {
const isRouteEnabled = (route: RouteDefinition, flags: FeatureFlags): boolean => {
if (route.isExperimental && !flags.experimental) return false;
if (route.isConnectors && !flags.connectors) return false;
return true;
};

export const getEnabledRoutes = (flags: FeatureFlags): RouteDefinition[] => {
return allRoutes.filter((route) => isRouteEnabled(route, flags));
};

export const getAgentSettingsNavItems = (
agentId: string,
flags: FeatureFlags
): SidebarNavItem[] => {
return agentRoutes
.filter((route) => route.navLabel)
.filter((route) => route.navLabel && isRouteEnabled(route, flags))
.map((route) => ({
label: route.navLabel ?? '',
path: route.path.replace(':agentId', agentId),
icon: route.navIcon,
isConnectors: route.isConnectors,
}));
};

export const getManageNavItems = (): SidebarNavItem[] => {
export const getManageNavItems = (flags: FeatureFlags): SidebarNavItem[] => {
return manageRoutes
.filter((route) => route.navLabel)
.filter((route) => route.navLabel && isRouteEnabled(route, flags))
.map((route) => ({
label: route.navLabel!,
path: route.path,
icon: route.navIcon,
isExperimental: route.isExperimental,
isConnectors: route.isConnectors,
}));
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,16 @@
import { Route, Routes } from '@kbn/shared-ux-router';
import React, { useMemo } from 'react';

import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids';
import { AppLayout } from './components/layout/app_layout';
import { RootRedirect } from './components/redirects/root_redirect';
import { LegacyConversationRedirect } from './components/redirects/legacy_conversation_redirect';
import { allRoutes } from './route_config';
import { useExperimentalFeatures } from './hooks/use_experimental_features';
import { useKibana } from './hooks/use_kibana';
import { getEnabledRoutes } from './route_config';
import { useFeatureFlags } from './hooks/use_feature_flags';

export const AgentBuilderRoutes: React.FC<{}> = () => {
const isExperimentalFeaturesEnabled = useExperimentalFeatures();
const {
services: { uiSettings },
} = useKibana();
const isConnectorsEnabled = uiSettings.get<boolean>(
AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID,
false
);
const featureFlags = useFeatureFlags();

const enabledRoutes = useMemo(() => {
return allRoutes.filter((route) => {
if (route.isExperimental && !isExperimentalFeaturesEnabled) {
return false;
}
if (route.isConnectors && !isConnectorsEnabled) {
return false;
}
return true;
});
}, [isExperimentalFeaturesEnabled, isConnectorsEnabled]);
const enabledRoutes = useMemo(() => getEnabledRoutes(featureFlags), [featureFlags]);

return (
<AppLayout>
Expand Down
Loading
Loading