diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/route_display.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/route_display.tsx
new file mode 100644
index 0000000000000..ef40fa9e878a3
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/route_display.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 React from 'react';
+import { useLocation, useParams } from 'react-router-dom';
+
+import { EuiCode, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+export const RouteDisplay: React.FC = () => {
+ const location = useLocation();
+ const params = useParams();
+ const { euiTheme } = useEuiTheme();
+
+ const containerStyles = css`
+ padding: ${euiTheme.size.xl};
+ height: 100%;
+ `;
+
+ const codeStyles = css`
+ font-size: ${euiTheme.size.l};
+ padding: ${euiTheme.size.m};
+ `;
+
+ return (
+
+
+
+ Current Route
+
+
+
+ {location.pathname}
+
+ {Object.keys(params).length > 0 && (
+
+
+ Route params: {JSON.stringify(params)}
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx
new file mode 100644
index 0000000000000..71e5e54e96160
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 React from 'react';
+
+import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+import { UnifiedSidebar } from './unified_sidebar/unified_sidebar';
+
+interface AppLayoutProps {
+ children: React.ReactNode;
+}
+
+export const AppLayout: React.FC = ({ children }) => {
+ const { euiTheme } = useEuiTheme();
+
+ const contentStyles = css`
+ overflow: auto;
+ background-color: ${euiTheme.colors.backgroundBasePlain};
+ `;
+
+ return (
+
+
+
+
+ {children}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/agent_selector.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/agent_selector.tsx
new file mode 100644
index 0000000000000..5b39e0e910557
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/agent_selector.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 React, { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom-v5-compat';
+
+import { EuiFlexItem, EuiText, EuiSelect, EuiLoadingSpinner } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+
+import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents';
+import { storageKeys } from '../../../storage_keys';
+
+const labels = {
+ agentLabel: i18n.translate('xpack.agentBuilder.sidebar.agentSelector.agentLabel', {
+ defaultMessage: 'Agent',
+ }),
+ selectAgent: i18n.translate('xpack.agentBuilder.sidebar.agentSelector.selectAgent', {
+ defaultMessage: 'Select agent',
+ }),
+};
+
+interface AgentSelectorProps {
+ agentId: string;
+ getNavigationPath: (newAgentId: string) => string;
+}
+
+export const AgentSelector: React.FC = ({ agentId, getNavigationPath }) => {
+ const { agents, isLoading } = useAgentBuilderAgents();
+ const navigate = useNavigate();
+ const [, setStoredAgentId] = useLocalStorage(storageKeys.agentId);
+
+ const handleAgentChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const newAgentId = e.target.value;
+ setStoredAgentId(newAgentId);
+ navigate(getNavigationPath(newAgentId));
+ },
+ [navigate, setStoredAgentId, getNavigationPath]
+ );
+
+ const agentOptions = agents.map((agent) => ({
+ value: agent.id,
+ text: agent.name,
+ }));
+
+ return (
+
+
+ {labels.agentLabel}
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx
new file mode 100644
index 0000000000000..4cf5a40dcf4cb
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 React, { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+
+import { EuiPanel } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+import { storageKeys } from '../../../storage_keys';
+import { getSidebarViewForRoute, getAgentIdFromPath } from '../../../route_config';
+import { ConversationSidebarView } from './views/conversation_view';
+import { AgentSettingsSidebarView } from './views/agent_settings_view';
+import { ManageSidebarView } from './views/manage_view';
+
+const SIDEBAR_WIDTH = 200;
+
+export const UnifiedSidebar: React.FC = () => {
+ const location = useLocation();
+ const sidebarView = getSidebarViewForRoute(location.pathname);
+ const agentIdFromPath = getAgentIdFromPath(location.pathname);
+ const [, setStoredAgentId] = useLocalStorage(storageKeys.agentId);
+
+ useEffect(() => {
+ if (agentIdFromPath) {
+ setStoredAgentId(agentIdFromPath);
+ }
+ }, [agentIdFromPath, setStoredAgentId]);
+
+ const sidebarStyles = css`
+ width: ${SIDEBAR_WIDTH}px;
+ min-width: ${SIDEBAR_WIDTH}px;
+ height: 100%;
+ border-radius: 0;
+ `;
+
+ return (
+
+ {sidebarView === 'conversation' && }
+ {sidebarView === 'agentSettings' && }
+ {sidebarView === 'manage' && }
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/agent_settings_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/agent_settings_view.tsx
new file mode 100644
index 0000000000000..443e1ac7651e1
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/agent_settings_view.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 React, { useCallback, useMemo } from 'react';
+import { Link } from 'react-router-dom-v5-compat';
+
+import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+
+import { appPaths } from '../../../../utils/app_paths';
+import { getAgentIdFromPath, getAgentSettingsNavItems } from '../../../../route_config';
+import { AgentSelector } from '../agent_selector';
+
+const labels = {
+ back: i18n.translate('xpack.agentBuilder.sidebar.agentSettings.back', {
+ defaultMessage: '← Back',
+ }),
+ title: i18n.translate('xpack.agentBuilder.sidebar.agentSettings.title', {
+ defaultMessage: 'Agent Settings',
+ }),
+};
+
+interface AgentSettingsSidebarViewProps {
+ pathname: string;
+}
+
+export const AgentSettingsSidebarView: React.FC = ({ pathname }) => {
+ const agentId = getAgentIdFromPath(pathname) ?? 'elastic-ai-agent';
+ const { euiTheme } = useEuiTheme();
+
+ const linkStyles = css`
+ text-decoration: none;
+ color: inherit;
+ &:hover {
+ text-decoration: underline;
+ }
+ `;
+
+ const activeLinkStyles = css`
+ ${linkStyles}
+ font-weight: ${euiTheme.font.weight.bold};
+ color: ${euiTheme.colors.primaryText};
+ `;
+
+ const getNavigationPath = useCallback(
+ (newAgentId: string) => pathname.replace(`/agents/${agentId}`, `/agents/${newAgentId}`),
+ [pathname, agentId]
+ );
+
+ const navItems = useMemo(() => {
+ return getAgentSettingsNavItems(agentId);
+ }, [agentId]);
+
+ const isActive = (path: string) => pathname === path;
+
+ return (
+
+
+
+ {labels.back}
+
+
+
+
+
+
+
+
+
+
+
+ {labels.title}
+
+
+
+ {navItems.map((item) => (
+
+
+ {item.label}
+
+
+ ))}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view.tsx
new file mode 100644
index 0000000000000..f9e8ce86d5b0d
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 React, { useCallback } from 'react';
+import { Link } from 'react-router-dom-v5-compat';
+
+import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+
+import { appPaths } from '../../../../utils/app_paths';
+import { getAgentIdFromPath } from '../../../../route_config';
+import { AgentSelector } from '../agent_selector';
+
+const labels = {
+ customize: i18n.translate('xpack.agentBuilder.sidebar.conversation.customize', {
+ defaultMessage: 'Customize',
+ }),
+ conversationsTitle: i18n.translate('xpack.agentBuilder.sidebar.conversation.conversationsTitle', {
+ defaultMessage: 'Conversations',
+ }),
+ newConversation: i18n.translate('xpack.agentBuilder.sidebar.conversation.newConversation', {
+ defaultMessage: '+ New conversation',
+ }),
+ manageComponents: i18n.translate('xpack.agentBuilder.sidebar.conversation.manageComponents', {
+ defaultMessage: 'Manage components',
+ }),
+};
+
+interface ConversationSidebarViewProps {
+ pathname: string;
+}
+
+export const ConversationSidebarView: React.FC = ({ pathname }) => {
+ const agentId = getAgentIdFromPath(pathname) ?? 'elastic-ai-agent';
+
+ const linkStyles = css`
+ text-decoration: none;
+ color: inherit;
+ &:hover {
+ text-decoration: underline;
+ }
+ `;
+
+ const getNavigationPath = useCallback(
+ (newAgentId: string) => appPaths.agent.root({ agentId: newAgentId }),
+ []
+ );
+
+ return (
+
+
+
+
+
+ {labels.customize}
+
+
+
+
+
+
+
+ {labels.conversationsTitle}
+
+
+
+
+
+ {labels.newConversation}
+
+
+
+
+
+
+
+ {labels.manageComponents}
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx
new file mode 100644
index 0000000000000..7b913a38d6d35
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 React, { useMemo } from 'react';
+import { Link } from 'react-router-dom-v5-compat';
+
+import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+
+import { appPaths } from '../../../../utils/app_paths';
+import { useLastAgentId } from '../../../../hooks/use_last_agent_id';
+import { useExperimentalFeatures } from '../../../../hooks/use_experimental_features';
+import { getManageNavItems } from '../../../../route_config';
+
+const labels = {
+ back: i18n.translate('xpack.agentBuilder.sidebar.manage.back', {
+ defaultMessage: '← Back',
+ }),
+ title: i18n.translate('xpack.agentBuilder.sidebar.manage.title', {
+ defaultMessage: 'Manage Components',
+ }),
+};
+
+interface ManageSidebarViewProps {
+ pathname: string;
+}
+
+export const ManageSidebarView: React.FC = ({ pathname }) => {
+ const lastAgentId = useLastAgentId();
+ const { euiTheme } = useEuiTheme();
+ const isExperimentalFeaturesEnabled = useExperimentalFeatures();
+
+ const linkStyles = css`
+ text-decoration: none;
+ color: inherit;
+ &:hover {
+ text-decoration: underline;
+ }
+ `;
+
+ const activeLinkStyles = css`
+ ${linkStyles}
+ font-weight: ${euiTheme.font.weight.bold};
+ color: ${euiTheme.colors.primaryText};
+ `;
+
+ const isActive = (path: string) => pathname.startsWith(path);
+
+ const navItems = useMemo(() => {
+ return getManageNavItems().filter((item) => {
+ if (item.isExperimental && !isExperimentalFeaturesEnabled) {
+ return false;
+ }
+ return true;
+ });
+ }, [isExperimentalFeaturesEnabled]);
+
+ return (
+
+
+
+ {labels.back}
+
+
+
+
+
+
+
+ {labels.title}
+
+
+
+ {navItems.map((item) => (
+
+
+ {item.label}
+
+
+ ))}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/redirects/legacy_conversation_redirect.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/redirects/legacy_conversation_redirect.tsx
new file mode 100644
index 0000000000000..ff39458cf86b2
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/redirects/legacy_conversation_redirect.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 React, { useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { Navigate, useNavigate } from 'react-router-dom-v5-compat';
+
+import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { useQuery } from '@kbn/react-query';
+
+import { useLastAgentId } from '../../hooks/use_last_agent_id';
+import { useAgentBuilderServices } from '../../hooks/use_agent_builder_service';
+import { appPaths } from '../../utils/app_paths';
+import { newConversationId } from '../../utils/new_conversation';
+
+export const LegacyConversationRedirect: React.FC = () => {
+ const { conversationId } = useParams<{ conversationId?: string }>();
+ const navigate = useNavigate();
+ const lastAgentId = useLastAgentId();
+ const { conversationsService } = useAgentBuilderServices();
+
+ const isNewConversation = !conversationId || conversationId === newConversationId;
+
+ const {
+ data: conversation,
+ isLoading,
+ isError,
+ } = useQuery({
+ queryKey: ['conversation-redirect', conversationId],
+ queryFn: () => conversationsService.get({ conversationId: conversationId! }),
+ enabled: !isNewConversation,
+ retry: false,
+ });
+
+ useEffect(() => {
+ if (conversation?.agent_id && conversationId) {
+ navigate(
+ appPaths.agent.conversations.byId({
+ agentId: conversation.agent_id,
+ conversationId,
+ }),
+ { replace: true }
+ );
+ } else if (isError && conversationId) {
+ navigate(appPaths.agent.root({ agentId: lastAgentId }), { replace: true });
+ }
+ }, [conversation, conversationId, isError, lastAgentId, navigate]);
+
+ if (isNewConversation) {
+ return ;
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return null;
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/redirects/root_redirect.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/redirects/root_redirect.tsx
new file mode 100644
index 0000000000000..87d900232c17e
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/redirects/root_redirect.tsx
@@ -0,0 +1,17 @@
+/*
+ * 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 React from 'react';
+import { Navigate } from 'react-router-dom-v5-compat';
+
+import { useLastAgentId } from '../../hooks/use_last_agent_id';
+import { appPaths } from '../../utils/app_paths';
+
+export const RootRedirect: React.FC = () => {
+ const lastAgentId = useLastAgentId();
+ return ;
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_last_agent_id.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_last_agent_id.ts
new file mode 100644
index 0000000000000..03c5c768a3802
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_last_agent_id.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 useLocalStorage from 'react-use/lib/useLocalStorage';
+import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common';
+
+import { storageKeys } from '../storage_keys';
+
+export const useLastAgentId = (): string => {
+ const [agentIdStorage] = useLocalStorage(storageKeys.agentId);
+ return agentIdStorage ?? agentBuilderDefaultAgentId;
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts
new file mode 100644
index 0000000000000..2612a536d16d7
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 { getSidebarViewForRoute, getAgentIdFromPath } from './route_config';
+
+describe('route_config', () => {
+ describe('getSidebarViewForRoute', () => {
+ describe('agent routes', () => {
+ it('returns "conversation" for agent root', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent')).toBe('conversation');
+ expect(getSidebarViewForRoute('/agents/my-custom-agent')).toBe('conversation');
+ });
+
+ it('returns "conversation" for conversation routes', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/conversations/123')).toBe(
+ 'conversation'
+ );
+ expect(getSidebarViewForRoute('/agents/my-agent/conversations/abc-def')).toBe(
+ 'conversation'
+ );
+ });
+
+ it('returns "agentSettings" for instructions route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/instructions')).toBe(
+ 'agentSettings'
+ );
+ });
+
+ it('returns "agentSettings" for skills route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/skills')).toBe('agentSettings');
+ });
+
+ it('returns "agentSettings" for tools route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/tools')).toBe('agentSettings');
+ });
+
+ it('returns "agentSettings" for plugins route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/plugins')).toBe('agentSettings');
+ });
+
+ it('returns "agentSettings" for connectors route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/connectors')).toBe('agentSettings');
+ });
+ });
+
+ describe('manage routes', () => {
+ it('returns "manage" for manage agents routes', () => {
+ expect(getSidebarViewForRoute('/manage/agents')).toBe('manage');
+ expect(getSidebarViewForRoute('/manage/agents/new')).toBe('manage');
+ });
+
+ it('returns "manage" for manage tools routes', () => {
+ expect(getSidebarViewForRoute('/manage/tools')).toBe('manage');
+ expect(getSidebarViewForRoute('/manage/tools/new')).toBe('manage');
+ expect(getSidebarViewForRoute('/manage/tools/tool-123')).toBe('manage');
+ expect(getSidebarViewForRoute('/manage/tools/bulk_import_mcp')).toBe('manage');
+ });
+
+ it('returns "manage" for manage skills routes', () => {
+ expect(getSidebarViewForRoute('/manage/skills')).toBe('manage');
+ expect(getSidebarViewForRoute('/manage/skills/new')).toBe('manage');
+ expect(getSidebarViewForRoute('/manage/skills/skill-123')).toBe('manage');
+ });
+
+ it('returns "manage" for manage plugins routes', () => {
+ expect(getSidebarViewForRoute('/manage/plugins')).toBe('manage');
+ expect(getSidebarViewForRoute('/manage/plugins/plugin-123')).toBe('manage');
+ });
+
+ it('returns "manage" for manage connectors route', () => {
+ expect(getSidebarViewForRoute('/manage/connectors')).toBe('manage');
+ });
+ });
+
+ describe('fallback behavior', () => {
+ it('returns "conversation" for unknown routes', () => {
+ expect(getSidebarViewForRoute('/unknown')).toBe('conversation');
+ expect(getSidebarViewForRoute('/')).toBe('conversation');
+ expect(getSidebarViewForRoute('/some/random/path')).toBe('conversation');
+ });
+ });
+ });
+
+ describe('getAgentIdFromPath', () => {
+ it('extracts agent ID from agent routes', () => {
+ expect(getAgentIdFromPath('/agents/elastic-ai-agent')).toBe('elastic-ai-agent');
+ expect(getAgentIdFromPath('/agents/my-custom-agent')).toBe('my-custom-agent');
+ expect(getAgentIdFromPath('/agents/elastic-ai-agent/skills')).toBe('elastic-ai-agent');
+ expect(getAgentIdFromPath('/agents/elastic-ai-agent/conversations/123')).toBe(
+ 'elastic-ai-agent'
+ );
+ });
+
+ it('returns undefined for non-agent routes', () => {
+ expect(getAgentIdFromPath('/manage/agents')).toBeUndefined();
+ expect(getAgentIdFromPath('/manage/tools')).toBeUndefined();
+ expect(getAgentIdFromPath('/')).toBeUndefined();
+ expect(getAgentIdFromPath('/unknown')).toBeUndefined();
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx
new file mode 100644
index 0000000000000..8fe7002b1fe96
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx
@@ -0,0 +1,194 @@
+/*
+ * 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 React from 'react';
+import { matchPath } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+
+import { RouteDisplay } from './components/common/route_display';
+
+export type SidebarView = 'conversation' | 'agentSettings' | 'manage';
+
+export interface RouteDefinition {
+ path: string;
+ element: React.ReactNode;
+ sidebarView: SidebarView;
+ isExperimental?: boolean;
+ navLabel?: string;
+}
+
+const navLabels = {
+ instructions: i18n.translate('xpack.agentBuilder.routeConfig.instructions', {
+ defaultMessage: 'Instructions',
+ }),
+ skills: i18n.translate('xpack.agentBuilder.routeConfig.skills', {
+ defaultMessage: 'Skills',
+ }),
+ tools: i18n.translate('xpack.agentBuilder.routeConfig.tools', {
+ defaultMessage: 'Tools',
+ }),
+ plugins: i18n.translate('xpack.agentBuilder.routeConfig.plugins', {
+ defaultMessage: 'Plugins',
+ }),
+ connectors: i18n.translate('xpack.agentBuilder.routeConfig.connectors', {
+ defaultMessage: 'Connectors',
+ }),
+ agents: i18n.translate('xpack.agentBuilder.routeConfig.agents', {
+ defaultMessage: 'Agents',
+ }),
+};
+
+// Routes ordered from most specific to least specific for correct matching
+export const agentRoutes: RouteDefinition[] = [
+ {
+ path: '/agents/:agentId/conversations/:conversationId',
+ sidebarView: 'conversation',
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/instructions',
+ sidebarView: 'agentSettings',
+ navLabel: navLabels.instructions,
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/skills',
+ sidebarView: 'agentSettings',
+ navLabel: navLabels.skills,
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/plugins',
+ sidebarView: 'agentSettings',
+ navLabel: navLabels.plugins,
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/connectors',
+ sidebarView: 'agentSettings',
+ navLabel: navLabels.connectors,
+ element: ,
+ },
+ // Catch-all for agent root - must be last
+ {
+ path: '/agents/:agentId',
+ sidebarView: 'conversation',
+ element: ,
+ },
+];
+
+export const manageRoutes: RouteDefinition[] = [
+ {
+ path: '/manage/agents',
+ sidebarView: 'manage',
+ navLabel: navLabels.agents,
+ element: ,
+ },
+ {
+ path: '/manage/agents/new',
+ sidebarView: 'manage',
+ element: ,
+ },
+ {
+ path: '/manage/tools',
+ sidebarView: 'manage',
+ navLabel: navLabels.tools,
+ element: ,
+ },
+ {
+ path: '/manage/tools/new',
+ sidebarView: 'manage',
+ element: ,
+ },
+ {
+ path: '/manage/tools/bulk_import_mcp',
+ sidebarView: 'manage',
+ element: ,
+ },
+ {
+ path: '/manage/tools/:toolId',
+ sidebarView: 'manage',
+ element: ,
+ },
+ {
+ path: '/manage/skills',
+ sidebarView: 'manage',
+ navLabel: navLabels.skills,
+ isExperimental: true,
+ element: ,
+ },
+ {
+ path: '/manage/skills/new',
+ sidebarView: 'manage',
+ isExperimental: true,
+ element: ,
+ },
+ {
+ path: '/manage/skills/:skillId',
+ sidebarView: 'manage',
+ isExperimental: true,
+ element: ,
+ },
+ {
+ path: '/manage/plugins',
+ sidebarView: 'manage',
+ navLabel: navLabels.plugins,
+ element: ,
+ },
+ {
+ path: '/manage/plugins/:pluginId',
+ sidebarView: 'manage',
+ element: ,
+ },
+ {
+ path: '/manage/connectors',
+ sidebarView: 'manage',
+ navLabel: navLabels.connectors,
+ element: ,
+ },
+];
+
+export const allRoutes: RouteDefinition[] = [...agentRoutes, ...manageRoutes];
+
+export const getSidebarViewForRoute = (pathname: string): SidebarView => {
+ for (const route of allRoutes) {
+ if (matchPath(pathname, { path: route.path, exact: false })) {
+ return route.sidebarView;
+ }
+ }
+ return 'conversation';
+};
+
+export const getAgentIdFromPath = (pathname: string): string | undefined => {
+ const match = pathname.match(/^\/agents\/([^/]+)/);
+ return match ? match[1] : undefined;
+};
+
+export const getAgentSettingsNavItems = (
+ agentId: string
+): Array<{ label: string; path: string }> => {
+ return agentRoutes
+ .filter((route) => route.navLabel && route.sidebarView === 'agentSettings')
+ .map((route) => ({
+ label: route.navLabel!,
+ path: route.path.replace(':agentId', agentId),
+ }));
+};
+
+export const getManageNavItems = (): Array<{
+ label: string;
+ path: string;
+ isExperimental?: boolean;
+}> => {
+ return manageRoutes
+ .filter((route) => route.navLabel)
+ .map((route) => ({
+ label: route.navLabel!,
+ path: route.path,
+ isExperimental: route.isExperimental,
+ }));
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/routes.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/routes.tsx
index 7d2388a6e8054..e2a2425c12bf7 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/routes.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/routes.tsx
@@ -6,88 +6,45 @@
*/
import { Route, Routes } from '@kbn/shared-ux-router';
-import React from 'react';
-import { AgentBuilderAgentsCreate } from './pages/agent_create';
-import { AgentBuilderAgentsEdit } from './pages/agent_edit';
-import { AgentBuilderAgentsPage } from './pages/agents';
-import { AgentBuilderConversationsPage } from './pages/conversations';
-import { AgentBuilderToolCreatePage } from './pages/tool_create';
-import { AgentBuilderToolDetailsPage } from './pages/tool_details';
-import { AgentBuilderToolsPage } from './pages/tools';
-import { AgentBuilderBulkImportMcpToolsPage } from './pages/bulk_import_mcp_tools';
-import { AgentBuilderSkillsPage } from './pages/skills';
-import { AgentBuilderSkillCreatePage } from './pages/skill_create';
-import { AgentBuilderSkillDetailsPage } from './pages/skill_details';
-import { AgentBuilderPluginsPage } from './pages/plugins';
-import { AgentBuilderPluginDetailsPage } from './pages/plugin_details';
+import React, { useMemo } from 'react';
+
+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';
export const AgentBuilderRoutes: React.FC<{}> = () => {
const isExperimentalFeaturesEnabled = useExperimentalFeatures();
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ const enabledRoutes = useMemo(() => {
+ return allRoutes.filter((route) => {
+ if (route.isExperimental && !isExperimentalFeaturesEnabled) {
+ return false;
+ }
+ return true;
+ });
+ }, [isExperimentalFeaturesEnabled]);
-
-
-
-
-
-
-
-
-
-
-
-
- {isExperimentalFeaturesEnabled
- ? [
-
-
- ,
-
-
- ,
-
-
- ,
- ]
- : null}
-
- {isExperimentalFeaturesEnabled
- ? [
-
-
- ,
-
-
- ,
- ]
- : null}
-
- {/* Default to conversations page */}
-
-
-
-
+ return (
+
+
+ {enabledRoutes.map((route) => (
+
+ {route.element}
+
+ ))}
+
+ {/* Legacy routes - redirect to new structure */}
+
+
+
+
+ {/* Root route - redirect to last used agent */}
+
+
+
+
+
);
};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts
index d7f910bbed71e..7d7888e1fafa1 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts
@@ -9,35 +9,35 @@ import { newConversationId } from './new_conversation';
export const appPaths = {
root: '/',
- agents: {
- list: '/agents',
- new: '/agents/new',
- edit: ({ agentId }: { agentId: string }) => {
- return `/agents/${agentId}`;
- },
- },
- chat: {
- new: `/conversations/${newConversationId}`,
- newWithAgent: ({ agentId }: { agentId: string }) => {
- return `/conversations/${newConversationId}?agent_id=${agentId}`;
- },
- conversation: ({ conversationId }: { conversationId: string }) => {
- return `/conversations/${conversationId}`;
+
+ // Agent-scoped routes (all under /agents/:agentId)
+ agent: {
+ root: ({ agentId }: { agentId: string }) => `/agents/${agentId}`,
+ conversations: {
+ new: ({ agentId }: { agentId: string }) =>
+ `/agents/${agentId}/conversations/${newConversationId}`,
+ byId: ({ agentId, conversationId }: { agentId: string; conversationId: string }) =>
+ `/agents/${agentId}/conversations/${conversationId}`,
},
+ skills: ({ agentId }: { agentId: string }) => `/agents/${agentId}/skills`,
+ plugins: ({ agentId }: { agentId: string }) => `/agents/${agentId}/plugins`,
+ connectors: ({ agentId }: { agentId: string }) => `/agents/${agentId}/connectors`,
+ instructions: ({ agentId }: { agentId: string }) => `/agents/${agentId}/instructions`,
},
- tools: {
- list: '/tools',
- new: '/tools/new',
- details: ({ toolId }: { toolId: string }) => `/tools/${toolId}`,
- bulkImportMcp: '/tools/bulk_import_mcp',
- },
- skills: {
- list: '/skills',
- new: '/skills/new',
- details: ({ skillId }: { skillId: string }) => `/skills/${skillId}`,
- },
- plugins: {
- list: '/plugins',
- details: ({ pluginId }: { pluginId: string }) => `/plugins/${pluginId}`,
+
+ // Manage routes (global CRUD, no agent context)
+ manage: {
+ agents: '/manage/agents',
+ agentsNew: '/manage/agents/new',
+ tools: '/manage/tools',
+ toolsNew: '/manage/tools/new',
+ toolDetails: ({ toolId }: { toolId: string }) => `/manage/tools/${toolId}`,
+ toolsBulkImport: '/manage/tools/bulk_import_mcp',
+ skills: '/manage/skills',
+ skillsNew: '/manage/skills/new',
+ skillDetails: ({ skillId }: { skillId: string }) => `/manage/skills/${skillId}`,
+ plugins: '/manage/plugins',
+ pluginDetails: ({ pluginId }: { pluginId: string }) => `/manage/plugins/${pluginId}`,
+ connectors: '/manage/connectors',
},
};