diff --git a/x-pack/platform/plugins/shared/agent_builder/moon.yml b/x-pack/platform/plugins/shared/agent_builder/moon.yml
index f261e78837ab8..7a42e77a6c760 100644
--- a/x-pack/platform/plugins/shared/agent_builder/moon.yml
+++ b/x-pack/platform/plugins/shared/agent_builder/moon.yml
@@ -110,7 +110,6 @@ dependsOn:
- '@kbn/core-elasticsearch-server-mocks'
- '@kbn/core-data-streams-server'
- '@kbn/task-manager-plugin'
- - '@kbn/deeplinks-data-sources'
- '@kbn/evals-plugin'
- '@kbn/usage-api-plugin'
- '@kbn/es-query'
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/active_item_row.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/active_item_row.tsx
new file mode 100644
index 0000000000000..7ddde57b3c56b
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/active_item_row.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 from 'react';
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+export interface ActiveItemRowProps {
+ id: string;
+ name: string;
+ isSelected: boolean;
+ onSelect: () => void;
+ onRemove: () => void;
+ isRemoving?: boolean;
+ removeAriaLabel: string;
+ readOnlyContent?: React.ReactNode;
+}
+
+export const ActiveItemRow: React.FC = ({
+ name,
+ isSelected,
+ onSelect,
+ onRemove,
+ isRemoving = false,
+ removeAriaLabel,
+ readOnlyContent,
+}) => {
+ const { euiTheme } = useEuiTheme();
+
+ return (
+
+
+
+ {name}
+
+
+ {isSelected && (
+
+ {readOnlyContent ?? (
+ {
+ event.stopPropagation();
+ onRemove();
+ }}
+ />
+ )}
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/index.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/constants.ts
similarity index 57%
rename from x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/index.ts
rename to x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/constants.ts
index 4a8f5fcb2b374..1f688fa7a0f02 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/index.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/constants.ts
@@ -5,4 +5,10 @@
* 2.0.
*/
-export { AgentSelector } from './agent_selector';
+export const FLYOUT_WIDTH = '960px';
+
+export const CONTAINER_WIDTH = '1200px';
+
+export const ICON_DIMENSIONS = { width: 24, height: 28 } as const;
+
+export const SEARCH_LIST_WIDTH = '340px';
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/detail_panel_layout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/detail_panel_layout.tsx
new file mode 100644
index 0000000000000..f557136a47392
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/detail_panel_layout.tsx
@@ -0,0 +1,145 @@
+/*
+ * 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, { useState } from 'react';
+import {
+ EuiConfirmModal,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiLoadingSpinner,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+
+interface ConfirmRemoveConfig {
+ title: string;
+ body: string;
+ confirmButtonText: string;
+ cancelButtonText: string;
+ onConfirm: () => void;
+}
+
+export interface DetailPanelLayoutProps {
+ isLoading: boolean;
+ isEmpty: boolean;
+ title: string;
+ showAutoIcon?: boolean;
+ headerActions: (openConfirmRemove: () => void) => React.ReactNode;
+ headerContent?: React.ReactNode;
+ children: React.ReactNode;
+ confirmRemove?: ConfirmRemoveConfig;
+}
+
+export const DetailPanelLayout: React.FC = ({
+ isLoading,
+ isEmpty,
+ title,
+ showAutoIcon = false,
+ headerActions,
+ headerContent,
+ children,
+ confirmRemove,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const [isConfirmOpen, setIsConfirmOpen] = useState(false);
+
+ const openConfirmRemove = () => setIsConfirmOpen(true);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (isEmpty) return null;
+
+ return (
+
+
+ {/* Header: pinned at top, does not scroll */}
+
+
+
+
+
+
+ {title}
+
+
+ {showAutoIcon && (
+
+
+
+ )}
+
+
+ {headerActions(openConfirmRemove)}
+
+ {headerContent}
+
+
+ {/* Body: fills remaining height and scrolls on overflow */}
+
+ {children}
+
+
+
+ {confirmRemove && isConfirmOpen && (
+
setIsConfirmOpen(false)}
+ onConfirm={() => {
+ setIsConfirmOpen(false);
+ confirmRemove.onConfirm();
+ }}
+ cancelButtonText={confirmRemove.cancelButtonText}
+ confirmButtonText={confirmRemove.confirmButtonText}
+ buttonColor="danger"
+ >
+ {confirmRemove.body}
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/detail_row.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/detail_row.tsx
new file mode 100644
index 0000000000000..4e09d9e048c59
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/detail_row.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { EuiText, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+export interface DetailRowProps {
+ label: string;
+ children: React.ReactNode;
+ isLast?: boolean;
+}
+
+export const DetailRow: React.FC = ({ label, children, isLast = false }) => {
+ const { euiTheme } = useEuiTheme();
+
+ return (
+
+
+ {label}
+
+
+ {children}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/library_panel.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/library_panel.tsx
new file mode 100644
index 0000000000000..68443ebba3a54
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/library_panel.tsx
@@ -0,0 +1,168 @@
+/*
+ * 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, useState, useCallback } from 'react';
+import {
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiLink,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { LibraryToggleRow } from './library_toggle_row';
+import { FLYOUT_WIDTH } from './constants';
+
+export interface LibraryItem {
+ id: string;
+ description: string;
+}
+
+export interface LibraryPanelLabels {
+ title: string;
+ manageLibraryLink: string;
+ searchPlaceholder: string;
+ availableSummary: (filtered: number, total: number) => string;
+ noMatchMessage: string;
+ noItemsMessage: string;
+ disabledBadgeLabel?: string;
+ disabledTooltipTitle?: string;
+ disabledTooltipBody?: string;
+}
+
+export interface LibraryPanelProps {
+ onClose: () => void;
+ allItems: T[];
+ activeItemIdSet: Set;
+ onToggleItem: (item: T, isActive: boolean) => void;
+ mutatingItemId: string | null;
+ flyoutTitleId: string;
+ libraryLabels: LibraryPanelLabels;
+ manageLibraryPath: string;
+ getItemName?: (item: T) => string;
+ getSearchableText?: (item: T) => string[];
+ disabledItemIdSet?: Set;
+ callout?: React.ReactNode;
+}
+
+const defaultGetItemName = (item: T): string => item.id;
+
+export const LibraryPanel = ({
+ onClose,
+ allItems,
+ activeItemIdSet,
+ onToggleItem,
+ mutatingItemId,
+ flyoutTitleId,
+ libraryLabels,
+ manageLibraryPath,
+ getItemName = defaultGetItemName,
+ getSearchableText,
+ disabledItemIdSet,
+ callout,
+}: LibraryPanelProps) => {
+ const { createAgentBuilderUrl } = useNavigation();
+ const manageLibraryUrl = createAgentBuilderUrl(manageLibraryPath);
+ const [searchQuery, setSearchQuery] = useState('');
+ const { euiTheme } = useEuiTheme();
+
+ const getSearchFields = useCallback(
+ (item: T): string[] =>
+ getSearchableText ? getSearchableText(item) : [getItemName(item), item.description],
+ [getSearchableText, getItemName]
+ );
+
+ const filteredItems = useMemo(() => {
+ if (!searchQuery.trim()) return allItems;
+ const lower = searchQuery.toLowerCase();
+ return allItems.filter((item) =>
+ getSearchFields(item).some((field) => field.toLowerCase().includes(lower))
+ );
+ }, [allItems, searchQuery, getSearchFields]);
+
+ return (
+
+
+
+
+
+ {libraryLabels.title}
+
+
+ {libraryLabels.manageLibraryLink}
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ incremental
+ fullWidth
+ />
+
+
+
+
+ {libraryLabels.availableSummary(filteredItems.length, allItems.length)}
+
+
+
+
+ {callout}
+
+ {filteredItems.length === 0 ? (
+
+ {searchQuery.trim() ? libraryLabels.noMatchMessage : libraryLabels.noItemsMessage}
+
+ ) : (
+
+ {filteredItems.map((item) => (
+
+ onToggleItem(item, checked)}
+ isMutating={mutatingItemId === item.id}
+ isDisabled={disabledItemIdSet?.has(item.id)}
+ disabledBadgeLabel={libraryLabels.disabledBadgeLabel}
+ disabledTooltipTitle={libraryLabels.disabledTooltipTitle}
+ disabledTooltipBody={libraryLabels.disabledTooltipBody}
+ />
+
+ ))}
+
+ )}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/library_toggle_row.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/library_toggle_row.tsx
new file mode 100644
index 0000000000000..715db4214c6d5
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/library_toggle_row.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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 {
+ EuiBadge,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiIcon,
+ EuiSwitch,
+ EuiText,
+ EuiToolTip,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+
+export interface LibraryToggleRowProps {
+ id: string;
+ name: string;
+ description: string;
+ isActive: boolean;
+ onToggle: (isActive: boolean) => void;
+ isMutating: boolean;
+ isDisabled?: boolean;
+ disabledBadgeLabel?: string;
+ disabledTooltipTitle?: string;
+ disabledTooltipBody?: string;
+}
+
+const EUI_TEXT_STYLES = css`
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+`;
+
+export const LibraryToggleRow: React.FC = ({
+ name,
+ description,
+ isActive,
+ onToggle,
+ isMutating,
+ isDisabled = false,
+ disabledBadgeLabel,
+ disabledTooltipTitle,
+ disabledTooltipBody,
+}) => {
+ const { euiTheme } = useEuiTheme();
+
+ return (
+
+
+
+
+
+ {name}
+
+
+ {isDisabled && (
+
+
+
+ )}
+
+
+ {description}
+
+
+
+ {isDisabled ? (
+
+ {disabledTooltipTitle && (
+
+ {disabledTooltipTitle}
+
+ )}
+ {disabledTooltipTitle && disabledTooltipBody && }
+ {disabledTooltipBody && {disabledTooltipBody}
}
+ >
+ ) : undefined
+ }
+ >
+
+ {disabledBadgeLabel ?? 'Auto-included'}
+
+
+ ) : (
+ onToggle(e.target.checked)}
+ disabled={isMutating}
+ compressed
+ />
+ )}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/page_wrapper.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/page_wrapper.tsx
new file mode 100644
index 0000000000000..d3f2125531fe9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/page_wrapper.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 { css } from '@emotion/react';
+import { CONTAINER_WIDTH } from './constants';
+
+export const PageWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/render_skill_content_read_only.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/render_skill_content_read_only.tsx
new file mode 100644
index 0000000000000..68119eb7229a1
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/render_skill_content_read_only.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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, { useState } from 'react';
+import {
+ EuiButtonGroup,
+ EuiCodeBlock,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiMarkdownFormat,
+ EuiPanel,
+} from '@elastic/eui';
+import { labels } from '../../../utils/i18n';
+
+const viewToggleOptions = [
+ {
+ id: 'rendered',
+ label: labels.agentSkills.instructionsViewRenderedLabel,
+ iconType: 'eye',
+ },
+ {
+ id: 'raw',
+ label: labels.agentSkills.instructionsViewRawLabel,
+ iconType: 'code',
+ },
+];
+
+interface RenderSkillContentReadOnlyProps {
+ content: string;
+}
+
+export const RenderSkillContentReadOnly: React.FC = ({
+ content,
+}) => {
+ const [showRaw, setShowRaw] = useState(false);
+
+ return (
+
+
+
+
+
+ setShowRaw(id === 'raw')}
+ isIconOnly
+ buttonSize="compressed"
+ />
+
+
+
+
+ {showRaw ? (
+
+ {content}
+
+ ) : (
+ {content}
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/styles.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/styles.ts
new file mode 100644
index 0000000000000..142aa4a75c60d
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/common/styles.ts
@@ -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 { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { SEARCH_LIST_WIDTH } from './constants';
+
+export const useListDetailPageStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ return useMemo(
+ () => ({
+ header: css`
+ padding: ${euiTheme.size.l};
+ flex-shrink: 0;
+ `,
+ body: css`
+ flex: 1;
+ overflow: hidden;
+ padding: 0 ${euiTheme.size.l};
+ `,
+ searchColumn: css`
+ width: ${SEARCH_LIST_WIDTH};
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ `,
+ searchInputWrapper: css`
+ padding: 0 ${euiTheme.size.m} ${euiTheme.size.s} 0;
+ flex-shrink: 0;
+ `,
+ scrollableList: css`
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 ${euiTheme.size.m} ${euiTheme.size.s} 0;
+ `,
+ detailPanelWrapper: css`
+ overflow: hidden;
+ `,
+ noSelectionPlaceholder: css`
+ height: 100%;
+ `,
+ loadingSpinner: css`
+ padding: ${euiTheme.size.xxl};
+ `,
+ }),
+ [euiTheme]
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/edit/agent_form.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/edit/agent_form.tsx
index d4422634204ae..241c259b5e8ef 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/edit/agent_form.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/edit/agent_form.tsx
@@ -215,7 +215,7 @@ export const AgentForm: React.FC = ({ editingAgentId, onDelete }
buttonId: BUTTON_IDS.SAVE_AND_CHAT,
navigateToListView: false,
});
- deferNavigateToAgentBuilderUrl(appPaths.chat.newWithAgent({ agentId: data.id }));
+ deferNavigateToAgentBuilderUrl(appPaths.agent.conversations.new({ agentId: data.id }));
},
[deferNavigateToAgentBuilderUrl, handleSave]
);
@@ -431,7 +431,7 @@ export const AgentForm: React.FC = ({ editingAgentId, onDelete }
- navigateToAgentBuilderUrl(appPaths.chat.newWithAgent({ agentId: editingAgentId }))
+ navigateToAgentBuilderUrl(appPaths.agent.conversations.new({ agentId: editingAgentId }))
}
>
{i18n.translate('xpack.agentBuilder.agents.form.chatButton', {
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents.tsx
index 404896f0edeee..b67035b57e26e 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
-import { EuiButton, EuiButtonEmpty, EuiLink, EuiText, useEuiTheme } from '@elastic/eui';
+import { EuiButton, EuiLink, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useAgentBuilderServices } from '../../../hooks/use_agent_builder_service';
@@ -17,24 +17,10 @@ import { useNavigation } from '../../../hooks/use_navigation';
import { appPaths } from '../../../utils/app_paths';
import { DeleteAgentProvider } from '../../../context/delete_agent_context';
import { useUiPrivileges } from '../../../hooks/use_ui_privileges';
-import { useExperimentalFeatures } from '../../../hooks/use_experimental_features';
-
-const manageToolsLabel = i18n.translate('xpack.agentBuilder.agents.manageToolsLabel', {
- defaultMessage: 'Manage tools',
-});
-
-const manageSkillsLabel = i18n.translate('xpack.agentBuilder.agents.manageSkillsLabel', {
- defaultMessage: 'Manage skills',
-});
-
-const managePluginsLabel = i18n.translate('xpack.agentBuilder.agents.managePluginsLabel', {
- defaultMessage: 'Manage plugins',
-});
export const AgentBuilderAgents = () => {
const { euiTheme } = useEuiTheme();
const { manageAgents } = useUiPrivileges();
- const isExperimentalFeaturesEnabled = useExperimentalFeatures();
const { docLinksService } = useAgentBuilderServices();
const headerStyles = css`
background-color: ${euiTheme.colors.backgroundBasePlain};
@@ -56,25 +42,6 @@ export const AgentBuilderAgents = () => {
})}
),
-
- {manageToolsLabel}
- ,
- ...(isExperimentalFeaturesEnabled
- ? [
-
- {manageSkillsLabel}
- ,
-
- {managePluginsLabel}
- ,
- ]
- : []),
];
return (
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents_list.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents_list.tsx
index 2196d31f15bec..5340ede0a64a9 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents_list.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/list/agents_list.tsx
@@ -203,7 +203,7 @@ export const AgentsList: React.FC = () => {
isPrimary: true,
showOnHover: true,
href: (agent) =>
- createAgentBuilderUrl(appPaths.chat.new, { [searchParamNames.agentId]: agent.id }),
+ createAgentBuilderUrl(appPaths.agent.conversations.new({ agentId: agent.id })),
},
{
type: 'icon',
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/agent_header.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/agent_header.tsx
new file mode 100644
index 0000000000000..0bc2ec1e62e6f
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/agent_header.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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 {
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiCopy,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import type { AgentDefinition } from '@kbn/agent-builder-common';
+import { labels } from '../../../utils/i18n';
+import { AgentAvatar } from '../../common/agent_avatar';
+import { AgentVisibilityBadge } from '../list/agent_visibility_badge';
+
+const { agentOverview: overviewLabels } = labels;
+
+export interface AgentHeaderProps {
+ agent: AgentDefinition;
+ docsUrl?: string;
+ canEditAgent: boolean;
+ onEditDetails: () => void;
+}
+
+export const AgentHeader: React.FC = ({
+ agent,
+ docsUrl,
+ canEditAgent,
+ onEditDetails,
+}) => (
+ <>
+
+
+
+
+
+
+
+ {agent.name}
+
+
+ {agent.created_by?.username && (
+
+ {overviewLabels.byAuthor(agent.created_by.username)}
+
+ )}
+
+ {(copy) => (
+
+ {overviewLabels.agentId(agent.id)}
+
+ )}
+
+
+
+
+
+
+
+ {docsUrl && (
+
+ {overviewLabels.docsLink}
+
+ )}
+ {canEditAgent && (
+
+ {overviewLabels.editDetailsButton}
+
+ )}
+
+
+
+
+
+
+ {agent.description}
+
+
+ {agent.labels && agent.labels.length > 0 && (
+ <>
+
+
+ {agent.labels.map((label) => (
+
+ {label}
+
+ ))}
+
+ >
+ )}
+ >
+);
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/agent_overview.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/agent_overview.tsx
new file mode 100644
index 0000000000000..093c295abc94a
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/agent_overview.tsx
@@ -0,0 +1,273 @@
+/*
+ * 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, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+import {
+ EuiFlexGroup,
+ EuiHorizontalRule,
+ EuiLoadingSpinner,
+ EuiSpacer,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { hasAgentWriteAccess } from '@kbn/agent-builder-common';
+import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids';
+import { useMutation, useQueryClient } from '@kbn/react-query';
+import { useAgentBuilderAgentById } from '../../../hooks/agents/use_agent_by_id';
+import { useSkillsService } from '../../../hooks/skills/use_skills';
+import { usePluginsService } from '../../../hooks/plugins/use_plugins';
+import { useAgentBuilderServices } from '../../../hooks/use_agent_builder_service';
+import { useExperimentalFeatures } from '../../../hooks/use_experimental_features';
+import { useKibana } from '../../../hooks/use_kibana';
+import { useUiPrivileges } from '../../../hooks/use_ui_privileges';
+import { useHasConnectorsAllPrivileges } from '../../../hooks/use_has_connectors_all_privileges';
+import { useCurrentUser } from '../../../hooks/agents/use_current_user';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { useToasts } from '../../../hooks/use_toasts';
+import { queryKeys } from '../../../query_keys';
+import { appPaths } from '../../../utils/app_paths';
+import { labels } from '../../../utils/i18n';
+import { storageKeys } from '../../../storage_keys';
+import { AgentHeader } from './agent_header';
+import { CapabilitiesSection } from './capabilities_section';
+import { EditDetailsFlyout } from './edit_details_flyout';
+import { SettingsSection } from './settings_section';
+import { TurnOffCapabilitiesModal } from './turn_off_capabilities_modal';
+
+const { agentOverview: overviewLabels } = labels;
+
+export const AgentOverview: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const { euiTheme } = useEuiTheme();
+ const { agentService, docLinksService } = useAgentBuilderServices();
+ const { addSuccessToast, addErrorToast } = useToasts();
+ const queryClient = useQueryClient();
+ const { navigateToAgentBuilderUrl } = useNavigation();
+
+ const isExperimentalFeaturesEnabled = useExperimentalFeatures();
+ const {
+ services: { uiSettings },
+ } = useKibana();
+ const isConnectorsEnabled = uiSettings.get(
+ AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID,
+ false
+ );
+
+ const { manageAgents, isAdmin } = useUiPrivileges();
+ const hasConnectorsPrivileges = useHasConnectorsAllPrivileges();
+ const { currentUser } = useCurrentUser({ enabled: isExperimentalFeaturesEnabled });
+
+ const { agent, isLoading } = useAgentBuilderAgentById(agentId);
+ const { skills: allSkills } = useSkillsService();
+ const { plugins: allPlugins } = usePluginsService();
+
+ const [isEditFlyoutOpen, setIsEditFlyoutOpen] = useState(false);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [instructions, setInstructions] = useState(undefined);
+
+ const [warningDismissed, setWarningDismissed] = useLocalStorage(
+ storageKeys.autoIncludeWarningDismissed,
+ false
+ );
+
+ const canEditAgent = useMemo(() => {
+ if (!manageAgents || !agent) return false;
+ if (!isExperimentalFeaturesEnabled) return true;
+ return hasAgentWriteAccess({
+ visibility: agent.visibility,
+ owner: agent.created_by,
+ currentUser: currentUser ?? undefined,
+ isAdmin,
+ });
+ }, [manageAgents, agent, isExperimentalFeaturesEnabled, currentUser, isAdmin]);
+
+ const currentInstructions = instructions ?? agent?.configuration?.instructions ?? '';
+ const enableElasticCapabilities = agent?.configuration?.enable_elastic_capabilities ?? false;
+
+ const agentSkillIdSet = useMemo(
+ () => new Set(agent?.configuration?.skill_ids ?? []),
+ [agent?.configuration?.skill_ids]
+ );
+
+ const builtinSkills = useMemo(() => allSkills.filter((s) => s.readonly), [allSkills]);
+
+ const skillsCount = useMemo(() => {
+ const explicitCount = agent?.configuration?.skill_ids?.length ?? 0;
+ if (!enableElasticCapabilities) return explicitCount;
+ const builtinNotExplicit = builtinSkills.filter((s) => !agentSkillIdSet.has(s.id)).length;
+ return explicitCount + builtinNotExplicit;
+ }, [agent?.configuration?.skill_ids, enableElasticCapabilities, builtinSkills, agentSkillIdSet]);
+
+ const agentPluginIdSet = useMemo(
+ () => new Set(agent?.configuration?.plugin_ids ?? []),
+ [agent?.configuration?.plugin_ids]
+ );
+
+ const builtinPlugins = useMemo(() => allPlugins.filter((p) => p.readonly), [allPlugins]);
+
+ const pluginsCount = useMemo(() => {
+ const explicitCount = agent?.configuration?.plugin_ids?.length ?? 0;
+ if (!enableElasticCapabilities) return explicitCount;
+ const builtinNotExplicit = builtinPlugins.filter((p) => !agentPluginIdSet.has(p.id)).length;
+ return explicitCount + builtinNotExplicit;
+ }, [
+ agent?.configuration?.plugin_ids,
+ enableElasticCapabilities,
+ builtinPlugins,
+ agentPluginIdSet,
+ ]);
+ const connectorsCount = 0;
+
+ const updateAgentMutation = useMutation({
+ mutationFn: (data: Record) => agentService.update(agentId!, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agentProfiles.byId(agentId) });
+ },
+ });
+
+ const handleToggleAutoInclude = useCallback(
+ (checked: boolean) => {
+ if (!checked && !warningDismissed) {
+ setIsModalVisible(true);
+ return;
+ }
+ updateAgentMutation.mutate(
+ { configuration: { enable_elastic_capabilities: checked } },
+ {
+ onSuccess: () => {
+ addSuccessToast({
+ title: checked
+ ? overviewLabels.autoIncludeEnabledToast
+ : overviewLabels.autoIncludeDisabledToast,
+ });
+ },
+ onError: () => {
+ addErrorToast({ title: overviewLabels.autoIncludeErrorToast });
+ },
+ }
+ );
+ },
+ [warningDismissed, updateAgentMutation, addSuccessToast, addErrorToast]
+ );
+
+ const handleConfirmTurnOff = useCallback(
+ (dontShowAgain: boolean) => {
+ if (dontShowAgain) {
+ setWarningDismissed(true);
+ }
+ setIsModalVisible(false);
+ updateAgentMutation.mutate(
+ { configuration: { enable_elastic_capabilities: false } },
+ {
+ onSuccess: () => {
+ addSuccessToast({ title: overviewLabels.autoIncludeDisabledToast });
+ },
+ onError: () => {
+ addErrorToast({ title: overviewLabels.autoIncludeErrorToast });
+ },
+ }
+ );
+ },
+ [updateAgentMutation, setWarningDismissed, addSuccessToast, addErrorToast]
+ );
+
+ const handleSaveInstructions = useCallback(() => {
+ updateAgentMutation.mutate(
+ { configuration: { instructions: currentInstructions } },
+ {
+ onSuccess: () => {
+ addSuccessToast({ title: overviewLabels.instructionsSavedToast });
+ },
+ onError: () => {
+ addErrorToast({ title: overviewLabels.instructionsErrorToast });
+ },
+ }
+ );
+ }, [updateAgentMutation, currentInstructions, addSuccessToast, addErrorToast]);
+
+ if (isLoading || !agent) {
+ return (
+
+
+
+ );
+ }
+
+ const containerStyles = css`
+ padding: ${euiTheme.size.l} ${euiTheme.size.xl};
+ overflow-y: auto;
+ height: 100%;
+ `;
+
+ return (
+
+
setIsEditFlyoutOpen(true)}
+ />
+
+
+
+
+
+
+ navigateToAgentBuilderUrl(appPaths.agent.skills({ agentId: agentId! }))
+ }
+ onNavigateToPlugins={() =>
+ navigateToAgentBuilderUrl(appPaths.agent.plugins({ agentId: agentId! }))
+ }
+ onNavigateToConnectors={() =>
+ navigateToAgentBuilderUrl(appPaths.agent.connectors({ agentId: agentId! }))
+ }
+ />
+
+
+
+
+
+
+
+ {isEditFlyoutOpen && agent && (
+ setIsEditFlyoutOpen(false)} />
+ )}
+
+ {isModalVisible && (
+ setIsModalVisible(false)}
+ />
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/capabilities_section.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/capabilities_section.tsx
new file mode 100644
index 0000000000000..e00614618fb34
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/capabilities_section.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { labels } from '../../../utils/i18n';
+import { CapabilityRow } from './capability_row';
+
+const { agentOverview: overviewLabels } = labels;
+
+export interface CapabilitiesSectionProps {
+ skillsCount: number;
+ pluginsCount: number;
+ connectorsCount: number;
+ enableElasticCapabilities: boolean;
+ isExperimentalFeaturesEnabled: boolean;
+ isConnectorsEnabled: boolean;
+ hasConnectorsPrivileges: boolean;
+ onNavigateToSkills: () => void;
+ onNavigateToPlugins: () => void;
+ onNavigateToConnectors: () => void;
+}
+
+export const CapabilitiesSection: React.FC = ({
+ skillsCount,
+ pluginsCount,
+ connectorsCount,
+ enableElasticCapabilities,
+ isExperimentalFeaturesEnabled,
+ isConnectorsEnabled,
+ hasConnectorsPrivileges,
+ onNavigateToSkills,
+ onNavigateToPlugins,
+ onNavigateToConnectors,
+}) => (
+
+
+
+ {overviewLabels.capabilitiesTitle}
+
+
+
+ {overviewLabels.capabilitiesDescription}
+
+
+
+
+
+ {isExperimentalFeaturesEnabled && (
+
+ )}
+
+ {isExperimentalFeaturesEnabled && (
+
+ )}
+
+ {isConnectorsEnabled && (
+
+ )}
+
+
+
+);
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/capability_row.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/capability_row.tsx
new file mode 100644
index 0000000000000..987c050230e14
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/capability_row.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+
+export interface CapabilityRowProps {
+ count: number;
+ label: string;
+ description: string;
+ actionLabel: string;
+ onAction?: () => void;
+}
+
+export const CapabilityRow: React.FC = ({
+ count,
+ label,
+ description,
+ actionLabel,
+ onAction,
+}) => {
+ const { euiTheme } = useEuiTheme();
+
+ return (
+
+
+
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.capabilities.countLabel', {
+ defaultMessage: '{count} {label}',
+ values: { count, label },
+ })}
+
+
+
+
+ {description}
+
+
+
+
+ {onAction ? (
+
+ {actionLabel}
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/edit_details_flyout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/edit_details_flyout.tsx
new file mode 100644
index 0000000000000..583477bdd15ad
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/edit_details_flyout.tsx
@@ -0,0 +1,369 @@
+/*
+ * 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 {
+ EuiBadge,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiColorPicker,
+ EuiComboBox,
+ EuiFieldText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiFormRow,
+ EuiSpacer,
+ EuiText,
+ EuiTextArea,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import {
+ AgentVisibility,
+ VISIBILITY_ICON,
+ VISIBILITY_BADGE_COLOR,
+ type AgentDefinition,
+} from '@kbn/agent-builder-common';
+import { Controller, useForm } from 'react-hook-form';
+import { useMutation, useQueryClient } from '@kbn/react-query';
+import { useAgentBuilderServices } from '../../../hooks/use_agent_builder_service';
+import { useAgentLabels } from '../../../hooks/agents/use_agent_labels';
+import { useToasts } from '../../../hooks/use_toasts';
+import { queryKeys } from '../../../query_keys';
+import { isValidAgentAvatarColor } from '../../../utils/color';
+import { truncateAvatarSymbol } from '../edit/agent_form_validation';
+import { labels as sharedLabels } from '../../../utils/i18n';
+import { FLYOUT_WIDTH } from '../common/constants';
+
+interface EditDetailsFormData {
+ name: string;
+ description: string;
+ avatar_symbol: string;
+ avatar_color: string;
+ labels: string[];
+}
+
+interface EditDetailsFlyoutProps {
+ agent: AgentDefinition;
+ onClose: () => void;
+}
+
+export const EditDetailsFlyout: React.FC = ({ agent, onClose }) => {
+ const { agentService } = useAgentBuilderServices();
+ const { addSuccessToast, addErrorToast } = useToasts();
+ const queryClient = useQueryClient();
+ const { labels: existingLabels, isLoading: labelsLoading } = useAgentLabels();
+
+ const { control, handleSubmit, formState } = useForm({
+ defaultValues: {
+ name: agent.name,
+ description: agent.description,
+ avatar_symbol: agent.avatar_symbol ?? '',
+ avatar_color: agent.avatar_color ?? '',
+ labels: agent.labels ?? [],
+ },
+ mode: 'onBlur',
+ });
+
+ const updateMutation = useMutation({
+ mutationFn: (data: EditDetailsFormData) =>
+ agentService.update(agent.id, {
+ name: data.name,
+ description: data.description,
+ avatar_symbol: data.avatar_symbol || undefined,
+ avatar_color: data.avatar_color || undefined,
+ labels: data.labels,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agentProfiles.byId(agent.id) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.agentProfiles.all });
+ addSuccessToast({
+ title: i18n.translate('xpack.agentBuilder.overview.editDetails.successToast', {
+ defaultMessage: 'Agent details updated',
+ }),
+ });
+ onClose();
+ },
+ onError: () => {
+ addErrorToast({
+ title: i18n.translate('xpack.agentBuilder.overview.editDetails.errorToast', {
+ defaultMessage: 'Unable to update agent details',
+ }),
+ });
+ },
+ });
+
+ const isShared = (agent.visibility as AgentVisibility) === AgentVisibility.Shared;
+
+ return (
+
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.title', {
+ defaultMessage: 'Edit agent details',
+ })}
+
+
+
+
+
+ {isShared && (
+ <>
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.sharedWarningPrefix', {
+ defaultMessage: "You're editing a ",
+ })}
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.sharedWarningBadge', {
+ defaultMessage: 'Shared agent',
+ })}
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.sharedWarningSuffix', {
+ defaultMessage: '. Changes will affect all users.',
+ })}
+
+ }
+ data-test-subj="editDetailsSharedWarning"
+ />
+
+ >
+ )}
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.identificationTitle', {
+ defaultMessage: 'Identification',
+ })}
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.identificationDescription', {
+ defaultMessage: 'Define how this agent is named and described.',
+ })}
+
+
+
+
+ (
+
+ )}
+ />
+
+
+
+ (
+
+ )}
+ />
+
+
+
+
+
+
+
+ {sharedLabels.common.optional}
+
+ }
+ isInvalid={!!formState.errors.avatar_symbol}
+ error={formState.errors.avatar_symbol?.message}
+ >
+ (
+ rest.onChange(truncateAvatarSymbol(e.target.value))}
+ inputRef={ref}
+ placeholder={i18n.translate(
+ 'xpack.agentBuilder.overview.editDetails.avatarSymbolPlaceholder',
+ { defaultMessage: 'Paste an emoji or use a two letter abbreviation' }
+ )}
+ data-test-subj="editDetailsAvatarSymbolInput"
+ />
+ )}
+ />
+
+
+
+
+ {sharedLabels.common.optional}
+
+ }
+ isInvalid={!!formState.errors.avatar_color}
+ error={formState.errors.avatar_color?.message}
+ >
+ {
+ if (!value) return true;
+ return (
+ isValidAgentAvatarColor(value) ||
+ i18n.translate('xpack.agentBuilder.overview.editDetails.avatarColorInvalid', {
+ defaultMessage: 'Enter a color hex code',
+ })
+ );
+ },
+ }}
+ render={({ field: { onChange, value } }) => (
+
+ )}
+ />
+
+
+
+
+
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.tagsTitle', {
+ defaultMessage: 'Tags',
+ })}
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.tagsDescription', {
+ defaultMessage: 'Add labels to organize and quickly find this agent.',
+ })}
+
+
+
+
+ (
+ ({ label: l }))}
+ options={existingLabels.map((label) => ({ label }))}
+ onCreateOption={(searchValue: string) => {
+ const newLabel = searchValue.trim();
+ if (!newLabel) return;
+ field.onChange(Array.from(new Set([...(field.value || []), newLabel])));
+ }}
+ onChange={(options) => field.onChange(options.map((o) => o.label))}
+ isLoading={labelsLoading}
+ isClearable
+ data-test-subj="editDetailsTagsComboBox"
+ />
+ )}
+ />
+
+
+
+
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.cancelButton', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+
+ updateMutation.mutate(data))}
+ isLoading={updateMutation.isLoading}
+ isDisabled={!formState.isDirty}
+ data-test-subj="editDetailsSaveButton"
+ >
+ {i18n.translate('xpack.agentBuilder.overview.editDetails.saveButton', {
+ defaultMessage: 'Save',
+ })}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/settings_section.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/settings_section.tsx
new file mode 100644
index 0000000000000..50e6a77092ca9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/settings_section.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiSwitch,
+ EuiText,
+ EuiTextArea,
+ EuiTitle,
+} from '@elastic/eui';
+import { labels } from '../../../utils/i18n';
+
+const { agentOverview: overviewLabels } = labels;
+
+export interface SettingsSectionProps {
+ enableElasticCapabilities: boolean;
+ currentInstructions: string;
+ canEditAgent: boolean;
+ isLoading: boolean;
+ onToggleAutoInclude: (checked: boolean) => void;
+ onInstructionsChange: (value: string) => void;
+ onSaveInstructions: () => void;
+}
+
+export const SettingsSection: React.FC = ({
+ enableElasticCapabilities,
+ currentInstructions,
+ canEditAgent,
+ isLoading,
+ onToggleAutoInclude,
+ onInstructionsChange,
+ onSaveInstructions,
+}) => (
+
+
+
+ {overviewLabels.settingsTitle}
+
+
+
+ {overviewLabels.settingsDescription}
+
+
+
+
+
+
+
+ {overviewLabels.autoIncludeTitle}
+
+
+
+ {overviewLabels.autoIncludeDescription}
+
+
+
+ onToggleAutoInclude(e.target.checked)}
+ disabled={!canEditAgent || isLoading}
+ data-test-subj="agentOverviewAutoIncludeSwitch"
+ />
+
+
+
+
+
+
+ {overviewLabels.instructionsTitle}
+
+
+
+ {overviewLabels.instructionsDescription}
+
+
+ onInstructionsChange(e.target.value)}
+ disabled={!canEditAgent}
+ data-test-subj="agentOverviewInstructionsInput"
+ />
+
+ {canEditAgent && (
+
+
+ {overviewLabels.saveInstructionsButton}
+
+
+ )}
+
+
+);
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/turn_off_capabilities_modal.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/turn_off_capabilities_modal.tsx
new file mode 100644
index 0000000000000..3488f56b64286
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/overview/turn_off_capabilities_modal.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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, { useState } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiCheckbox,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiText,
+ useGeneratedHtmlId,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+interface TurnOffCapabilitiesModalProps {
+ onConfirm: (dontShowAgain: boolean) => void;
+ onCancel: () => void;
+}
+
+export const TurnOffCapabilitiesModal: React.FC = ({
+ onConfirm,
+ onCancel,
+}) => {
+ const [dontShowAgain, setDontShowAgain] = useState(false);
+ const modalTitleId = useGeneratedHtmlId();
+
+ return (
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.turnOffCapabilities.title', {
+ defaultMessage: 'Turn off auto-included capabilities?',
+ })}
+
+
+
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.turnOffCapabilities.body', {
+ defaultMessage:
+ 'This will remove all built-in skills, plugins, and tools that were added automatically. You\u2019ll need to add and manage those capabilities manually going forward. You can turn this back on at any time.',
+ })}
+
+
+
+
+
+
+
+ setDontShowAgain(e.target.checked)}
+ data-test-subj="turnOffCapabilitiesDontShowAgainCheckbox"
+ />
+
+
+
+
+ {i18n.translate('xpack.agentBuilder.overview.turnOffCapabilities.cancel', {
+ defaultMessage: 'Cancel',
+ })}
+
+ onConfirm(dontShowAgain)}
+ data-test-subj="turnOffCapabilitiesConfirmButton"
+ >
+ {i18n.translate('xpack.agentBuilder.overview.turnOffCapabilities.confirm', {
+ defaultMessage: 'Turn off',
+ })}
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/agent_plugins.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/agent_plugins.tsx
new file mode 100644
index 0000000000000..a5ee35e005fd9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/agent_plugins.tsx
@@ -0,0 +1,374 @@
+/*
+ * 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, useState, useEffect, useRef, useCallback } from 'react';
+import { useParams } from 'react-router-dom';
+import {
+ EuiBadge,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiLoadingSpinner,
+ EuiPopover,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import type { PluginDefinition } from '@kbn/agent-builder-common';
+import { useMutation, useQueryClient } from '@kbn/react-query';
+import { labels } from '../../../utils/i18n';
+import { appPaths } from '../../../utils/app_paths';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { usePluginsService } from '../../../hooks/plugins/use_plugins';
+import { useAgentBuilderAgentById } from '../../../hooks/agents/use_agent_by_id';
+import { useAgentBuilderServices } from '../../../hooks/use_agent_builder_service';
+import { useToasts } from '../../../hooks/use_toasts';
+import { queryKeys } from '../../../query_keys';
+import { useFlyoutState } from '../../../hooks/use_flyout_state';
+import { ActiveItemRow } from '../common/active_item_row';
+import { PluginLibraryPanel } from './plugin_library_panel';
+import { PluginDetailPanel } from './plugin_detail_panel';
+import { InstallPluginFlyout } from './install_plugin_flyout';
+import { PageWrapper } from '../common/page_wrapper';
+import { ICON_DIMENSIONS } from '../common/constants';
+import { useListDetailPageStyles } from '../common/styles';
+
+export const AgentPlugins: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const styles = useListDetailPageStyles();
+ const { createAgentBuilderUrl } = useNavigation();
+ const { agentService } = useAgentBuilderServices();
+ const { addSuccessToast, addErrorToast } = useToasts();
+ const queryClient = useQueryClient();
+
+ const { agent, isLoading: agentLoading } = useAgentBuilderAgentById(agentId);
+ const { plugins: allPlugins, isLoading: pluginsLoading } = usePluginsService();
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedPluginId, setSelectedPluginId] = useState(null);
+ const pendingSelectPluginIdRef = useRef(null);
+ const [isAddMenuOpen, setIsAddMenuOpen] = useState(false);
+ const [mutatingPluginId, setMutatingPluginId] = useState(null);
+ const {
+ isOpen: isLibraryOpen,
+ openFlyout: openLibrary,
+ closeFlyout: closeLibrary,
+ } = useFlyoutState();
+ const {
+ isOpen: isInstallFlyoutOpen,
+ openFlyout: openInstallFlyout,
+ closeFlyout: closeInstallFlyout,
+ } = useFlyoutState();
+
+ const handleOpenLibrary = useCallback(() => {
+ setIsAddMenuOpen(false);
+ openLibrary();
+ }, [openLibrary]);
+
+ const handleOpenInstallFlyout = useCallback(() => {
+ setIsAddMenuOpen(false);
+ openInstallFlyout();
+ }, [openInstallFlyout]);
+
+ const agentPluginIds = useMemo(
+ () => agent?.configuration?.plugin_ids,
+ [agent?.configuration?.plugin_ids]
+ );
+
+ const agentPluginIdSet = useMemo(
+ () => (agentPluginIds ? new Set(agentPluginIds) : undefined),
+ [agentPluginIds]
+ );
+
+ const enableElasticCapabilities = agent?.configuration?.enable_elastic_capabilities ?? false;
+
+ const builtinPlugins = useMemo(() => allPlugins.filter((p) => p.readonly), [allPlugins]);
+
+ const builtinPluginIdSet = useMemo(
+ () => new Set(builtinPlugins.map((p) => p.id)),
+ [builtinPlugins]
+ );
+
+ const activePlugins = useMemo(() => {
+ if (!agentPluginIdSet) return [];
+ if (enableElasticCapabilities) {
+ const explicitPlugins = allPlugins.filter((p) => agentPluginIdSet.has(p.id));
+ const builtinNotExplicit = builtinPlugins.filter((p) => !agentPluginIdSet.has(p.id));
+ return [...explicitPlugins, ...builtinNotExplicit];
+ }
+ return allPlugins.filter((p) => agentPluginIdSet.has(p.id));
+ }, [allPlugins, agentPluginIdSet, enableElasticCapabilities, builtinPlugins]);
+
+ useEffect(() => {
+ if (pendingSelectPluginIdRef.current) {
+ const pendingInActive = activePlugins.some((p) => p.id === pendingSelectPluginIdRef.current);
+ if (pendingInActive) {
+ setSelectedPluginId(pendingSelectPluginIdRef.current);
+ pendingSelectPluginIdRef.current = null;
+ return;
+ }
+ }
+
+ if (!selectedPluginId) {
+ if (activePlugins.length > 0) {
+ setSelectedPluginId(activePlugins[0].id);
+ }
+ } else {
+ const stillActive = activePlugins.some((p) => p.id === selectedPluginId);
+ if (!stillActive) {
+ setSelectedPluginId(activePlugins[0]?.id ?? null);
+ }
+ }
+ }, [activePlugins, selectedPluginId]);
+
+ const filteredActivePlugins = useMemo(() => {
+ if (!searchQuery.trim()) return activePlugins;
+ const lower = searchQuery.toLowerCase();
+ return activePlugins.filter(
+ (p) => p.name.toLowerCase().includes(lower) || p.description.toLowerCase().includes(lower)
+ );
+ }, [activePlugins, searchQuery]);
+
+ const updatePluginsMutation = useMutation({
+ mutationFn: (newPluginIds: string[]) => {
+ return agentService.update(agentId!, { configuration: { plugin_ids: newPluginIds } });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agentProfiles.byId(agentId) });
+ },
+ onError: () => {
+ addErrorToast({ title: labels.agentPlugins.updatePluginsErrorToast });
+ },
+ });
+
+ const handleAddPlugin = useCallback(
+ async (
+ plugin: PluginDefinition,
+ { selectOnSuccess = false }: { selectOnSuccess?: boolean } = {}
+ ) => {
+ const currentIds = agentPluginIds ?? [];
+ if (currentIds.includes(plugin.id)) return;
+ const newIds = [...currentIds, plugin.id];
+ setMutatingPluginId(plugin.id);
+ try {
+ await updatePluginsMutation.mutateAsync(newIds);
+ if (selectOnSuccess) {
+ pendingSelectPluginIdRef.current = plugin.id;
+ }
+ addSuccessToast({ title: labels.agentPlugins.addPluginSuccessToast(plugin.name) });
+ } finally {
+ setMutatingPluginId(null);
+ }
+ },
+ [agentPluginIds, updatePluginsMutation, addSuccessToast]
+ );
+
+ const handleRemovePlugin = useCallback(
+ (plugin: PluginDefinition) => {
+ const currentIds = agentPluginIds ?? [];
+ const newIds = currentIds.filter((id) => id !== plugin.id);
+ setMutatingPluginId(plugin.id);
+ updatePluginsMutation.mutate(newIds, {
+ onSuccess: () => {
+ setSelectedPluginId(null);
+ addSuccessToast({ title: labels.agentPlugins.removePluginSuccessToast(plugin.name) });
+ },
+ onSettled: () => setMutatingPluginId(null),
+ });
+ },
+ [agentPluginIds, updatePluginsMutation, addSuccessToast]
+ );
+
+ const handleTogglePlugin = useCallback(
+ (plugin: PluginDefinition, isActive: boolean) => {
+ if (enableElasticCapabilities && plugin.readonly) return;
+ if (isActive) {
+ handleAddPlugin(plugin);
+ } else {
+ handleRemovePlugin(plugin);
+ }
+ },
+ [handleAddPlugin, handleRemovePlugin, enableElasticCapabilities]
+ );
+
+ const handleRemoveSelectedPlugin = useCallback(() => {
+ if (!selectedPluginId) return;
+ const plugin = activePlugins.find((p) => p.id === selectedPluginId);
+ if (plugin) {
+ if (enableElasticCapabilities && plugin.readonly) return;
+ handleRemovePlugin(plugin);
+ }
+ }, [selectedPluginId, activePlugins, handleRemovePlugin, enableElasticCapabilities]);
+
+ const libraryActivePluginIdSet = useMemo(() => {
+ if (!agentPluginIdSet) return new Set();
+ if (enableElasticCapabilities) return new Set([...agentPluginIdSet, ...builtinPluginIdSet]);
+ return agentPluginIdSet;
+ }, [agentPluginIdSet, enableElasticCapabilities, builtinPluginIdSet]);
+
+ const isLoading = agentLoading || pluginsLoading;
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {labels.plugins.title}
+
+
+
+
+
+
+
+
+ {labels.agentPlugins.manageAllPlugins}
+
+
+
+ setIsAddMenuOpen((prev) => !prev)}
+ >
+ {labels.agentPlugins.installPluginButton}
+
+ }
+ isOpen={isAddMenuOpen}
+ closePopover={() => setIsAddMenuOpen(false)}
+ anchorPosition="downLeft"
+ panelPaddingSize="none"
+ >
+
+ {labels.agentPlugins.fromUrlOrZipMenuItem}
+ ,
+
+ {labels.agentPlugins.fromLibraryMenuItem}
+ ,
+ ]}
+ />
+
+
+
+
+
+
+
+
+ {labels.agentPlugins.pageDescription}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ incremental
+ fullWidth
+ />
+
+
+
+ {filteredActivePlugins.length === 0 ? (
+
+
+ {searchQuery.trim()
+ ? labels.agentPlugins.noActivePluginsMatchMessage
+ : labels.agentPlugins.noActivePluginsMessage}
+
+
+ ) : (
+ filteredActivePlugins.map((plugin) => (
+
setSelectedPluginId(plugin.id)}
+ onRemove={() => handleRemovePlugin(plugin)}
+ isRemoving={updatePluginsMutation.isLoading}
+ removeAriaLabel={labels.agentPlugins.removePluginAriaLabel}
+ readOnlyContent={
+ enableElasticCapabilities && plugin.readonly ? (
+ {labels.agentPlugins.autoBadge}
+ ) : undefined
+ }
+ />
+ ))
+ )}
+
+
+
+
+ {selectedPluginId ? (
+
+ ) : (
+
+
+ {labels.agentPlugins.noPluginSelectedMessage}
+
+
+ )}
+
+
+
+ {isLibraryOpen && (
+
+ )}
+
+ {isInstallFlyoutOpen && (
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/install_plugin_flyout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/install_plugin_flyout.tsx
new file mode 100644
index 0000000000000..cc899b626273c
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/install_plugin_flyout.tsx
@@ -0,0 +1,198 @@
+/*
+ * 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, useState } from 'react';
+import type { EuiFilePickerProps } from '@elastic/eui';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFieldText,
+ EuiFilePicker,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiForm,
+ EuiFormRow,
+ EuiTab,
+ EuiTabs,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import type { PluginDefinition } from '@kbn/agent-builder-common';
+import { labels } from '../../../utils/i18n';
+import {
+ useInstallPluginFromUrl,
+ useUploadPlugin,
+} from '../../../hooks/plugins/use_install_plugin';
+
+type InstallTab = 'url' | 'upload';
+
+interface InstallPluginFlyoutProps {
+ onClose: () => void;
+ /** Called after a plugin is successfully installed, receives the new plugin data.
+ * May return a promise — the flyout waits for it to settle before closing. */
+ onPluginInstalled?: (plugin: PluginDefinition) => void | Promise;
+}
+
+export const InstallPluginFlyout: React.FC = ({
+ onClose,
+ onPluginInstalled,
+}) => {
+ const [activeTab, setActiveTab] = useState('url');
+ const { euiTheme } = useEuiTheme();
+ const [url, setUrl] = useState('');
+ const [file, setFile] = useState(null);
+
+ const handleInstallSuccess = useCallback(
+ async (data: PluginDefinition) => {
+ try {
+ await onPluginInstalled?.(data);
+ } finally {
+ onClose();
+ }
+ },
+ [onPluginInstalled, onClose]
+ );
+
+ const { installFromUrl, isLoading: isUrlLoading } = useInstallPluginFromUrl({
+ onSuccess: handleInstallSuccess,
+ });
+
+ const { uploadPlugin, isLoading: isUploadLoading } = useUploadPlugin({
+ onSuccess: handleInstallSuccess,
+ });
+
+ const isLoading = isUrlLoading || isUploadLoading;
+
+ const handleUrlSubmit = useCallback(async () => {
+ if (!url.trim()) return;
+ await installFromUrl({ url: url.trim() });
+ }, [url, installFromUrl]);
+
+ const handleUploadSubmit = useCallback(async () => {
+ if (!file) return;
+ await uploadPlugin({ file });
+ }, [file, uploadPlugin]);
+
+ const handleInstall = useCallback(() => {
+ if (activeTab === 'url') {
+ handleUrlSubmit();
+ } else {
+ handleUploadSubmit();
+ }
+ }, [activeTab, handleUrlSubmit, handleUploadSubmit]);
+
+ const handleFileChange: EuiFilePickerProps['onChange'] = useCallback((files: FileList | null) => {
+ setFile(files && files.length > 0 ? files[0] : null);
+ }, []);
+
+ const isInstallDisabled = isLoading || (activeTab === 'url' ? !url.trim() : !file);
+
+ return (
+
+
+
+ {labels.agentPlugins.installPluginFlyoutTitle}
+
+
+
+
+ setActiveTab('url')}
+ disabled={isLoading}
+ >
+ {labels.agentPlugins.installPluginUrlTab}
+
+ setActiveTab('upload')}
+ disabled={isLoading}
+ >
+ {labels.agentPlugins.installPluginUploadTab}
+
+
+
+
+
+ {activeTab === 'url' ? (
+ {
+ e.preventDefault();
+ handleUrlSubmit();
+ }}
+ >
+
+ setUrl(e.target.value)}
+ placeholder={labels.plugins.urlFieldPlaceholder}
+ fullWidth
+ disabled={isLoading}
+ data-test-subj="agentBuilderInstallPluginUrlField"
+ />
+
+
+ ) : (
+ {
+ e.preventDefault();
+ handleUploadSubmit();
+ }}
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+ {labels.plugins.cancelButton}
+
+
+
+
+ {labels.plugins.installButton}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/plugin_detail_panel.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/plugin_detail_panel.tsx
new file mode 100644
index 0000000000000..5652b77534877
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/plugin_detail_panel.tsx
@@ -0,0 +1,204 @@
+/*
+ * 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, { useState } from 'react';
+import {
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiHorizontalRule,
+ EuiLink,
+ EuiLoadingSpinner,
+ EuiText,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { labels } from '../../../utils/i18n';
+import { usePlugin } from '../../../hooks/plugins/use_plugin';
+import { useSkill } from '../../../hooks/skills/use_skills';
+import { DetailRow } from '../common/detail_row';
+import { DetailPanelLayout } from '../common/detail_panel_layout';
+import { RenderSkillContentReadOnly } from '../common/render_skill_content_read_only';
+
+interface PluginDetailPanelProps {
+ pluginId: string;
+ onRemove: () => void;
+ isAuto?: boolean;
+}
+
+export const PluginDetailPanel: React.FC = ({
+ pluginId,
+ onRemove,
+ isAuto = false,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const { plugin, isLoading } = usePlugin({ pluginId });
+ const [selectedSkillId, setSelectedSkillId] = useState(null);
+
+ return (
+ <>
+
+
+
+ {labels.agentPlugins.skillsCountBadge(plugin?.skill_ids.length ?? 0)}
+
+
+
+ {plugin?.description || '\u2014'}
+
+ >
+ }
+ headerActions={(openConfirmRemove) =>
+ isAuto ? (
+ {labels.agentPlugins.autoIncludedBadgeLabel}
+ ) : (
+
+ {labels.agentPlugins.removePluginButtonLabel}
+
+ )
+ }
+ confirmRemove={{
+ title: labels.agentPlugins.removePluginConfirmTitle(plugin?.name ?? pluginId),
+ body: labels.agentPlugins.removePluginConfirmBody,
+ confirmButtonText: labels.agentPlugins.removePluginConfirmButton,
+ cancelButtonText: labels.agentPlugins.removePluginCancelButton,
+ onConfirm: onRemove,
+ }}
+ >
+
+
+ {plugin?.id}
+
+
+ {plugin?.source_url ? (
+
+ {plugin.source_url}
+
+ ) : (
+
+ {'\u2014'}
+
+ )}
+
+
+
+ {plugin?.skill_ids && plugin.skill_ids.length > 0 ? (
+
+ {plugin.skill_ids.map((skillId) => (
+
+ setSelectedSkillId(skillId)}>{skillId}
+
+ ))}
+
+ ) : (
+
+ {labels.plugins.noSkillsLabel}
+
+ )}
+
+
+
+
+ {selectedSkillId && (
+ setSelectedSkillId(null)}
+ />
+ )}
+ >
+ );
+};
+
+const SkillDetailFlyout: React.FC<{
+ skillId: string;
+ pluginName: string;
+ onClose: () => void;
+}> = ({ skillId, pluginName, onClose }) => {
+ const { skill, isLoading } = useSkill({ skillId });
+
+ return (
+
+
+
+
+
+ {skill?.name ?? skillId}
+
+
+
+
+ {labels.agentSkills.readOnlyBadge}
+
+
+
+
+ {labels.agentPlugins.skillDetailInstalledVia(pluginName)}
+
+
+
+ {isLoading ? (
+
+
+
+ ) : skill ? (
+
+
+
+ {labels.agentPlugins.pluginDetailNameLabel}
+
+ {skill.name}
+
+
+
+
+ {labels.agentPlugins.pluginDetailIdLabel}
+
+ {skill.id}
+
+
+
+
+ {labels.agentPlugins.pluginDetailDescriptionLabel}
+
+ {skill.description || '\u2014'}
+
+
+
+
+
+
+ ) : null}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/plugin_library_panel.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/plugin_library_panel.tsx
new file mode 100644
index 0000000000000..42bc172effa51
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/plugins/plugin_library_panel.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 type { PluginDefinition } from '@kbn/agent-builder-common';
+import { labels } from '../../../utils/i18n';
+import { appPaths } from '../../../utils/app_paths';
+import { LibraryPanel } from '../common/library_panel';
+import type { LibraryPanelLabels } from '../common/library_panel';
+
+const libraryLabels: LibraryPanelLabels = {
+ title: labels.agentPlugins.addPluginFromLibraryTitle,
+ manageLibraryLink: labels.agentPlugins.managePluginLibraryLink,
+ searchPlaceholder: labels.agentPlugins.searchAvailablePluginsPlaceholder,
+ availableSummary: labels.agentPlugins.availablePluginsSummary,
+ noMatchMessage: labels.agentPlugins.noAvailablePluginsMatchMessage,
+ noItemsMessage: labels.agentPlugins.noAvailablePluginsMessage,
+ disabledBadgeLabel: labels.agentPlugins.autoIncludedBadgeLabel,
+ disabledTooltipTitle: labels.agentPlugins.autoIncludedTooltipTitle,
+ disabledTooltipBody: labels.agentPlugins.autoIncludedTooltipBody,
+};
+
+const getPluginName = (plugin: PluginDefinition): string => plugin.name;
+
+interface PluginLibraryPanelProps {
+ onClose: () => void;
+ allPlugins: PluginDefinition[];
+ activePluginIdSet: Set;
+ onTogglePlugin: (plugin: PluginDefinition, isActive: boolean) => void;
+ mutatingPluginId: string | null;
+ autoPluginIdSet?: Set;
+}
+
+export const PluginLibraryPanel: React.FC = ({
+ onClose,
+ allPlugins,
+ activePluginIdSet,
+ onTogglePlugin,
+ mutatingPluginId,
+ autoPluginIdSet,
+}) => (
+
+ onClose={onClose}
+ allItems={allPlugins}
+ activeItemIdSet={activePluginIdSet}
+ onToggleItem={onTogglePlugin}
+ mutatingItemId={mutatingPluginId}
+ flyoutTitleId="pluginLibraryFlyoutTitle"
+ libraryLabels={libraryLabels}
+ manageLibraryPath={appPaths.plugins.list}
+ getItemName={getPluginName}
+ disabledItemIdSet={autoPluginIdSet}
+ />
+);
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/active_skill_row.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/active_skill_row.tsx
new file mode 100644
index 0000000000000..97aff8174a97f
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/active_skill_row.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 { EuiBadge } from '@elastic/eui';
+import type { PublicSkillSummary } from '@kbn/agent-builder-common';
+import { labels } from '../../../utils/i18n';
+import { ActiveItemRow } from '../common/active_item_row';
+
+export interface ActiveSkillRowProps {
+ skill: PublicSkillSummary;
+ isSelected: boolean;
+ onSelect: (skill: PublicSkillSummary) => void;
+ onRemove: (skill: PublicSkillSummary) => void;
+ isRemoving?: boolean;
+ readOnly?: boolean;
+}
+
+export const ActiveSkillRow: React.FC = ({
+ skill,
+ isSelected,
+ onSelect,
+ onRemove,
+ isRemoving = false,
+ readOnly = false,
+}) => {
+ return (
+ onSelect(skill)}
+ onRemove={() => onRemove(skill)}
+ isRemoving={isRemoving}
+ removeAriaLabel={labels.agentSkills.removeSkillAriaLabel}
+ readOnlyContent={
+ readOnly ? (
+ {labels.agentSkills.elasticCapabilitiesReadOnlyBadge}
+ ) : undefined
+ }
+ />
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/agent_skills.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/agent_skills.tsx
new file mode 100644
index 0000000000000..da5ed62084c00
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/agent_skills.tsx
@@ -0,0 +1,377 @@
+/*
+ * 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, useState, useEffect, useRef } from 'react';
+import { useParams } from 'react-router-dom';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiPopover,
+ EuiSpacer,
+ EuiText,
+ EuiIcon,
+ EuiTitle,
+} from '@elastic/eui';
+import type { PublicSkillDefinition, PublicSkillSummary } from '@kbn/agent-builder-common';
+import { useMutation, useQueryClient } from '@kbn/react-query';
+import { labels } from '../../../utils/i18n';
+import { appPaths } from '../../../utils/app_paths';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { useSkillsService } from '../../../hooks/skills/use_skills';
+import { useAgentBuilderAgentById } from '../../../hooks/agents/use_agent_by_id';
+import { useAgentBuilderServices } from '../../../hooks/use_agent_builder_service';
+import { useToasts } from '../../../hooks/use_toasts';
+import { queryKeys } from '../../../query_keys';
+import { useFlyoutState } from '../../../hooks/use_flyout_state';
+import { SkillLibraryPanel } from './skill_library_panel';
+import { ActiveSkillRow } from './active_skill_row';
+import { SkillDetailPanel } from './skill_detail_panel';
+import { SkillEditFlyout } from './skill_edit_flyout';
+import { SkillCreateFlyout } from './skill_create_flyout';
+import { PageWrapper } from '../common/page_wrapper';
+import { ICON_DIMENSIONS } from '../common/constants';
+import { useListDetailPageStyles } from '../common/styles';
+
+export const AgentSkills: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const styles = useListDetailPageStyles();
+ const { createAgentBuilderUrl } = useNavigation();
+ const { agentService } = useAgentBuilderServices();
+ const { addSuccessToast, addErrorToast } = useToasts();
+ const queryClient = useQueryClient();
+
+ const { agent, isLoading: agentLoading } = useAgentBuilderAgentById(agentId);
+ const { skills: allSkills, isLoading: skillsLoading } = useSkillsService();
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedSkillId, setSelectedSkillId] = useState(null);
+ const pendingSelectSkillIdRef = useRef(null);
+ const [editingSkillId, setEditingSkillId] = useState(null);
+ const [isCreateFlyoutOpen, setIsCreateFlyoutOpen] = useState(false);
+ const [isAddMenuOpen, setIsAddMenuOpen] = useState(false);
+ const [mutatingSkillId, setMutatingSkillId] = useState(null);
+ const {
+ isOpen: isLibraryOpen,
+ openFlyout: openLibrary,
+ closeFlyout: closeLibrary,
+ } = useFlyoutState();
+
+ const handleImportFromLibrary = () => {
+ setIsAddMenuOpen(false);
+ openLibrary();
+ };
+
+ const handleOpenCreateFlyout = () => {
+ setIsAddMenuOpen(false);
+ setIsCreateFlyoutOpen(true);
+ };
+
+ const agentSkillIds = useMemo(
+ () => agent?.configuration?.skill_ids,
+ [agent?.configuration?.skill_ids]
+ );
+
+ const agentSkillIdSet = useMemo(
+ () => (agentSkillIds ? new Set(agentSkillIds) : undefined),
+ [agentSkillIds]
+ );
+
+ const enableElasticCapabilities = agent?.configuration?.enable_elastic_capabilities ?? false;
+
+ const builtinSkills = useMemo(() => allSkills.filter((s) => s.readonly), [allSkills]);
+
+ const builtinSkillIdSet = useMemo(() => new Set(builtinSkills.map((s) => s.id)), [builtinSkills]);
+
+ const activeSkills = useMemo(() => {
+ if (!agentSkillIdSet) return allSkills;
+ if (enableElasticCapabilities) {
+ const explicitSkills = allSkills.filter((s) => agentSkillIdSet.has(s.id));
+ const builtinNotExplicit = builtinSkills.filter((s) => !agentSkillIdSet.has(s.id));
+ return [...explicitSkills, ...builtinNotExplicit];
+ }
+ return allSkills.filter((s) => agentSkillIdSet.has(s.id));
+ }, [allSkills, agentSkillIdSet, enableElasticCapabilities, builtinSkills]);
+
+ useEffect(() => {
+ if (pendingSelectSkillIdRef.current) {
+ const pendingInActive = activeSkills.some((s) => s.id === pendingSelectSkillIdRef.current);
+ if (pendingInActive) {
+ setSelectedSkillId(pendingSelectSkillIdRef.current);
+ pendingSelectSkillIdRef.current = null;
+ return;
+ }
+ }
+
+ if (!selectedSkillId) {
+ if (activeSkills.length > 0) {
+ setSelectedSkillId(activeSkills[0].id);
+ }
+ } else {
+ const stillActive = activeSkills.some((s) => s.id === selectedSkillId);
+ if (!stillActive) {
+ setSelectedSkillId(activeSkills[0]?.id ?? null);
+ }
+ }
+ }, [activeSkills, selectedSkillId]);
+
+ const filteredActiveSkills = useMemo(() => {
+ if (!searchQuery.trim()) return activeSkills;
+ const lower = searchQuery.toLowerCase();
+ return activeSkills.filter(
+ (s) => s.name.toLowerCase().includes(lower) || s.description.toLowerCase().includes(lower)
+ );
+ }, [activeSkills, searchQuery]);
+
+ const updateSkillsMutation = useMutation({
+ mutationFn: (newSkillIds: string[]) => {
+ return agentService.update(agentId!, { configuration: { skill_ids: newSkillIds } });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agentProfiles.byId(agentId) });
+ },
+ onError: () => {
+ addErrorToast({ title: labels.agentSkills.updateSkillsErrorToast });
+ },
+ });
+
+ const handleAddSkill = (
+ skill: PublicSkillSummary | PublicSkillDefinition,
+ { selectOnSuccess = false }: { selectOnSuccess?: boolean } = {}
+ ) => {
+ const currentIds = agentSkillIds ?? allSkills.map((s) => s.id);
+ if (currentIds.includes(skill.id)) return;
+ const newIds = [...currentIds, skill.id];
+ setMutatingSkillId(skill.id);
+ updateSkillsMutation.mutate(newIds, {
+ onSuccess: () => {
+ if (selectOnSuccess) {
+ pendingSelectSkillIdRef.current = skill.id;
+ }
+ addSuccessToast({ title: labels.agentSkills.addSkillSuccessToast(skill.name) });
+ },
+ onSettled: () => setMutatingSkillId(null),
+ });
+ };
+
+ const handleRemoveSkill = (skill: PublicSkillSummary) => {
+ const currentIds = agentSkillIds ?? allSkills.map((s) => s.id);
+ const newIds = currentIds.filter((id) => id !== skill.id);
+ setMutatingSkillId(skill.id);
+ updateSkillsMutation.mutate(newIds, {
+ onSuccess: () => {
+ setSelectedSkillId(null);
+ addSuccessToast({ title: labels.agentSkills.removeSkillSuccessToast(skill.name) });
+ },
+ onSettled: () => setMutatingSkillId(null),
+ });
+ };
+
+ const handleToggleSkill = (skill: PublicSkillSummary, isActive: boolean) => {
+ if (enableElasticCapabilities && skill.readonly) return;
+ if (isActive) {
+ handleAddSkill(skill);
+ } else {
+ handleRemoveSkill(skill);
+ }
+ };
+
+ const handleRemoveSelectedSkill = () => {
+ if (!selectedSkillId) return;
+ const skill = activeSkills.find((s) => s.id === selectedSkillId);
+ if (skill) {
+ if (enableElasticCapabilities && skill.readonly) return;
+ handleRemoveSkill(skill);
+ }
+ };
+
+ const libraryActiveSkillIdSet = useMemo(() => {
+ if (!agentSkillIdSet) return new Set(allSkills.map((s) => s.id));
+ if (enableElasticCapabilities) return new Set([...agentSkillIdSet, ...builtinSkillIdSet]);
+ return agentSkillIdSet;
+ }, [agentSkillIdSet, allSkills, enableElasticCapabilities, builtinSkillIdSet]);
+
+ const isLoading = agentLoading || skillsLoading;
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {labels.skills.title}
+
+
+
+
+
+
+
+
+ {labels.agentSkills.manageAllSkills}
+
+
+
+ setIsAddMenuOpen((prev) => !prev)}
+ >
+ {labels.agentSkills.addSkillButton}
+
+ }
+ isOpen={isAddMenuOpen}
+ closePopover={() => setIsAddMenuOpen(false)}
+ anchorPosition="downLeft"
+ panelPaddingSize="none"
+ >
+
+ {labels.agentSkills.importFromLibraryMenuItem}
+ ,
+
+ {labels.agentSkills.createSkillMenuItem}
+ ,
+ ]}
+ />
+
+
+
+
+
+
+
+
+ {labels.agentSkills.pageDescription}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ incremental
+ fullWidth
+ />
+
+
+
+ {filteredActiveSkills.length === 0 ? (
+
+
+ {searchQuery.trim()
+ ? labels.agentSkills.noActiveSkillsMatchMessage
+ : labels.agentSkills.noActiveSkillsMessage}
+
+
+ ) : (
+ filteredActiveSkills.map((skill) => (
+
setSelectedSkillId(s.id)}
+ onRemove={handleRemoveSkill}
+ isRemoving={mutatingSkillId === skill.id}
+ readOnly={enableElasticCapabilities && skill.readonly}
+ />
+ ))
+ )}
+
+
+
+
+ {selectedSkillId ? (
+ setEditingSkillId(selectedSkillId)}
+ onRemove={handleRemoveSelectedSkill}
+ isReadOnly={
+ enableElasticCapabilities &&
+ (activeSkills.find((s) => s.id === selectedSkillId)?.readonly ?? false)
+ }
+ />
+ ) : (
+
+
+ {labels.agentSkills.noSkillSelectedMessage}
+
+
+ )}
+
+
+
+ {isLibraryOpen && (
+
+ )}
+
+ {editingSkillId && (
+ setEditingSkillId(null)}
+ onSaved={() => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.skills.byId(editingSkillId) });
+ setSelectedSkillId(editingSkillId);
+ }}
+ />
+ )}
+
+ {isCreateFlyoutOpen && (
+ setIsCreateFlyoutOpen(false)}
+ onSkillCreated={(skill) => handleAddSkill(skill, { selectOnSuccess: true })}
+ />
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_create_flyout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_create_flyout.tsx
new file mode 100644
index 0000000000000..28b6ab804e38e
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_create_flyout.tsx
@@ -0,0 +1,137 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiForm,
+ EuiLink,
+ EuiSpacer,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { FormProvider } from 'react-hook-form';
+import type { PublicSkillDefinition } from '@kbn/agent-builder-common';
+import { labels } from '../../../utils/i18n';
+import { useCreateSkill } from '../../../hooks/skills/use_create_skill';
+import { useSkillForm } from '../../../hooks/skills/use_skill_form';
+import { useTools } from '../../../hooks/tools/use_tools';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { appPaths } from '../../../utils/app_paths';
+import { FLYOUT_WIDTH } from '../common/constants';
+import { SkillForm } from './skill_form';
+
+interface SkillCreateFlyoutProps {
+ onClose: () => void;
+ onSkillCreated?: (skill: PublicSkillDefinition) => void;
+}
+
+export const SkillCreateFlyout: React.FC = ({
+ onClose,
+ onSkillCreated,
+}) => {
+ const { createAgentBuilderUrl } = useNavigation();
+ const skillLibraryUrl = createAgentBuilderUrl(appPaths.manage.skills);
+ const { tools } = useTools();
+ const { euiTheme } = useEuiTheme();
+
+ const form = useSkillForm();
+ const {
+ control,
+ handleSubmit,
+ formState: { errors },
+ } = form;
+
+ const { isSubmitting, createSkill } = useCreateSkill({
+ onSuccess: (response) => {
+ onSkillCreated?.(response);
+ onClose();
+ },
+ });
+
+ const toolOptions = useMemo(
+ () => tools.map((tool) => ({ label: tool.id, value: tool.id })),
+ [tools]
+ );
+
+ const onSubmit = useCallback(
+ async (data: {
+ id: string;
+ name: string;
+ description: string;
+ content: string;
+ tool_ids: string[];
+ }) => {
+ await createSkill({
+ id: data.id,
+ name: data.name,
+ description: data.description,
+ content: data.content,
+ tool_ids: data.tool_ids,
+ });
+ },
+ [createSkill]
+ );
+
+ const hasErrors = Object.keys(errors).length > 0;
+
+ return (
+
+
+
+ {labels.agentSkills.createSkillFlyoutTitle}
+
+
+
+ {labels.agentSkills.viewSkillLibraryLink}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {labels.skills.cancelButtonLabel}
+
+
+
+ {labels.skills.saveButtonLabel}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_detail_panel.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_detail_panel.tsx
new file mode 100644
index 0000000000000..a997679d8e20c
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_detail_panel.tsx
@@ -0,0 +1,135 @@
+/*
+ * 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, { useState } from 'react';
+import {
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiText,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { labels } from '../../../utils/i18n';
+import { useSkill } from '../../../hooks/skills/use_skills';
+import { DetailRow } from '../common/detail_row';
+import { DetailPanelLayout } from '../common/detail_panel_layout';
+import { RenderSkillContentReadOnly } from '../common/render_skill_content_read_only';
+import { ToolReadOnlyFlyout } from '../tools/tool_readonly_flyout';
+
+interface SkillDetailPanelProps {
+ skillId: string;
+ onEdit: () => void;
+ onRemove: () => void;
+ isReadOnly?: boolean;
+}
+
+export const SkillDetailPanel: React.FC = ({
+ skillId,
+ onEdit,
+ onRemove,
+ isReadOnly = false,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const { skill, isLoading } = useSkill({ skillId });
+ const [selectedToolId, setSelectedToolId] = useState(null);
+
+ return (
+ <>
+
+
+ {skill?.id}
+
+
+ {skill?.description}
+
+ >
+ }
+ headerActions={(openConfirmRemove) =>
+ isReadOnly ? (
+ {labels.agentSkills.autoIncludedBadgeLabel}
+ ) : (
+
+ {!skill?.readonly && (
+
+
+ {labels.skills.editSkillButtonLabel}
+
+
+ )}
+
+
+ {labels.agentSkills.removeSkillButtonLabel}
+
+
+
+ )
+ }
+ confirmRemove={{
+ title: labels.agentSkills.removeSkillConfirmTitle(skill?.name ?? skillId),
+ body: labels.agentSkills.removeSkillConfirmBody,
+ confirmButtonText: labels.agentSkills.removeSkillConfirmButton,
+ cancelButtonText: labels.agentSkills.removeSkillCancelButton,
+ onConfirm: onRemove,
+ }}
+ >
+
+
+
+
+ {skill?.tool_ids && skill.tool_ids.length > 0 && (
+
+
+ {skill.tool_ids.map((toolId) => (
+
+ setSelectedToolId(toolId)}>{toolId}
+
+ ))}
+
+
+ )}
+
+
+ {selectedToolId && (
+ setSelectedToolId(null)} />
+ )}
+ >
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_edit_flyout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_edit_flyout.tsx
new file mode 100644
index 0000000000000..037b53fbaac3e
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_edit_flyout.tsx
@@ -0,0 +1,147 @@
+/*
+ * 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, useEffect, useMemo } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiForm,
+ EuiLink,
+ EuiLoadingSpinner,
+ EuiSpacer,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { FormProvider } from 'react-hook-form';
+import { labels } from '../../../utils/i18n';
+import { useEditSkill } from '../../../hooks/skills/use_edit_skill';
+import { useSkillForm } from '../../../hooks/skills/use_skill_form';
+import { useTools } from '../../../hooks/tools/use_tools';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { appPaths } from '../../../utils/app_paths';
+import { FLYOUT_WIDTH } from '../common/constants';
+import { SkillForm } from './skill_form';
+
+interface SkillEditFlyoutProps {
+ skillId: string;
+ onClose: () => void;
+ onSaved?: () => void;
+}
+
+export const SkillEditFlyout: React.FC = ({ skillId, onClose, onSaved }) => {
+ const { createAgentBuilderUrl } = useNavigation();
+ const skillLibraryUrl = createAgentBuilderUrl(appPaths.manage.skills);
+ const { tools } = useTools();
+ const { euiTheme } = useEuiTheme();
+ const form = useSkillForm();
+ const {
+ control,
+ reset,
+ handleSubmit,
+ formState: { errors, isDirty },
+ } = form;
+
+ const { skill, isLoading, isSubmitting, editSkill } = useEditSkill({
+ skillId,
+ onSuccess: () => {
+ onSaved?.();
+ onClose();
+ },
+ });
+
+ useEffect(() => {
+ if (skill) {
+ reset({
+ id: skill.id,
+ name: skill.name,
+ description: skill.description,
+ content: skill.content,
+ tool_ids: skill.tool_ids ?? [],
+ });
+ }
+ }, [skill, reset]);
+
+ const toolOptions = useMemo(
+ () => tools.map((tool) => ({ label: tool.id, value: tool.id })),
+ [tools]
+ );
+
+ const onSubmit = useCallback(
+ async (data: { name: string; description: string; content: string; tool_ids: string[] }) => {
+ await editSkill({
+ name: data.name,
+ description: data.description,
+ content: data.content,
+ tool_ids: data.tool_ids,
+ });
+ },
+ [editSkill]
+ );
+
+ const hasErrors = Object.keys(errors).length > 0;
+
+ return (
+
+
+
+ {labels.agentSkills.editSkillFlyoutTitle}
+
+
+
+ {labels.agentSkills.viewSkillLibraryLink}
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+
+
+ {labels.skills.cancelButtonLabel}
+
+
+
+ {labels.skills.saveButtonLabel}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_form.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_form.tsx
new file mode 100644
index 0000000000000..304d9b2db8c0f
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_form.tsx
@@ -0,0 +1,145 @@
+/*
+ * 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 {
+ EuiAccordion,
+ EuiComboBox,
+ EuiFieldText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiMarkdownEditor,
+ EuiSpacer,
+ EuiTextArea,
+} from '@elastic/eui';
+import { Controller } from 'react-hook-form';
+import type { Control } from 'react-hook-form';
+import { labels } from '../../../utils/i18n';
+import type { SkillFormData } from '../../skills/skill_form_validation';
+
+interface SkillFormProps {
+ control: Control;
+ toolOptions: Array<{ label: string; value: string }>;
+ /**
+ * When provided, the ID field is rendered as a disabled static input.
+ * When omitted, the ID field is rendered as an editable Controller-driven input.
+ */
+ readonlySkillId?: string;
+}
+
+export const SkillForm: React.FC = ({ control, toolOptions, readonlySkillId }) => {
+ return (
+ <>
+
+
+ {readonlySkillId !== undefined ? (
+
+
+
+ ) : (
+ (
+
+
+
+ )}
+ />
+ )}
+
+
+ (
+
+
+
+ )}
+ />
+
+
+
+
+
+ (
+
+
+
+ )}
+ />
+
+ (
+
+
+
+ )}
+ />
+
+
+
+
+
+ (
+
+ ({ label: toolId, value: toolId }))}
+ onChange={(selected) => onChange(selected.map((opt) => opt.value as string))}
+ />
+
+ )}
+ />
+
+ >
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_library_panel.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_library_panel.tsx
new file mode 100644
index 0000000000000..657dc0c2f0108
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/skills/skill_library_panel.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 type { PublicSkillSummary } from '@kbn/agent-builder-common';
+import { labels } from '../../../utils/i18n';
+import { appPaths } from '../../../utils/app_paths';
+import { LibraryPanel } from '../common/library_panel';
+import type { LibraryPanelLabels } from '../common/library_panel';
+
+const libraryLabels: LibraryPanelLabels = {
+ title: labels.agentSkills.addSkillFromLibraryTitle,
+ manageLibraryLink: labels.agentSkills.manageSkillLibraryLink,
+ searchPlaceholder: labels.agentSkills.searchAvailableSkillsPlaceholder,
+ availableSummary: labels.agentSkills.availableSkillsSummary,
+ noMatchMessage: labels.agentSkills.noAvailableSkillsMatchMessage,
+ noItemsMessage: labels.agentSkills.noAvailableSkillsMessage,
+ disabledBadgeLabel: labels.agentSkills.autoIncludedBadgeLabel,
+ disabledTooltipTitle: labels.agentSkills.autoIncludedTooltipTitle,
+ disabledTooltipBody: labels.agentSkills.autoIncludedTooltipBody,
+};
+
+const getSkillName = (skill: PublicSkillSummary): string => skill.name;
+
+interface SkillLibraryPanelProps {
+ onClose: () => void;
+ allSkills: PublicSkillSummary[];
+ activeSkillIdSet: Set;
+ onToggleSkill: (skill: PublicSkillSummary, isActive: boolean) => void;
+ mutatingSkillId: string | null;
+ enableElasticCapabilities?: boolean;
+ builtinSkillIdSet?: Set;
+}
+
+export const SkillLibraryPanel: React.FC = ({
+ onClose,
+ allSkills,
+ activeSkillIdSet,
+ onToggleSkill,
+ mutatingSkillId,
+ enableElasticCapabilities = false,
+ builtinSkillIdSet,
+}) => {
+ const disabledItemIdSet = useMemo(() => {
+ if (!enableElasticCapabilities || !builtinSkillIdSet) return undefined;
+ return builtinSkillIdSet;
+ }, [enableElasticCapabilities, builtinSkillIdSet]);
+
+ return (
+
+ onClose={onClose}
+ allItems={allSkills}
+ activeItemIdSet={activeSkillIdSet}
+ onToggleItem={onToggleSkill}
+ mutatingItemId={mutatingSkillId}
+ flyoutTitleId="skillLibraryFlyoutTitle"
+ libraryLabels={libraryLabels}
+ manageLibraryPath={appPaths.manage.skills}
+ getItemName={getSkillName}
+ disabledItemIdSet={disabledItemIdSet}
+ />
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/agent_tools.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/agent_tools.tsx
new file mode 100644
index 0000000000000..b4f5f5ba0b339
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/agent_tools.tsx
@@ -0,0 +1,346 @@
+/*
+ * 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, useState, useEffect, useCallback } from 'react';
+import { useParams } from 'react-router-dom';
+import {
+ EuiBadge,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiLoadingSpinner,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import type { ToolDefinition, ToolSelection } from '@kbn/agent-builder-common';
+import { defaultAgentToolIds } from '@kbn/agent-builder-common';
+import { useMutation, useQueryClient } from '@kbn/react-query';
+import { labels } from '../../../utils/i18n';
+import { appPaths } from '../../../utils/app_paths';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { useToolsService } from '../../../hooks/tools/use_tools';
+import { useAgentBuilderAgentById } from '../../../hooks/agents/use_agent_by_id';
+import { useAgentBuilderServices } from '../../../hooks/use_agent_builder_service';
+import { useToasts } from '../../../hooks/use_toasts';
+import { queryKeys } from '../../../query_keys';
+import { useFlyoutState } from '../../../hooks/use_flyout_state';
+import {
+ getActiveTools,
+ isToolSelected,
+ toggleToolSelection,
+} from '../../../utils/tool_selection_utils';
+import { ActiveItemRow } from '../common/active_item_row';
+import { ToolLibraryPanel } from './tool_library_panel';
+import { ToolDetailPanel } from './tool_detail_panel';
+import { PageWrapper } from '../common/page_wrapper';
+import { ICON_DIMENSIONS } from '../common/constants';
+import { useListDetailPageStyles } from '../common/styles';
+
+const ActiveToolsList: React.FC<{
+ filteredActiveTools: ToolDefinition[];
+ searchQuery: string;
+ selectedToolId: string | null;
+ enableElasticCapabilities: boolean;
+ defaultToolIdSet: Set;
+ isRemoving: boolean;
+ onSelect: (id: string) => void;
+ onRemove: (tool: ToolDefinition) => void;
+}> = ({
+ filteredActiveTools,
+ searchQuery,
+ selectedToolId,
+ enableElasticCapabilities,
+ defaultToolIdSet,
+ isRemoving,
+ onSelect,
+ onRemove,
+}) => {
+ if (filteredActiveTools.length === 0) {
+ return (
+
+
+ {searchQuery.trim()
+ ? labels.agentTools.noActiveToolsMatchMessage
+ : labels.agentTools.noActiveToolsMessage}
+
+
+ );
+ }
+
+ return (
+ <>
+ {filteredActiveTools.map((tool) => {
+ const isBuiltIn = defaultToolIdSet.has(tool.id);
+ const isAutoIncluded = enableElasticCapabilities && isBuiltIn;
+ return (
+ onSelect(tool.id)}
+ onRemove={() => onRemove(tool)}
+ isRemoving={isRemoving}
+ removeAriaLabel={labels.agentTools.removeToolAriaLabel}
+ readOnlyContent={
+ isAutoIncluded ? (
+
+ {labels.agentTools.elasticCapabilitiesReadOnlyBadge}
+
+ ) : isBuiltIn ? (
+ {labels.agentTools.readOnlyBadge}
+ ) : undefined
+ }
+ />
+ );
+ })}
+ >
+ );
+};
+
+export const AgentTools: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const styles = useListDetailPageStyles();
+ const { createAgentBuilderUrl } = useNavigation();
+ const { agentService } = useAgentBuilderServices();
+ const { addSuccessToast, addErrorToast } = useToasts();
+ const queryClient = useQueryClient();
+
+ const { agent, isLoading: agentLoading } = useAgentBuilderAgentById(agentId);
+ const { tools: allTools, isLoading: toolsLoading } = useToolsService();
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedToolId, setSelectedToolId] = useState(null);
+ const [mutatingToolId, setMutatingToolId] = useState(null);
+ const {
+ isOpen: isLibraryOpen,
+ openFlyout: openLibrary,
+ closeFlyout: closeLibrary,
+ } = useFlyoutState();
+
+ const agentToolSelections = useMemo(
+ () => agent?.configuration?.tools ?? [],
+ [agent?.configuration?.tools]
+ );
+
+ const enableElasticCapabilities = agent?.configuration?.enable_elastic_capabilities ?? false;
+
+ const defaultToolIdSet = useMemo(() => new Set(defaultAgentToolIds), []);
+
+ const activeTools = useMemo(
+ () =>
+ agent
+ ? getActiveTools(allTools, agentToolSelections, enableElasticCapabilities, defaultToolIdSet)
+ : [],
+ [allTools, agentToolSelections, agent, enableElasticCapabilities, defaultToolIdSet]
+ );
+
+ const activeToolIdSet = useMemo(() => new Set(activeTools.map((t) => t.id)), [activeTools]);
+
+ const libraryActiveToolIdSet = useMemo(() => {
+ if (enableElasticCapabilities) return new Set([...activeToolIdSet, ...defaultToolIdSet]);
+ return activeToolIdSet;
+ }, [activeToolIdSet, enableElasticCapabilities, defaultToolIdSet]);
+
+ useEffect(() => {
+ if (!selectedToolId) {
+ if (activeTools.length > 0) {
+ setSelectedToolId(activeTools[0].id);
+ }
+ } else {
+ const stillActive = activeTools.some((t) => t.id === selectedToolId);
+ if (!stillActive) {
+ setSelectedToolId(activeTools[0]?.id ?? null);
+ }
+ }
+ }, [activeTools, selectedToolId]);
+
+ const filteredActiveTools = useMemo(() => {
+ if (!searchQuery.trim()) return activeTools;
+ const lower = searchQuery.toLowerCase();
+ return activeTools.filter(
+ (t) => t.id.toLowerCase().includes(lower) || t.description.toLowerCase().includes(lower)
+ );
+ }, [activeTools, searchQuery]);
+
+ const updateToolsMutation = useMutation({
+ mutationFn: (newToolSelections: ToolSelection[]) => {
+ return agentService.update(agentId!, { configuration: { tools: newToolSelections } });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agentProfiles.byId(agentId) });
+ },
+ onError: () => {
+ addErrorToast({ title: labels.agentTools.updateToolsErrorToast });
+ },
+ });
+
+ const handleAddTool = useCallback(
+ async (tool: ToolDefinition) => {
+ if (isToolSelected(tool, agentToolSelections)) return;
+ const newSelections = toggleToolSelection(tool.id, allTools, agentToolSelections);
+ setMutatingToolId(tool.id);
+ try {
+ await updateToolsMutation.mutateAsync(newSelections);
+ addSuccessToast({ title: labels.agentTools.addToolSuccessToast(tool.id) });
+ } finally {
+ setMutatingToolId(null);
+ }
+ },
+ [agentToolSelections, allTools, updateToolsMutation, addSuccessToast]
+ );
+
+ const handleRemoveTool = useCallback(
+ (tool: ToolDefinition) => {
+ const newSelections = toggleToolSelection(tool.id, allTools, agentToolSelections);
+ setMutatingToolId(tool.id);
+ updateToolsMutation.mutate(newSelections, {
+ onSuccess: () => {
+ setSelectedToolId(null);
+ addSuccessToast({ title: labels.agentTools.removeToolSuccessToast(tool.id) });
+ },
+ onSettled: () => setMutatingToolId(null),
+ });
+ },
+ [agentToolSelections, allTools, updateToolsMutation, addSuccessToast]
+ );
+
+ const handleToggleTool = useCallback(
+ (tool: ToolDefinition, isActive: boolean) => {
+ if (enableElasticCapabilities && defaultToolIdSet.has(tool.id)) return;
+ if (isActive) {
+ handleAddTool(tool);
+ } else {
+ handleRemoveTool(tool);
+ }
+ },
+ [handleAddTool, handleRemoveTool, enableElasticCapabilities, defaultToolIdSet]
+ );
+
+ /** Guarded removal: only prevents removing auto-included tools from the agent. */
+ const handleRemoveSelectedTool = useCallback(() => {
+ if (!selectedToolId) return;
+ if (enableElasticCapabilities && defaultToolIdSet.has(selectedToolId)) return;
+ const tool = activeTools.find((t) => t.id === selectedToolId);
+ if (tool) {
+ handleRemoveTool(tool);
+ }
+ }, [selectedToolId, activeTools, handleRemoveTool, enableElasticCapabilities, defaultToolIdSet]);
+
+ const isLoading = agentLoading || toolsLoading;
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {labels.tools.title}
+
+
+
+
+
+
+
+
+ {labels.agentTools.manageAllTools}
+
+
+
+
+ {labels.agentTools.addToolButton}
+
+
+
+
+
+
+
+
+ {labels.agentTools.pageDescription}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ incremental
+ fullWidth
+ />
+
+
+
+
+
+
+ {selectedToolId ? (
+
+ ) : (
+
+
+ {labels.agentTools.noToolSelectedMessage}
+
+
+ )}
+
+
+
+ {isLibraryOpen && (
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_detail_panel.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_detail_panel.tsx
new file mode 100644
index 0000000000000..dfec8267760a2
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_detail_panel.tsx
@@ -0,0 +1,132 @@
+/*
+ * 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 {
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiText,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { labels } from '../../../utils/i18n';
+import { useToolService } from '../../../hooks/tools/use_tools';
+import { appPaths } from '../../../utils/app_paths';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { DetailPanelLayout } from '../common/detail_panel_layout';
+
+interface ToolDetailPanelProps {
+ toolId: string;
+ onRemove: () => void;
+ isAutoIncluded?: boolean;
+}
+
+export const ToolDetailPanel: React.FC = ({
+ toolId,
+ onRemove,
+ isAutoIncluded = false,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const { tool, isLoading } = useToolService(toolId);
+ const { createAgentBuilderUrl } = useNavigation();
+ const isReadOnly = tool?.readonly;
+ const editInLibraryUrl = createAgentBuilderUrl(appPaths.manage.toolDetails({ toolId }));
+
+ return (
+
+ {tool?.id}
+
+ }
+ headerActions={(openConfirmRemove) => (
+
+ {isAutoIncluded ? (
+
+ {labels.agentTools.autoIncludedBadgeLabel}
+
+ ) : isReadOnly ? (
+ <>
+
+
+ {labels.agentTools.readOnlyBadge}
+
+
+
+
+ {labels.agentTools.removeToolButtonLabel}
+
+
+ >
+ ) : (
+ <>
+
+
+ {labels.agentTools.editInLibraryLink}
+
+
+
+
+ {labels.agentTools.removeToolButtonLabel}
+
+
+ >
+ )}
+
+ )}
+ confirmRemove={{
+ title: labels.agentTools.removeToolConfirmTitle(tool?.id ?? toolId),
+ body: labels.agentTools.removeToolConfirmBody,
+ confirmButtonText: labels.agentTools.removeToolConfirmButton,
+ cancelButtonText: labels.agentTools.removeToolCancelButton,
+ onConfirm: onRemove,
+ }}
+ >
+
+
+ {labels.agentTools.toolDetailDescriptionLabel}
+
+
+ {tool?.description || '\u2014'}
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_library_panel.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_library_panel.tsx
new file mode 100644
index 0000000000000..6dc731c0bec7c
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_library_panel.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 type { ToolDefinition } from '@kbn/agent-builder-common';
+import { labels } from '../../../utils/i18n';
+import { appPaths } from '../../../utils/app_paths';
+import { LibraryPanel } from '../common/library_panel';
+import type { LibraryPanelLabels } from '../common/library_panel';
+
+const libraryLabels: LibraryPanelLabels = {
+ title: labels.agentTools.addToolFromLibraryTitle,
+ manageLibraryLink: labels.agentTools.manageToolLibraryLink,
+ searchPlaceholder: labels.agentTools.searchAvailableToolsPlaceholder,
+ availableSummary: labels.agentTools.availableToolsSummary,
+ noMatchMessage: labels.agentTools.noAvailableToolsMatchMessage,
+ noItemsMessage: labels.agentTools.noAvailableToolsMessage,
+ disabledBadgeLabel: labels.agentTools.autoIncludedBadgeLabel,
+ disabledTooltipTitle: labels.agentTools.autoIncludedTooltipTitle,
+ disabledTooltipBody: labels.agentTools.autoIncludedTooltipBody,
+};
+
+interface ToolLibraryPanelProps {
+ onClose: () => void;
+ allTools: ToolDefinition[];
+ activeToolIdSet: Set;
+ onToggleTool: (tool: ToolDefinition, isActive: boolean) => void;
+ mutatingToolId: string | null;
+ enableElasticCapabilities?: boolean;
+ builtinToolIdSet?: Set;
+}
+
+export const ToolLibraryPanel: React.FC = ({
+ onClose,
+ allTools,
+ activeToolIdSet,
+ onToggleTool,
+ mutatingToolId,
+ enableElasticCapabilities = false,
+ builtinToolIdSet,
+}) => {
+ const disabledItemIdSet = useMemo(() => {
+ if (!enableElasticCapabilities || !builtinToolIdSet) return undefined;
+ return builtinToolIdSet;
+ }, [enableElasticCapabilities, builtinToolIdSet]);
+
+ return (
+
+ onClose={onClose}
+ allItems={allTools}
+ activeItemIdSet={activeToolIdSet}
+ onToggleItem={onToggleTool}
+ mutatingItemId={mutatingToolId}
+ flyoutTitleId="toolLibraryFlyoutTitle"
+ libraryLabels={libraryLabels}
+ manageLibraryPath={appPaths.tools.list}
+ disabledItemIdSet={disabledItemIdSet}
+ />
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_readonly_flyout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_readonly_flyout.tsx
new file mode 100644
index 0000000000000..f104cb7020291
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/agents/tools/tool_readonly_flyout.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 {
+ EuiBadge,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiHorizontalRule,
+ EuiLoadingSpinner,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { labels } from '../../../utils/i18n';
+import { useToolService } from '../../../hooks/tools/use_tools';
+
+interface ToolReadOnlyFlyoutProps {
+ toolId: string;
+ onClose: () => void;
+}
+
+export const ToolReadOnlyFlyout: React.FC = ({ toolId, onClose }) => {
+ const { tool, isLoading } = useToolService(toolId);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {labels.agentTools.readOnlyBadge}
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : tool ? (
+
+
+
+ {labels.agentTools.toolDetailIdLabel}
+
+ {tool.id}
+
+
+
+
+ {labels.agentTools.toolDetailDescriptionLabel}
+
+ {tool.description || '\u2014'}
+
+
+ ) : null}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx
index 84ea6ae42c9f3..fd594a0a453c7 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx
@@ -86,21 +86,23 @@ export const AgentAvatar: React.FC = (props) => {
const isDefaultAgent = agentId === agentBuilderDefaultAgentId;
const shouldUseIcon = !symbol && (isBuiltIn || isDefaultAgent || Boolean(icon));
+ const borderAndShapeStyles = css`
+ border: 1px solid ${euiTheme.colors.borderBaseSubdued};
+ ${shape === 'circle' ? 'border-radius: 50%;' : roundedBorderRadiusStyles}
+ `;
+
if (shouldUseIcon) {
const iconType = icon ?? 'logoElastic';
const iconSize = getIconSize({ size });
- if (hasBackground) {
- const panelStyles = css`
- background-color: ${color};
- ${roundedBorderRadiusStyles}
- `;
- return (
-
-
-
- );
- }
- return ;
+ const panelStyles = css`
+ ${hasBackground ? `background-color: ${color};` : ''}
+ ${borderAndShapeStyles}
+ `;
+ return (
+
+
+
+ );
}
let type: 'user' | 'space' | undefined;
@@ -109,7 +111,6 @@ export const AgentAvatar: React.FC = (props) => {
} else if (shape === 'square') {
type = 'space';
}
- const avatarStyles = shape === 'square' && roundedBorderRadiusStyles;
return (
= (props) => {
initials={symbol}
type={type}
color={color}
- css={avatarStyles}
+ css={borderAndShapeStyles}
/>
);
};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector/agent_selector_dropdown.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector/agent_selector_dropdown.tsx
new file mode 100644
index 0000000000000..727ffdacebdb9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector/agent_selector_dropdown.tsx
@@ -0,0 +1,237 @@
+/*
+ * 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, { useState } from 'react';
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiPopover,
+ EuiPopoverFooter,
+ EuiSelectable,
+ EuiText,
+ useEuiTheme,
+} from '@elastic/eui';
+import type { EuiPopoverProps } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import type { AgentDefinition } from '@kbn/agent-builder-common';
+
+import { useUiPrivileges } from '../../../hooks/use_ui_privileges';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { appPaths } from '../../../utils/app_paths';
+import {
+ selectorPopoverPanelStyles,
+ useSelectorListStyles,
+} from '../../conversations/conversation_input/input_actions/input_actions.styles';
+import { useAgentOptions } from './use_agent_options';
+
+const SELECTOR_POPOVER_HEIGHT = 400;
+const AGENT_OPTION_ROW_HEIGHT = 52; // 48px content + 4px bottom gap
+const HORIZONTAL_RULE_HEIGHT = 1;
+const AGENT_SELECTOR_HEADER_HEIGHT = 56;
+const AGENT_SELECTOR_FOOTER_HEIGHT = 64;
+
+const labels = {
+ selectAgent: i18n.translate('xpack.agentBuilder.agentSelectorDropdown.selectAgent.ariaLabel', {
+ defaultMessage: 'Select an agent',
+ }),
+ availableAgents: i18n.translate('xpack.agentBuilder.agentSelectorDropdown.availableAgents', {
+ defaultMessage: 'Available agents',
+ }),
+ newAgent: i18n.translate('xpack.agentBuilder.agentSelectorDropdown.newAgent', {
+ defaultMessage: 'New agent',
+ }),
+ manageAgents: i18n.translate('xpack.agentBuilder.agentSelectorDropdown.manageAgents', {
+ defaultMessage: 'Manage agents',
+ }),
+};
+
+const agentSelectId = 'agentBuilderAgentSelectorDropdown';
+const agentListId = `${agentSelectId}_listbox`;
+
+const AgentListHeader: React.FC = () => {
+ const { euiTheme } = useEuiTheme();
+ const { manageAgents } = useUiPrivileges();
+ const { createAgentBuilderUrl } = useNavigation();
+ const createAgentHref = createAgentBuilderUrl(appPaths.agents.new);
+ return (
+
+
+
+ {labels.availableAgents}
+
+
+
+
+ {labels.newAgent}
+
+
+
+ );
+};
+
+const AgentListFooter: React.FC = () => {
+ const { euiTheme } = useEuiTheme();
+ const { manageAgents } = useUiPrivileges();
+ const { createAgentBuilderUrl } = useNavigation();
+ const manageAgentsHref = createAgentBuilderUrl(appPaths.agents.list);
+ return (
+
+
+ {labels.manageAgents}
+
+
+ );
+};
+
+export interface AgentSelectorDropdownProps {
+ agents: AgentDefinition[];
+ selectedAgent?: AgentDefinition;
+ onAgentChange: (agentId: string) => void;
+ anchorPosition?: EuiPopoverProps['anchorPosition'];
+ /** Shown in the trigger button when selectedAgent is undefined (e.g. deleted agent) */
+ fallbackLabel?: string;
+}
+
+export const AgentSelectorDropdown: React.FC = ({
+ agents,
+ selectedAgent,
+ onAgentChange,
+ anchorPosition = 'downLeft',
+ fallbackLabel,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const { agentOptions, renderAgentOption } = useAgentOptions({
+ agents,
+ selectedAgentId: selectedAgent?.id,
+ });
+
+ const maxListHeight =
+ SELECTOR_POPOVER_HEIGHT -
+ AGENT_SELECTOR_HEADER_HEIGHT -
+ HORIZONTAL_RULE_HEIGHT -
+ AGENT_SELECTOR_FOOTER_HEIGHT -
+ HORIZONTAL_RULE_HEIGHT;
+ const listHeight = Math.min(agentOptions.length * AGENT_OPTION_ROW_HEIGHT, maxListHeight);
+
+ const selectorListStyles = css`
+ ${useSelectorListStyles({ listId: agentListId })}
+ ${agentListId} .euiSelectableListItem {
+ align-items: flex-start;
+ padding-bottom: ${euiTheme.size.xs};
+ }
+ `;
+
+ const triggerButton = (
+ setIsPopoverOpen((v) => !v)}
+ data-test-subj="agentBuilderAgentSelectorButton"
+ >
+ {selectedAgent?.name ?? fallbackLabel}
+
+ );
+
+ return (
+ setIsPopoverOpen(false)}
+ >
+
+
+
+
+
+
+
+
+ {
+ const { checked, key: agentId } = changedOption;
+ if (checked === 'on' && agentId) {
+ onAgentChange(agentId);
+ setIsPopoverOpen(false);
+ }
+ }}
+ singleSelection
+ renderOption={(option) => renderAgentOption({ agent: option.agent })}
+ height={listHeight}
+ listProps={{
+ id: agentListId,
+ isVirtualized: true,
+ rowHeight: AGENT_OPTION_ROW_HEIGHT,
+ onFocusBadge: false,
+ css: selectorListStyles,
+ }}
+ >
+ {(list) => (
+ <>
+ {list}
+
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/use_agent_options.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector/use_agent_options.tsx
similarity index 50%
rename from x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/use_agent_options.tsx
rename to x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector/use_agent_options.tsx
index 25320154b52e2..b91aa3a5ee47d 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/use_agent_options.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector/use_agent_options.tsx
@@ -10,9 +10,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
import type { AgentDefinition } from '@kbn/agent-builder-common';
import React, { useMemo } from 'react';
import { css } from '@emotion/react';
-import { i18n } from '@kbn/i18n';
-import { AgentAvatar } from '../../../../common/agent_avatar';
-import { OptionText } from '../option_text';
+import { AgentAvatar } from '../agent_avatar';
+import { OptionText } from '../../conversations/conversation_input/input_actions/option_text';
type AgentOptionData = EuiSelectableOption<{ agent?: AgentDefinition }>;
@@ -20,18 +19,11 @@ interface AgentOptionProps {
agent?: AgentDefinition;
}
-const readonlyAgentTooltip = i18n.translate(
- 'xpack.agentBuilder.agentSelector.readonlyAgentTooltip',
- {
- defaultMessage: 'This agent is read-only.',
- }
-);
-
const AgentOptionPrepend: React.FC<{ agent: AgentDefinition }> = ({ agent }) => {
return (
-
+
);
@@ -44,25 +36,31 @@ const AgentOption: React.FC = ({ agent }) => {
return (
-
+
{agent.name}
- {agent.readonly && (
-
-
-
- )}
+
+
+
);
@@ -75,25 +73,28 @@ export const useAgentOptions = ({
agents: AgentDefinition[];
selectedAgentId?: string;
}) => {
- const agentOptions = useMemo(
- () =>
- agents.map((agent) => {
- let checked: 'on' | undefined;
- if (agent.id === selectedAgentId) {
- checked = 'on';
- }
- const option: AgentOptionData = {
- key: agent.id,
- label: agent.name,
- checked,
- prepend: ,
- textWrap: 'wrap',
- data: { agent },
- };
- return option;
- }),
- [agents, selectedAgentId]
- );
+ const agentOptions = useMemo(() => {
+ const sorted = [...agents].sort((a, b) => {
+ if (a.id === selectedAgentId) return -1;
+ if (b.id === selectedAgentId) return 1;
+ return 0;
+ });
+ return sorted.map((agent) => {
+ let checked: 'on' | undefined;
+ if (agent.id === selectedAgentId) {
+ checked = 'on';
+ }
+ const option: AgentOptionData = {
+ key: agent.id,
+ label: agent.name,
+ checked,
+ prepend: ,
+ textWrap: 'wrap',
+ data: { agent },
+ };
+ return option;
+ });
+ }, [agents, selectedAgentId]);
return {
agentOptions,
renderAgentOption: (props: AgentOptionProps) => ,
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/conversations/actions/start_new_conversation_button.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/actions/start_new_conversation_button.tsx
index e71c16d521917..b956c458b83cd 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/actions/start_new_conversation_button.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/actions/start_new_conversation_button.tsx
@@ -10,6 +10,7 @@ import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useConversationContext } from '../../../context/conversation/conversation_context';
import { useNavigation } from '../../../hooks/use_navigation';
+import { useLastAgentId } from '../../../hooks/use_last_agent_id';
import { appPaths } from '../../../utils/app_paths';
const NEW_CONVERSATION_BUTTON_LABEL = i18n.translate(
@@ -19,19 +20,18 @@ const NEW_CONVERSATION_BUTTON_LABEL = i18n.translate(
}
);
-const NEW_CONVERSATION_PATH = appPaths.chat.new;
-
export const StartNewConversationButton: React.FC = () => {
const { navigateToAgentBuilderUrl } = useNavigation();
const { isEmbeddedContext, setConversationId } = useConversationContext();
+ const lastAgentId = useLastAgentId();
const handleClick = useCallback(() => {
if (isEmbeddedContext) {
setConversationId?.(undefined);
} else {
- navigateToAgentBuilderUrl(NEW_CONVERSATION_PATH);
+ navigateToAgentBuilderUrl(appPaths.agent.conversations.new({ agentId: lastAgentId }));
}
- }, [isEmbeddedContext, setConversationId, navigateToAgentBuilderUrl]);
+ }, [isEmbeddedContext, setConversationId, navigateToAgentBuilderUrl, lastAgentId]);
return (
void;
-}
-
-export const CloseDockedViewButton: React.FC = ({ onClose }) => {
- return (
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_actions_left.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_actions_left.tsx
deleted file mode 100644
index 3eaf439b1e24f..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_actions_left.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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, EuiTourStep } from '@elastic/eui';
-import { ConversationsHistoryButton } from './conversations_history_button';
-import { useHasActiveConversation } from '../../../hooks/use_conversation';
-import { NewConversationButton } from './new_conversation_button';
-import { TourStep, useAgentBuilderTour } from '../../../context/agent_builder_tour_context';
-
-export const ConversationLeftActions: React.FC<{}> = () => {
- const hasActiveConversation = useHasActiveConversation();
- const { getStepProps } = useAgentBuilderTour();
-
- return (
-
-
-
-
- {hasActiveConversation && }
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_actions_right.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_actions_right.tsx
index ede854986dce9..66f71e2fbb514 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_actions_right.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_actions_right.tsx
@@ -6,47 +6,45 @@
*/
import React from 'react';
-import { EuiFlexGroup, EuiTourStep } from '@elastic/eui';
+import { EuiButtonIcon, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useConversationContext } from '../../../context/conversation/conversation_context';
import { MoreActionsButton } from './more_actions_button';
-import { CloseDockedViewButton } from './close_docked_view_button';
-import { TourStep, useAgentBuilderTour } from '../../../context/agent_builder_tour_context';
const labels = {
container: i18n.translate('xpack.agentBuilder.conversationActions.container', {
defaultMessage: 'Conversation actions',
}),
+ close: i18n.translate('xpack.agentBuilder.conversationActions.close', {
+ defaultMessage: 'Close',
+ }),
};
export interface ConversationRightActionsProps {
onClose?: () => void;
- onRenameConversation: () => void;
}
-export const ConversationRightActions: React.FC = ({
- onClose,
- onRenameConversation,
-}) => {
+export const ConversationRightActions: React.FC = ({ onClose }) => {
const { isEmbeddedContext } = useConversationContext();
- const { getStepProps } = useAgentBuilderTour();
-
return (
-
-
+ {isEmbeddedContext && (
+
-
- {isEmbeddedContext ? : null}
+ )}
);
};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_header.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_header.tsx
index 1a5d160b4667c..90d9bfb163d16 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_header.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_header.tsx
@@ -5,15 +5,20 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React from 'react';
import { css } from '@emotion/react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ConversationRightActions } from './conversation_actions_right';
-import { ConversationLeftActions } from './conversation_actions_left';
import { ConversationTitle } from './conversation_title';
-const centerSectionStyles = css`
+const headerGridStyles = css`
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
align-items: center;
+ width: 100%;
+`;
+
+const rightActionsStyles = css`
+ justify-self: end;
`;
interface ConversationHeaderProps {
@@ -24,26 +29,18 @@ export const ConversationHeader: React.FC = ({
onClose,
ariaLabelledBy,
}) => {
- const [isEditing, setIsEditing] = useState(false);
-
return (
-
-
-
-
-
-
-
-
- setIsEditing(true)}
- />
-
-
+
+ {/* Left column — intentionally empty, reserved for future actions */}
+
+
+ {/* Center column — always exactly centered */}
+
+
+ {/* Right column — right-aligned within its 1fr column */}
+
+
+
+
);
};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_title.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_title.tsx
index ffdd97c61cf6c..daa5850d6caab 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_title.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversation_title.tsx
@@ -5,129 +5,142 @@
* 2.0.
*/
-import React, { useState, useEffect } from 'react';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
+import React, { useState } from 'react';
+import {
+ EuiButtonEmpty,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiIcon,
+ EuiPopover,
+ useEuiTheme,
+} from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
-import {
- useConversationTitle,
- useHasActiveConversation,
- useHasPersistedConversation,
-} from '../../../hooks/use_conversation';
-import { RenameConversationInput } from './rename_conversation_input';
+import { useConversationTitle, useHasPersistedConversation } from '../../../hooks/use_conversation';
+import { DeleteConversationModal } from '../delete_conversation_modal';
+import { RenameConversationModal } from '../rename_conversation_modal';
const labels = {
- ariaLabel: i18n.translate('xpack.agentBuilder.conversationTitle.ariaLabel', {
- defaultMessage: 'Conversation title',
- }),
rename: i18n.translate('xpack.agentBuilder.conversationTitle.rename', {
- defaultMessage: 'Rename conversation',
+ defaultMessage: 'Rename',
+ }),
+ delete: i18n.translate('xpack.agentBuilder.conversationTitle.delete', {
+ defaultMessage: 'Delete',
}),
newConversation: i18n.translate('xpack.agentBuilder.conversationTitle.newConversation', {
defaultMessage: 'New conversation',
}),
+ openTitleMenu: i18n.translate('xpack.agentBuilder.conversationTitle.openTitleMenu', {
+ defaultMessage: 'Open conversation menu',
+ }),
};
interface ConversationTitleProps {
ariaLabelledBy?: string;
- isEditing: boolean;
- setIsEditing: (isEditing: boolean) => void;
}
-export const ConversationTitle: React.FC = ({
- ariaLabelledBy,
- isEditing,
- setIsEditing,
-}) => {
- const { title, isLoading } = useConversationTitle();
- const hasActiveConversation = useHasActiveConversation();
+export const ConversationTitle: React.FC = ({ ariaLabelledBy }) => {
+ const { title, isLoading: isLoadingTitle } = useConversationTitle();
const hasPersistedConversation = useHasPersistedConversation();
const { euiTheme } = useEuiTheme();
- const [isHovering, setIsHovering] = useState(false);
- const [previousTitle, setPreviousTitle] = useState('');
- const [currentText, setCurrentText] = useState('');
-
- useEffect(() => {
- if (isLoading || !hasActiveConversation) return;
-
- const fullText = title || labels.newConversation;
-
- // Typewriter: ONLY when transitioning from "New conversation" to actual title
- if (previousTitle === labels.newConversation && title) {
- if (currentText.length < fullText.length) {
- const timeout = setTimeout(() => {
- setCurrentText(fullText.substring(0, currentText.length + 1));
- }, 50);
- return () => clearTimeout(timeout);
- }
- } else if (title && title !== previousTitle) {
- // Normal title change: immediate
- setCurrentText(fullText);
- } else if (!title) {
- // Reset when switching to new conversation (no title)
- setCurrentText('');
- }
-
- // Always track the previous title
- setPreviousTitle(fullText);
- }, [title, currentText, isLoading, previousTitle, hasActiveConversation]);
- const displayedTitle = currentText || previousTitle;
-
- const handlePencilClick = () => {
- setIsEditing(true);
- setIsHovering(false);
- };
-
- const handleCancel = () => {
- setIsEditing(false);
- };
-
- const shouldShowTitle = hasActiveConversation;
- if (!shouldShowTitle) {
- return null;
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const [isRenameModalOpen, setIsRenameModalOpen] = useState(false);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ const displayedTitle = isLoadingTitle ? '' : title || labels.newConversation;
+
+ // No popover for unsaved conversations — just show the title
+ if (!hasPersistedConversation) {
+ return (
+
+ {displayedTitle}
+
+ );
}
- if (isEditing) {
- return ;
- }
+ const menuItems = [
+ {
+ setIsPopoverOpen(false);
+ setIsRenameModalOpen(true);
+ }}
+ data-test-subj="agentBuilderConversationRenameButton"
+ >
+ {labels.rename}
+ ,
+ }
+ onClick={() => {
+ setIsPopoverOpen(false);
+ setIsDeleteModalOpen(true);
+ }}
+ css={css`
+ color: ${euiTheme.colors.danger};
+ `}
+ data-test-subj="agentBuilderConversationDeleteButton"
+ >
+ {labels.delete}
+ ,
+ ];
- const titleStyles = css`
+ const titleButtonStyles = css`
+ max-width: 100%;
+ block-size: auto;
font-weight: ${euiTheme.font.weight.semiBold};
+ .euiButtonEmpty__text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
`;
- // Only show rename icon when there is a conversation ID !== 'new'
- const canRename = hasPersistedConversation;
+ const titleButton = (
+ setIsPopoverOpen((open) => !open)}
+ aria-expanded={isPopoverOpen}
+ css={titleButtonStyles}
+ data-test-subj="agentBuilderConversationTitleButton"
+ >
+ {displayedTitle}
+
+ );
return (
- setIsHovering(true)}
- onMouseLeave={() => setIsHovering(false)}
- data-test-subj="agentBuilderConversationTitle"
- >
-
-
- {displayedTitle}
-
-
- {canRename && (
-
-
-
- )}
-
+ <>
+ setIsPopoverOpen(false)}
+ panelPaddingSize="none"
+ anchorPosition="downCenter"
+ aria-label={labels.openTitleMenu}
+ >
+
+
+
+ setIsRenameModalOpen(false)}
+ />
+
+ setIsDeleteModalOpen(false)}
+ />
+ >
);
};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversations_history_button.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversations_history_button.tsx
deleted file mode 100644
index 7774094e4ee55..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/conversations_history_button.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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 {
- EuiButtonEmpty,
- EuiButtonIcon,
- EuiPageHeaderSection,
- EuiToolTip,
- useEuiTheme,
-} from '@elastic/eui';
-import React, { useState } from 'react';
-import { i18n } from '@kbn/i18n';
-import { css } from '@emotion/react';
-import { useHasActiveConversation } from '../../../hooks/use_conversation';
-import { ConversationsHistoryPopover } from '../conversations_history/conversations_history_popover';
-import { useConversationContext } from '../../../context/conversation/conversation_context';
-import { useConversationList } from '../../../hooks/use_conversation_list';
-
-const labels = {
- open: i18n.translate('xpack.agentBuilder.conversationsHistory.open', {
- defaultMessage: 'Open conversations',
- }),
- close: i18n.translate('xpack.agentBuilder.conversationsHistory.close', {
- defaultMessage: 'Close conversations',
- }),
- conversations: i18n.translate('xpack.agentBuilder.conversationsHistory.conversations', {
- defaultMessage: 'Conversations',
- }),
-};
-
-export const ConversationsHistoryButton: React.FC = () => {
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
- const hasActiveConversation = useHasActiveConversation();
- const { euiTheme } = useEuiTheme();
- const { isEmbeddedContext } = useConversationContext();
- const { conversations, isLoading } = useConversationList();
-
- const hasNoConversations = !isLoading && conversations?.length === 0;
-
- const togglePopover = () => {
- setIsPopoverOpen(!isPopoverOpen);
- };
-
- const paddingLeft = css`
- padding-left: ${euiTheme.size.s};
- `;
-
- const showButtonIcon = hasActiveConversation;
-
- const button = showButtonIcon ? (
- {labels.conversations}
}>
-
-
- ) : (
-
- {labels.conversations}
-
- );
-
- const shouldAddPaddingLeft = hasActiveConversation && !isEmbeddedContext;
- return (
-
- setIsPopoverOpen(false)}
- />
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/more_actions_button.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/more_actions_button.tsx
index 94448c672c3fb..836fea064304e 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/more_actions_button.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/more_actions_button.tsx
@@ -8,38 +8,22 @@
import {
EuiContextMenuItem,
EuiButtonIcon,
- EuiButtonEmpty,
EuiPopover,
EuiContextMenuPanel,
- EuiTitle,
EuiSpacer,
- EuiIcon,
- useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useState } from 'react';
-import {
- DATA_SOURCES_ENABLED_SETTING_ID,
- AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID,
-} from '@kbn/management-settings-ids';
-import { DATA_SOURCES_APP_ID } from '@kbn/deeplinks-data-sources';
-import { css } from '@emotion/react';
-import { useIsAgentReadOnly } from '../../../hooks/agents/use_is_agent_read_only';
-import { useExperimentalFeatures } from '../../../hooks/use_experimental_features';
import { useNavigation } from '../../../hooks/use_navigation';
-import {
- useHasActiveConversation,
- useAgentId,
- useHasPersistedConversation,
-} from '../../../hooks/use_conversation';
+import { useAgentId } from '../../../hooks/use_conversation';
import { useConversationContext } from '../../../context/conversation/conversation_context';
import { useConversationId } from '../../../context/conversation/use_conversation_id';
import { useKibana } from '../../../hooks/use_kibana';
-import { searchParamNames } from '../../../search_param_names';
import { appPaths } from '../../../utils/app_paths';
import { DeleteConversationModal } from '../delete_conversation_modal';
import { useHasConnectorsAllPrivileges } from '../../../hooks/use_has_connectors_all_privileges';
import { useUiPrivileges } from '../../../hooks/use_ui_privileges';
+
const fullscreenLabels = {
actions: i18n.translate('xpack.agentBuilder.conversationActions.actions', {
defaultMessage: 'More',
@@ -47,53 +31,8 @@ const fullscreenLabels = {
actionsAriaLabel: i18n.translate('xpack.agentBuilder.conversationActions.actionsAriaLabel', {
defaultMessage: 'More',
}),
- conversationTitleLabel: i18n.translate(
- 'xpack.agentBuilder.conversationActions.conversationTitleLabel',
- {
- defaultMessage: 'Conversation',
- }
- ),
- editCurrentAgent: i18n.translate('xpack.agentBuilder.conversationActions.editCurrentAgent', {
- defaultMessage: 'Edit agent',
- }),
- cloneAgentAsNew: i18n.translate('xpack.agentBuilder.conversationActions.duplicateAgentAsNew', {
- defaultMessage: 'Duplicate as new',
- }),
- conversationAgentLabel: i18n.translate(
- 'xpack.agentBuilder.conversationActions.conversationAgentLabel',
- {
- defaultMessage: 'Agent',
- }
- ),
- conversationManagementLabel: i18n.translate(
- 'xpack.agentBuilder.conversationActions.conversationManagementLabel',
- {
- defaultMessage: 'Management',
- }
- ),
- agents: i18n.translate('xpack.agentBuilder.conversationActions.agents', {
- defaultMessage: 'View all agents',
- }),
- tools: i18n.translate('xpack.agentBuilder.conversationActions.tools', {
- defaultMessage: 'View all tools',
- }),
- skills: i18n.translate('xpack.agentBuilder.conversationActions.skills', {
- defaultMessage: 'View all skills',
- }),
- plugins: i18n.translate('xpack.agentBuilder.conversationActions.plugins', {
- defaultMessage: 'View all plugins',
- }),
- connectors: i18n.translate('xpack.agentBuilder.conversationActions.connectors', {
- defaultMessage: 'View all connectors',
- }),
- sources: i18n.translate('xpack.agentBuilder.conversationActions.sources', {
- defaultMessage: 'View all sources',
- }),
- rename: i18n.translate('xpack.agentBuilder.conversationActions.rename', {
- defaultMessage: 'Rename',
- }),
- delete: i18n.translate('xpack.agentBuilder.conversationActions.delete', {
- defaultMessage: 'Delete',
+ agentDetails: i18n.translate('xpack.agentBuilder.conversationActions.agentDetails', {
+ defaultMessage: 'Agent details',
}),
genAiSettings: i18n.translate('xpack.agentBuilder.conversationActions.genAiSettings', {
defaultMessage: 'GenAI Settings',
@@ -108,63 +47,28 @@ const fullscreenLabels = {
defaultMessage: 'View',
}),
fullScreen: i18n.translate('xpack.agentBuilder.conversationActions.fullScreen', {
- defaultMessage: 'Full screen',
+ defaultMessage: 'Open in full screen',
}),
};
-const popoverMinWidthStyles = css`
- min-width: 240px;
-`;
-
-const MenuSectionTitle = ({ title }: { title: string }) => {
- const { euiTheme } = useEuiTheme();
- return (
- <>
-
-
- {title}
-
-
- >
- );
-};
-
interface MoreActionsButtonProps {
- onRenameConversation: () => void;
onCloseSidebar?: () => void;
}
-export const MoreActionsButton: React.FC = ({
- onRenameConversation,
- onCloseSidebar,
-}) => {
+export const MoreActionsButton: React.FC = ({ onCloseSidebar }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
- const hasActiveConversation = useHasActiveConversation();
- const hasPersistedConversation = useHasPersistedConversation();
+
const agentId = useAgentId();
- const isAgentReadOnly = useIsAgentReadOnly(agentId);
const { createAgentBuilderUrl, navigateToAgentBuilderUrl } = useNavigation();
const { isEmbeddedContext } = useConversationContext();
const conversationId = useConversationId();
- const { euiTheme } = useEuiTheme();
const { manageAgents } = useUiPrivileges();
const {
- services: { application, uiSettings },
+ services: { application },
} = useKibana();
const hasAccessToGenAiSettings = useHasConnectorsAllPrivileges();
- const isExperimentalFeaturesEnabled = useExperimentalFeatures();
- const isDataSourcesEnabled = uiSettings.get(DATA_SOURCES_ENABLED_SETTING_ID, false);
- const isABConnectorsEnabled = uiSettings.get(
- AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID,
- false
- );
const closePopover = () => {
setIsPopoverOpen(false);
@@ -175,22 +79,31 @@ export const MoreActionsButton: React.FC = ({
};
const handleOpenFullScreen = useCallback(() => {
- if (!application) {
- return;
- }
+ if (!application) return;
+
setIsPopoverOpen(false);
onCloseSidebar?.();
+
const path = conversationId
- ? appPaths.chat.conversation({ conversationId })
- : appPaths.chat.new;
- const params = !conversationId && agentId ? { [searchParamNames.agentId]: agentId } : undefined;
- navigateToAgentBuilderUrl(path, params);
+ ? appPaths.agent.conversations.byId({ agentId: agentId!, conversationId: conversationId! })
+ : appPaths.agent.conversations.new({ agentId: agentId! });
+
+ navigateToAgentBuilderUrl(path);
}, [application, agentId, conversationId, navigateToAgentBuilderUrl, onCloseSidebar]);
- const menuItems = [
+ const embeddedContextMenuItems = [
+
+ {fullscreenLabels.agentDetails}
+ ,
...(isEmbeddedContext && application
? [
- ,
= ({
,
]
: []),
- ...(hasPersistedConversation
+ ...(hasAccessToGenAiSettings
? [
- ,
{
- closePopover();
- onRenameConversation();
- }}
- >
- {fullscreenLabels.rename}
- ,
- {
- closePopover();
- setIsDeleteModalOpen(true);
- }}
+ key="agentBuilderSettings"
+ icon="gear"
+ onClick={closePopover}
+ href={application.getUrlForApp('management', { path: '/ai/genAiSettings' })}
+ data-test-subj="agentBuilderGenAiSettingsButton"
>
- {fullscreenLabels.delete}
+ {fullscreenLabels.genAiSettings}
,
]
: []),
- ,
-
- {fullscreenLabels.editCurrentAgent}
- ,
+ ];
+
+ const fullscreenMenuItems = [
- {fullscreenLabels.cloneAgentAsNew}
+ {fullscreenLabels.agentDetails}
,
- ,
- }
- onClick={closePopover}
- href={createAgentBuilderUrl(appPaths.agents.list)}
- data-test-subj="agentBuilderActionsAgents"
- >
- {fullscreenLabels.agents}
- ,
-
- {fullscreenLabels.tools}
- ,
- ...(isExperimentalFeaturesEnabled
- ? [
-
- {fullscreenLabels.skills}
- ,
-
- {fullscreenLabels.plugins}
- ,
- ]
- : []),
- ...(isABConnectorsEnabled
- ? [
-
- {fullscreenLabels.connectors}
- ,
- ]
- : []),
- ...(isDataSourcesEnabled
- ? [
-
- {fullscreenLabels.sources}
- ,
- ]
- : []),
...(hasAccessToGenAiSettings
? [
= ({
: []),
];
+ const menuItems = isEmbeddedContext ? embeddedContextMenuItems : fullscreenMenuItems;
+
const buttonProps = {
iconType: 'boxesVertical' as const,
color: 'text' as const,
+ size: 'm' as const,
'aria-label': fullscreenLabels.actionsAriaLabel,
onClick: togglePopover,
'data-test-subj': 'agentBuilderMoreActionsButton',
};
- const showButtonIcon = hasActiveConversation;
- const button = showButtonIcon ? (
-
- ) : (
- {fullscreenLabels.actions}
- );
return (
<>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="xs"
anchorPosition="downCenter"
- panelProps={{
- css: popoverMinWidthStyles,
- }}
+ aria-label={fullscreenLabels.actionsAriaLabel}
>
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/new_conversation_button.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/new_conversation_button.tsx
deleted file mode 100644
index 1818c9e9f7ed5..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_header/new_conversation_button.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { useNavigation } from '../../../hooks/use_navigation';
-import { appPaths } from '../../../utils/app_paths';
-import { useConversationId } from '../../../context/conversation/use_conversation_id';
-import { useSendMessage } from '../../../context/send_message/send_message_context';
-import { useConversationContext } from '../../../context/conversation/conversation_context';
-
-interface NewConversationButtonProps {
- onClose?: () => void;
-}
-
-export const NewConversationButton: React.FC = ({ onClose }) => {
- const { createAgentBuilderUrl } = useNavigation();
- const { isEmbeddedContext, setConversationId } = useConversationContext();
- const conversationId = useConversationId();
- const isNewConversation = !conversationId;
- const { cleanConversation } = useSendMessage();
-
- const handleClick = () => {
- // For new conversations, there isn't anywhere to navigate to, so instead we clean the conversation state
- if (isNewConversation) {
- cleanConversation();
- }
- if (isEmbeddedContext) {
- setConversationId?.(undefined);
- }
- onClose?.();
- };
-
- const buttonProps = isEmbeddedContext
- ? {}
- : {
- href: createAgentBuilderUrl(appPaths.chat.new),
- };
-
- const label = i18n.translate('xpack.agentBuilder.newConversationButton.ariaLabel', {
- defaultMessage: 'Create new conversation',
- });
-
- return (
- {label}}>
-
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/agent_select_dropdown.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/agent_select_dropdown.tsx
deleted file mode 100644
index f2bbd7e2cd829..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/agent_select_dropdown.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * 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 {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPopover,
- EuiPopoverFooter,
- EuiSelectable,
-} from '@elastic/eui';
-import { css } from '@emotion/react';
-import { i18n } from '@kbn/i18n';
-import type { AgentDefinition } from '@kbn/agent-builder-common';
-import React, { useState } from 'react';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { useUiPrivileges } from '../../../../../hooks/use_ui_privileges';
-import { useHasActiveConversation } from '../../../../../hooks/use_conversation';
-import { useNavigation } from '../../../../../hooks/use_navigation';
-import { appPaths } from '../../../../../utils/app_paths';
-import {
- getMaxListHeight,
- selectorPopoverPanelStyles,
- useSelectorListStyles,
-} from '../input_actions.styles';
-import { useAgentOptions } from './use_agent_options';
-import { InputPopoverButton } from '../input_popover_button';
-import { AgentAvatar } from '../../../../common/agent_avatar';
-
-const AGENT_OPTION_ROW_HEIGHT = 44;
-
-const selectAgentAriaLabel = i18n.translate(
- 'xpack.agentBuilder.conversationInput.agentSelector.selectAgent.ariaLabel',
- {
- defaultMessage: 'Select an agent',
- }
-);
-const selectAgentFallbackButtonLabel = i18n.translate(
- 'xpack.agentBuilder.conversationInput.agentSelector.fallbackButtonLabel',
- { defaultMessage: 'Agents' }
-);
-const createAgentAriaLabel = i18n.translate(
- 'xpack.agentBuilder.conversationInput.agentSelector.createAgent.ariaLabel',
- {
- defaultMessage: 'Create an agent',
- }
-);
-const manageAgentsAriaLabel = i18n.translate(
- 'xpack.agentBuilder.conversationInput.agentSelector.manageAgents.ariaLabel',
- {
- defaultMessage: 'Manage agents',
- }
-);
-
-const agentSelectId = 'agentBuilderAgentSelect';
-const agentListId = `${agentSelectId}_listbox`;
-
-const AgentSelectPopoverButton: React.FC<{
- isPopoverOpen: boolean;
- selectedAgent?: AgentDefinition;
- onClick: () => void;
-}> = ({ isPopoverOpen, selectedAgent, onClick }) => {
- const hasActiveConversation = useHasActiveConversation();
- const iconType = selectedAgent
- ? () =>
- : 'productAgent';
- return (
-
- {selectedAgent?.name ?? selectAgentFallbackButtonLabel}
-
- );
-};
-
-const AgentListFooter: React.FC = () => {
- const { manageAgents } = useUiPrivileges();
- const { createAgentBuilderUrl } = useNavigation();
- const createAgentHref = createAgentBuilderUrl(appPaths.agents.new);
- const manageAgentsHref = createAgentBuilderUrl(appPaths.agents.list);
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-interface AgentSelectDropdownProps {
- selectedAgent?: AgentDefinition;
- onAgentChange: (agentId: string) => void;
- agents?: AgentDefinition[];
-}
-
-export const AgentSelectDropdown: React.FC = ({
- selectedAgent,
- onAgentChange,
- agents = [],
-}) => {
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
-
- const { agentOptions, renderAgentOption } = useAgentOptions({
- agents,
- selectedAgentId: selectedAgent?.id,
- });
- const selectorListStyles = css`
- ${useSelectorListStyles({ listId: agentListId })}
- ${agentListId} .euiSelectableListItem {
- align-items: flex-start;
- }
- `;
-
- const listItemsHeight = agentOptions.length * AGENT_OPTION_ROW_HEIGHT;
- // Calculate height based on item count, capped at max rows
- const listHeight = Math.min(listItemsHeight, getMaxListHeight({ withFooter: true }));
-
- return (
- setIsPopoverOpen(!isPopoverOpen)}
- />
- }
- isOpen={isPopoverOpen}
- anchorPosition="upCenter"
- closePopover={() => setIsPopoverOpen(false)}
- >
- {
- const { checked, key: agentId } = changedOption;
- const isChecked = checked === 'on';
- if (isChecked && agentId) {
- onAgentChange(agentId);
- setIsPopoverOpen(false);
- }
- }}
- singleSelection
- renderOption={(option) => renderAgentOption({ agent: option.agent })}
- height={listHeight}
- listProps={{
- id: agentListId,
- isVirtualized: true,
- rowHeight: AGENT_OPTION_ROW_HEIGHT,
- onFocusBadge: false,
- css: selectorListStyles,
- }}
- >
- {(list) => (
- <>
- {list}
-
- >
- )}
-
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/agent_selector.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/agent_selector.tsx
deleted file mode 100644
index ca80b14a522db..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/agent_selector.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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 { EuiLoadingSpinner } from '@elastic/eui';
-import React from 'react';
-import { useConversationContext } from '../../../../../context/conversation/conversation_context';
-import { useAgentBuilderAgents } from '../../../../../hooks/agents/use_agents';
-import { AgentSelectDropdown } from './agent_select_dropdown';
-
-interface AgentSelectorProps {
- agentId?: string;
-}
-
-export const AgentSelector: React.FC = ({ agentId }) => {
- const { agents, isLoading: isLoadingAgents } = useAgentBuilderAgents();
- const { conversationActions } = useConversationContext();
-
- const handleAgentChange = (newAgentId: string) => {
- conversationActions.setAgentId(newAgentId);
- };
-
- if (isLoadingAgents || !agentId) {
- return ;
- }
-
- const currentAgent = agents.find((agent) => agent.id === agentId);
-
- return (
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts
index 74e0ca1e8a72e..1e0e3ef68aa8d 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts
@@ -28,7 +28,7 @@ export const getMaxListHeight = ({
return height;
};
-const SELECTOR_POPOVER_WIDTH = 275;
+const SELECTOR_POPOVER_WIDTH = 400;
export const selectorPopoverPanelStyles = css`
inline-size: ${SELECTOR_POPOVER_WIDTH}px;
`;
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.tsx
index 6a5ff01a88bb6..6860861659e56 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.tsx
@@ -5,10 +5,8 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiTourStep } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
-import { TourStep, useAgentBuilderTour } from '../../../../context/agent_builder_tour_context';
-import { AgentSelector } from './agent_selector';
import { ConversationActionButton } from './conversation_action_button';
import { ConnectorSelector } from './connector_selector';
@@ -24,39 +22,28 @@ export const InputActions: React.FC = ({
isSubmitDisabled,
resetToPendingMessage,
agentId,
-}) => {
- const { getStepProps } = useAgentBuilderTour();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
+}) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_list_item_styles.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_list_item_styles.ts
new file mode 100644
index 0000000000000..954e3ac632079
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_list_item_styles.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { css } from '@emotion/react';
+import type { EuiThemeComputed } from '@elastic/eui';
+
+export const createConversationListItemStyles = (euiTheme: EuiThemeComputed) => css`
+ text-decoration: none;
+ padding: 6px ${euiTheme.size.s};
+ border-radius: ${euiTheme.border.radius.small};
+ color: ${euiTheme.colors.textParagraph};
+ font-size: ${euiTheme.font.scale.s}${euiTheme.font.defaultUnits};
+ cursor: pointer;
+ background: none;
+ border: none;
+ width: 100%;
+ text-align: left;
+ &:hover {
+ background-color: ${euiTheme.colors.backgroundLightPrimary};
+ color: ${euiTheme.colors.textPrimary};
+ text-decoration: none;
+ }
+`;
+
+export const createActiveConversationListItemStyles = (euiTheme: EuiThemeComputed) => css`
+ ${createConversationListItemStyles(euiTheme)}
+ background-color: ${euiTheme.colors.backgroundLightPrimary};
+ color: ${euiTheme.colors.textPrimary};
+`;
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_search_modal.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_search_modal.tsx
new file mode 100644
index 0000000000000..99c34bb8dacf1
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_search_modal.tsx
@@ -0,0 +1,165 @@
+/*
+ * 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, useState } from 'react';
+import {
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiModal,
+ EuiModalBody,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiText,
+ EuiTextTruncate,
+ useEuiTheme,
+ useGeneratedHtmlId,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import { useConversationList } from '../../hooks/use_conversation_list';
+import {
+ createActiveConversationListItemStyles,
+ createConversationListItemStyles,
+} from './conversation_list_item_styles';
+import { NoConversationsPrompt } from './embeddable_conversation_header/no_conversations_prompt';
+
+const labels = {
+ title: i18n.translate('xpack.agentBuilder.conversationSearchModal.title', {
+ defaultMessage: 'Search chats',
+ }),
+ searchPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.conversationSearchModal.searchPlaceholder',
+ { defaultMessage: 'Search chats' }
+ ),
+ latestChats: i18n.translate('xpack.agentBuilder.conversationSearchModal.latestChats', {
+ defaultMessage: 'Latest chats',
+ }),
+};
+
+const MODAL_WIDTH = 480;
+const LIST_MAX_HEIGHT = 290;
+
+interface ConversationSearchModalProps {
+ agentId: string;
+ currentConversationId?: string;
+ onClose: () => void;
+ onSelectConversation: (conversationId: string) => void;
+}
+
+export const ConversationSearchModal: React.FC = ({
+ agentId,
+ currentConversationId,
+ onClose,
+ onSelectConversation,
+}) => {
+ const [searchValue, setSearchValue] = useState('');
+
+ const { euiTheme } = useEuiTheme();
+ const modalTitleId = useGeneratedHtmlId();
+
+ const { conversations = [], isLoading } = useConversationList({ agentId });
+
+ const sortedConversations = useMemo(
+ () =>
+ [...conversations].sort(
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
+ ),
+ [conversations]
+ );
+
+ const filteredConversations = useMemo(() => {
+ if (!searchValue) return sortedConversations;
+ const lower = searchValue.toLowerCase();
+ return sortedConversations.filter((c) => c.title.toLowerCase().includes(lower));
+ }, [sortedConversations, searchValue]);
+
+ const itemStyles = createConversationListItemStyles(euiTheme);
+ const activeItemStyles = createActiveConversationListItemStyles(euiTheme);
+
+ const listStyles = css`
+ overflow-y: auto;
+ max-height: ${LIST_MAX_HEIGHT}px;
+ margin-top: ${euiTheme.size.s};
+ `;
+
+ const renderList = () => {
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (filteredConversations.length === 0) {
+ return 0} />;
+ }
+
+ return (
+
+ {filteredConversations.map((conversation) => {
+ const isActive = currentConversationId === conversation.id;
+ return (
+
+ {
+ onSelectConversation(conversation.id);
+ onClose();
+ }}
+ data-test-subj={`agentBuilderConversationSearchResult-${conversation.id}`}
+ >
+
+
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+
+ {labels.title}
+
+
+
+ setSearchValue(e.target.value)}
+ data-test-subj="agentBuilderConversationSearchInput"
+ />
+
+
+ {labels.latestChats}
+
+
+ {renderList()}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/conversations_history_list.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/conversations_history_list.tsx
deleted file mode 100644
index f44f96c384318..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/conversations_history_list.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * 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 type { EuiSelectableOption } from '@elastic/eui';
-import {
- EuiButtonIcon,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPopoverTitle,
- EuiSelectable,
- useEuiTheme,
-} from '@elastic/eui';
-import { css } from '@emotion/react';
-import { i18n } from '@kbn/i18n';
-import type { ConversationWithoutRounds } from '@kbn/agent-builder-common';
-import React, { useCallback, useMemo, useState } from 'react';
-import { useConversationContext } from '../../../context/conversation/conversation_context';
-import { useConversationId } from '../../../context/conversation/use_conversation_id';
-import { useConversationList } from '../../../hooks/use_conversation_list';
-import { useNavigation } from '../../../hooks/use_navigation';
-import { appPaths } from '../../../utils/app_paths';
-import { groupConversationsByTime } from '../../../utils/group_conversations';
-import { NoConversationsPrompt } from './no_conversations_prompt';
-import { DeleteConversationModal } from '../delete_conversation_modal';
-
-const EMPTY_CONTAINER_HEIGHT = 300;
-
-const ROW_HEIGHT = 32;
-const MAX_ROWS = 18;
-const MAX_LIST_HEIGHT = ROW_HEIGHT * MAX_ROWS;
-
-const emptyContainerStyles = css`
- height: ${EMPTY_CONTAINER_HEIGHT}px;
- justify-content: center;
- align-items: center;
-`;
-
-const deleteConversationLabel = (title: string) =>
- i18n.translate('xpack.agentBuilder.conversationsHistory.deleteConversation', {
- defaultMessage: 'Delete conversation {title}',
- values: { title },
- });
-
-interface ConversationHistoryListProps {
- onClose?: () => void;
-}
-
-export const ConversationHistoryList: React.FC = ({ onClose }) => {
- const { conversations = [], isLoading } = useConversationList();
- const currentConversationId = useConversationId();
- const { navigateToAgentBuilderUrl } = useNavigation();
- const { isEmbeddedContext, setConversationId } = useConversationContext();
- const { euiTheme } = useEuiTheme();
- const [conversationToDelete, setConversationToDelete] =
- useState(null);
-
- const timeSections = useMemo(() => {
- if (!conversations || conversations.length === 0) {
- return [];
- }
- return groupConversationsByTime(conversations);
- }, [conversations]);
-
- const selectableOptions = useMemo(() => {
- const options: EuiSelectableOption[] = [];
-
- timeSections.forEach(({ label, conversations: sectionConversations }) => {
- // Add group label
- options.push({
- label,
- isGroupLabel: true,
- });
-
- // Add conversation options
- sectionConversations.forEach((conversation) => {
- options.push({
- key: conversation.id,
- label: conversation.title,
- checked: currentConversationId === conversation.id ? 'on' : undefined,
- 'data-test-subj': `conversationItem-${conversation.id}`,
- append: (
- {
- // Must stop click event from propagating to list item which would trigger navigation
- event.stopPropagation();
- setConversationToDelete(conversation);
- }}
- data-test-subj={`deleteConversationButton-${conversation.id}`}
- />
- ),
- data: {
- conversation,
- },
- });
- });
- });
-
- return options;
- }, [timeSections, currentConversationId]);
-
- const handleChange = useCallback(
- (_options: EuiSelectableOption[], _event: unknown, changedOption: EuiSelectableOption) => {
- if (!changedOption?.data?.conversation) return;
-
- const conversation = changedOption.data.conversation as ConversationWithoutRounds;
-
- if (isEmbeddedContext) {
- setConversationId?.(conversation.id);
- } else {
- navigateToAgentBuilderUrl(appPaths.chat.conversation({ conversationId: conversation.id }));
- }
- onClose?.();
- },
- [isEmbeddedContext, onClose, setConversationId, navigateToAgentBuilderUrl]
- );
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (timeSections.length === 0) {
- return (
-
-
-
- );
- }
-
- // remove borders from list items and group labels
- const listStylesOverride = css`
- .euiSelectableListItem:not(:last-of-type) {
- border-block-end: 0;
- }
- .euiSelectableList__groupLabel {
- border-block-end: 0;
- :not(:first-of-type) {
- padding-block-start: ${euiTheme.size.m};
- }
- }
- /* Only show append icon on hover or focus */
- .euiSelectableListItem__append {
- opacity: 0;
- }
- .euiSelectableListItem:hover .euiSelectableListItem__append,
- .euiSelectableListItem-isFocused .euiSelectableListItem__append {
- opacity: 1;
- }
- `;
-
- const listItemsHeight = selectableOptions.length * ROW_HEIGHT;
- // Calculate height based on item count, capped at max rows
- const listHeight = Math.min(listItemsHeight, MAX_LIST_HEIGHT);
-
- return (
- <>
- {
- node?.focus();
- },
- }}
- options={selectableOptions}
- onChange={handleChange}
- singleSelection={true}
- aria-label={i18n.translate('xpack.agentBuilder.conversationsHistory.conversations', {
- defaultMessage: 'Conversations',
- })}
- data-test-subj="agentBuilderConversationList"
- listProps={{
- bordered: false,
- showIcons: false,
- onFocusBadge: false,
- }}
- css={listStylesOverride}
- >
- {(list, search) => (
- <>
- {search}
- {list}
- >
- )}
-
- {
- setConversationToDelete(null);
- }}
- conversation={conversationToDelete ?? undefined}
- />
- >
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/conversations_history_popover.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/conversations_history_popover.tsx
deleted file mode 100644
index 3557e32836092..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/conversations_history_popover.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 { css } from '@emotion/react';
-import { EuiPopover } from '@elastic/eui';
-import React from 'react';
-import { ConversationHistoryList } from './conversations_history_list';
-
-interface ConversationsHistoryPopoverProps {
- button: React.ReactElement;
- isOpen: boolean;
- closePopover: () => void;
-}
-
-const popoverPanelStyles = css`
- width: 400px;
-`;
-
-export const ConversationsHistoryPopover: React.FC = ({
- button,
- isOpen,
- closePopover,
-}) => {
- return (
-
-
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/no_conversations_prompt.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/no_conversations_prompt.tsx
deleted file mode 100644
index a9389eddc11dc..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_history/no_conversations_prompt.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-
-export const NoConversationsPrompt: React.FC = () => {
- return (
-
-
-
-
-
-
-
- {i18n.translate('xpack.agentBuilder.conversationsHistory.noConversations', {
- defaultMessage: "You haven't started any conversations yet.",
- })}
-
-
-
-
- );
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx
index 787bb582bfb19..b3c39d5c05cd2 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx
@@ -7,12 +7,9 @@
import { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
-import { i18n } from '@kbn/i18n';
-import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import React from 'react';
import { Conversation } from './conversation';
import { ConversationHeader } from './conversation_header/conversation_header';
-import { AgentBuilderTourProvider } from '../../context/agent_builder_tour_context';
import { RoutedConversationsProvider } from '../../context/conversation/routed_conversations_provider';
import { SendMessageProvider } from '../../context/send_message/send_message_context';
import { conversationBackgroundStyles, headerHeight } from './conversation.styles';
@@ -20,17 +17,24 @@ import { conversationBackgroundStyles, headerHeight } from './conversation.style
export const AgentBuilderConversationsView: React.FC<{}> = () => {
const { euiTheme } = useEuiTheme();
- const mainStyles = css`
- border: none;
+ const containerStyles = css`
+ display: flex;
+ flex-direction: column;
+ height: var(--kbn-application--content-height);
${conversationBackgroundStyles(euiTheme)}
`;
+
const headerStyles = css`
- justify-content: center;
+ flex-shrink: 0;
height: ${headerHeight}px;
+ display: flex;
+ align-items: center;
+ padding: ${euiTheme.size.m};
`;
+
const contentStyles = css`
width: 100%;
- height: 100%;
+ flex: 1;
max-block-size: calc(var(--kbn-application--content-height) - ${headerHeight}px);
display: flex;
justify-content: center;
@@ -38,51 +42,17 @@ export const AgentBuilderConversationsView: React.FC<{}> = () => {
padding: 0 ${euiTheme.size.base} ${euiTheme.size.base} ${euiTheme.size.base};
`;
- const labels = {
- header: i18n.translate('xpack.agentBuilder.conversationsView.header', {
- defaultMessage: 'Conversation header',
- }),
- content: i18n.translate('xpack.agentBuilder.conversationsView.content', {
- defaultMessage: 'Conversation content',
- }),
- };
-
return (
-
-
-
-
-
-
-
-
-
-
+
);
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/delete_conversation_modal.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/delete_conversation_modal.tsx
index 119387e5f4bb8..f0195162142fa 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/delete_conversation_modal.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/delete_conversation_modal.tsx
@@ -53,7 +53,7 @@ export const DeleteConversationModal: React.FC = (
title={
}
titleProps={{ id: confirmModalTitleId }}
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/agents_popover_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/agents_popover_view.tsx
new file mode 100644
index 0000000000000..d75f24f7ef66e
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/agents_popover_view.tsx
@@ -0,0 +1,164 @@
+/*
+ * 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 type { EuiSelectableOption } from '@elastic/eui';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiSelectable,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import type { AgentDefinition } from '@kbn/agent-builder-common';
+import { useConversationContext } from '../../../context/conversation/conversation_context';
+import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents';
+import { useNavigation } from '../../../hooks/use_navigation';
+import { appPaths } from '../../../utils/app_paths';
+import { useAgentOptions } from '../../common/agent_selector/use_agent_options';
+import { useSelectorListStyles } from '../conversation_input/input_actions/input_actions.styles';
+
+const labels = {
+ availableAgents: i18n.translate('xpack.agentBuilder.embeddableAgentsView.availableAgents', {
+ defaultMessage: 'Available agents',
+ }),
+ agentDetails: i18n.translate('xpack.agentBuilder.embeddableAgentsView.agentDetails', {
+ defaultMessage: 'Agent details',
+ }),
+ selectAgent: i18n.translate('xpack.agentBuilder.embeddableAgentsView.selectAgent', {
+ defaultMessage: 'Select an agent',
+ }),
+};
+
+type AgentOption = EuiSelectableOption<{ agent?: AgentDefinition }>;
+
+interface AgentsPopoverViewProps {
+ panelHeight: number;
+ panelWidth: number;
+ onBack: () => void;
+ onClose: () => void;
+}
+
+export const AgentsPopoverView: React.FC = ({
+ panelHeight,
+ panelWidth,
+ onBack,
+ onClose,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const { agentId, setAgentId } = useConversationContext();
+ const { agents } = useAgentBuilderAgents();
+ const { agentOptions, renderAgentOption } = useAgentOptions({ agents, selectedAgentId: agentId });
+
+ const agentListStyles = css`
+ ${useSelectorListStyles({ listId: 'agentBuilderEmbeddableAgentsList' })}
+ agentBuilderEmbeddableAgentsList .euiSelectableListItem {
+ align-items: flex-start;
+ }
+ `;
+
+ const { createAgentBuilderUrl } = useNavigation();
+ const agentDetailsHref = agentId
+ ? createAgentBuilderUrl(appPaths.agent.overview({ agentId }))
+ : undefined;
+
+ const handleAgentChange = (
+ _options: AgentOption[],
+ _event: unknown,
+ changedOption: AgentOption
+ ) => {
+ const { checked, key: newAgentId } = changedOption;
+ if (checked === 'on' && newAgentId) {
+ setAgentId?.(newAgentId);
+ onClose();
+ }
+ };
+
+ const rowPaddingStyles = css`
+ padding: ${euiTheme.size.base};
+ `;
+
+ return (
+
+ {/* Header row — back button + agent details link */}
+
+
+
+
+ {labels.availableAgents}
+
+
+
+
+ {labels.agentDetails}
+
+
+
+
+
+
+
+ {/* Agents list — scrollable */}
+
+ renderAgentOption({ agent: option.agent })}
+ listProps={{
+ id: 'agentBuilderEmbeddableAgentsList',
+ isVirtualized: false,
+ onFocusBadge: false,
+ bordered: false,
+ css: agentListStyles,
+ }}
+ >
+ {(list) => list}
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/conversations_popover_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/conversations_popover_view.tsx
new file mode 100644
index 0000000000000..b3eb21a8705a9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/conversations_popover_view.tsx
@@ -0,0 +1,157 @@
+/*
+ * 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, { useState } from 'react';
+import {
+ EuiButton,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiIcon,
+ EuiText,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import { useConversationContext } from '../../../context/conversation/conversation_context';
+import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents';
+import { useAgentId } from '../../../hooks/use_conversation';
+import { AgentAvatar } from '../../common/agent_avatar';
+import { EmbeddableConversationList } from './embeddable_conversation_list';
+
+const labels = {
+ newChat: i18n.translate('xpack.agentBuilder.embeddableConversationsView.newChat', {
+ defaultMessage: 'New chat',
+ }),
+ searchPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.embeddableConversationsView.searchPlaceholder',
+ { defaultMessage: 'Search chats' }
+ ),
+ availableAgents: i18n.translate(
+ 'xpack.agentBuilder.embeddableConversationsView.availableAgents',
+ { defaultMessage: 'Available agents' }
+ ),
+};
+
+interface ConversationsPopoverViewProps {
+ panelHeight: number;
+ panelWidth: number;
+ onSwitchToAgents: () => void;
+ onClose: () => void;
+}
+
+export const ConversationsPopoverView: React.FC = ({
+ panelHeight,
+ panelWidth,
+ onSwitchToAgents,
+ onClose,
+}) => {
+ const [searchValue, setSearchValue] = useState('');
+
+ const { euiTheme } = useEuiTheme();
+ const { setConversationId } = useConversationContext();
+ const { agents } = useAgentBuilderAgents();
+ const agentId = useAgentId();
+
+ const currentAgent = agents.find((a) => a.id === agentId);
+
+ const handleNewChat = () => {
+ setConversationId?.(undefined);
+ onClose();
+ };
+
+ const agentRowStyles = css`
+ padding: ${euiTheme.size.base};
+ cursor: pointer;
+ &:hover {
+ background: ${euiTheme.colors.backgroundBaseSubdued};
+ }
+ `;
+
+ const searchRowStyles = css`
+ padding: ${euiTheme.size.s} ${euiTheme.size.m};
+ `;
+
+ const listStyles = css`
+ padding: ${euiTheme.size.xs} ${euiTheme.size.s};
+ overflow-y: auto;
+ min-height: 0;
+ `;
+
+ return (
+
+ {/* Agent row — click to switch to agents view */}
+
+
+ {currentAgent && (
+
+
+
+ )}
+
+
+ {currentAgent?.name}
+
+
+
+
+
+
+
+
+
+
+ {/* Search + New chat */}
+
+
+
+ setSearchValue(e.target.value)}
+ data-test-subj="agentBuilderEmbeddableConversationSearch"
+ />
+
+
+
+ {labels.newChat}
+
+
+
+
+
+
+
+ {/* Conversation list — scrollable */}
+
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_conversation_header.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_conversation_header.tsx
new file mode 100644
index 0000000000000..e387fc6773fbe
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_conversation_header.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 { css } from '@emotion/react';
+import { EuiText, useEuiTheme } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { ConversationRightActions } from '../conversation_header/conversation_actions_right';
+import { ConversationTitle } from '../conversation_header/conversation_title';
+import { EmbeddableMenuButton } from './embeddable_menu_button';
+import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents';
+import { useAgentId, useHasActiveConversation } from '../../../hooks/use_conversation';
+
+const newConversationTitleLabel = i18n.translate(
+ 'xpack.agentBuilder.embeddableHeader.newConversation',
+ {
+ defaultMessage: 'New Conversation',
+ }
+);
+
+interface EmbeddableConversationHeaderProps {
+ onClose?: () => void;
+ ariaLabelledBy?: string;
+}
+
+export const EmbeddableConversationHeader: React.FC = ({
+ onClose,
+ ariaLabelledBy,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const agentId = useAgentId();
+ const { agents } = useAgentBuilderAgents();
+ const hasActiveConversation = useHasActiveConversation();
+ const currentAgent = agents.find((a) => a.id === agentId);
+
+ return (
+
+
+
+ {/* Center: title + agent subtitle — overflow hidden ensures nothing escapes the column */}
+
+ {hasActiveConversation ? (
+
+ ) : (
+
+ {newConversationTitleLabel}
+
+ )}
+ {currentAgent && (
+
+ {currentAgent.name}
+
+ )}
+
+
+ {/* Right: kebab menu + close */}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_conversation_list.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_conversation_list.tsx
new file mode 100644
index 0000000000000..fd8b347647aa2
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_conversation_list.tsx
@@ -0,0 +1,96 @@
+/*
+ * 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 {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiTextTruncate,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { useConversationContext } from '../../../context/conversation/conversation_context';
+import { useConversationList } from '../../../hooks/use_conversation_list';
+import {
+ createConversationListItemStyles,
+ createActiveConversationListItemStyles,
+} from '../conversation_list_item_styles';
+import { NoConversationsPrompt } from './no_conversations_prompt';
+
+interface EmbeddableConversationListProps {
+ searchValue: string;
+ onClose: () => void;
+}
+
+export const EmbeddableConversationList: React.FC = ({
+ searchValue,
+ onClose,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const { agentId, conversationId, setConversationId } = useConversationContext();
+ const { conversations = [], isLoading } = useConversationList({ agentId });
+
+ const sortedConversations = useMemo(
+ () =>
+ [...conversations].sort(
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
+ ),
+ [conversations]
+ );
+
+ const filteredConversations = useMemo(() => {
+ if (!searchValue) return sortedConversations;
+ const lower = searchValue.toLowerCase();
+ return sortedConversations.filter((c) => c.title.toLowerCase().includes(lower));
+ }, [sortedConversations, searchValue]);
+
+ const itemStyles = createConversationListItemStyles(euiTheme);
+ const activeItemStyles = createActiveConversationListItemStyles(euiTheme);
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (filteredConversations.length === 0) {
+ return 0} />;
+ }
+
+ return (
+
+ {filteredConversations.map((conversation) => {
+ const isActive = conversationId === conversation.id;
+ return (
+
+ {
+ setConversationId?.(conversation.id);
+ onClose();
+ }}
+ data-test-subj={`agentBuilderEmbeddableConversation-${conversation.id}`}
+ >
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_menu_button.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_menu_button.tsx
new file mode 100644
index 0000000000000..c7740bf39ce6a
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/embeddable_menu_button.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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, { useState } from 'react';
+import { EuiButtonIcon, EuiPopover } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { ConversationsPopoverView } from './conversations_popover_view';
+import { AgentsPopoverView } from './agents_popover_view';
+
+const POPOVER_HEIGHT = 500;
+const POPOVER_WIDTH = 400;
+
+const openMenuLabel = i18n.translate('xpack.agentBuilder.embeddableMenuButton.openMenu', {
+ defaultMessage: 'Open navigation menu',
+});
+
+export const EmbeddableMenuButton: React.FC = () => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const [view, setView] = useState<'conversations' | 'agents'>('conversations');
+
+ const closePopover = () => {
+ setIsPopoverOpen(false);
+ setView('conversations');
+ };
+
+ const button = (
+ setIsPopoverOpen((v) => !v)}
+ data-test-subj="agentBuilderEmbeddableMenuButton"
+ />
+ );
+
+ return (
+
+ {view === 'conversations' ? (
+ setView('agents')}
+ onClose={closePopover}
+ />
+ ) : (
+ setView('conversations')}
+ onClose={closePopover}
+ />
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/no_conversations_prompt.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/no_conversations_prompt.tsx
new file mode 100644
index 0000000000000..42ac453d5482d
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/embeddable_conversation_header/no_conversations_prompt.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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, EuiIcon, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+interface NoConversationsPromptProps {
+ isFiltered: boolean;
+}
+
+export const NoConversationsPrompt: React.FC = ({ isFiltered }) => {
+ const message = isFiltered
+ ? i18n.translate('xpack.agentBuilder.embeddableConversationList.noResults', {
+ defaultMessage: 'No conversations match your search.',
+ })
+ : i18n.translate('xpack.agentBuilder.embeddableConversationList.noConversations', {
+ defaultMessage: "You haven't started any conversations yet.",
+ });
+
+ return (
+
+
+
+
+
+
+ {message}
+
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/rename_conversation_modal.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/rename_conversation_modal.tsx
new file mode 100644
index 0000000000000..f87e38739f757
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/rename_conversation_modal.tsx
@@ -0,0 +1,135 @@
+/*
+ * 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, useEffect, useState } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFieldText,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ useGeneratedHtmlId,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+import { formatAgentBuilderErrorMessage } from '@kbn/agent-builder-browser';
+import { useConversationContext } from '../../context/conversation/conversation_context';
+import { useConversationId } from '../../context/conversation/use_conversation_id';
+import { useConversationTitle } from '../../hooks/use_conversation';
+import { useToasts } from '../../hooks/use_toasts';
+
+const labels = {
+ inputPlaceholder: i18n.translate('xpack.agentBuilder.renameConversationModal.inputPlaceholder', {
+ defaultMessage: 'Enter conversation name',
+ }),
+ renameErrorToast: i18n.translate('xpack.agentBuilder.renameConversationModal.renameErrorToast', {
+ defaultMessage: 'Failed to rename conversation',
+ }),
+};
+
+interface RenameConversationModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export const RenameConversationModal: React.FC = ({
+ isOpen,
+ onClose,
+}) => {
+ const conversationId = useConversationId();
+ const { title } = useConversationTitle();
+ const { conversationActions } = useConversationContext();
+ const { addErrorToast } = useToasts();
+ const [newTitle, setNewTitle] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Sync input to current title each time the modal opens
+ useEffect(() => {
+ if (isOpen) {
+ setNewTitle(title || '');
+ }
+ }, [isOpen, title]);
+
+ const isDirty = newTitle.trim() !== (title || '').trim();
+
+ const handleSave = useCallback(async () => {
+ if (!conversationId || !newTitle.trim() || !isDirty) return;
+ setIsLoading(true);
+ try {
+ await conversationActions.renameConversation(conversationId, newTitle.trim());
+ onClose();
+ } catch (error) {
+ addErrorToast({
+ title: labels.renameErrorToast,
+ text: formatAgentBuilderErrorMessage(error),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }, [conversationId, newTitle, isDirty, conversationActions, onClose, addErrorToast]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') handleSave();
+ if (e.key === 'Escape') onClose();
+ },
+ [handleSave, onClose]
+ );
+
+ const modalTitleId = useGeneratedHtmlId();
+
+ if (!isOpen || !conversationId) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ setNewTitle(e.target.value)}
+ onKeyDown={handleKeyDown}
+ data-test-subj="renameConversationModalInput"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
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..15c1fa6713e25
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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, useState } from 'react';
+
+import { EuiWindowEvent, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import { isMac } from '@kbn/shared-ux-utility';
+
+import {
+ CONDENSED_SIDEBAR_WIDTH,
+ SIDEBAR_WIDTH,
+ UnifiedSidebar,
+} from './unified_sidebar/unified_sidebar';
+
+interface AppLayoutProps {
+ children: React.ReactNode;
+}
+
+export const AppLayout: React.FC = ({ children }) => {
+ const { euiTheme } = useEuiTheme();
+ const [isCondensed, setIsCondensed] = useState(false);
+
+ const onKeyDown = useCallback((event: KeyboardEvent) => {
+ if ((event.code === 'Period' || event.key === '.') && (isMac ? event.metaKey : event.ctrlKey)) {
+ event.preventDefault();
+ setIsCondensed((v) => !v);
+ }
+ }, []);
+
+ const sidebarStyles = css`
+ @media (max-width: ${euiTheme.breakpoint.m - 1}px) {
+ display: none;
+ }
+ `;
+
+ const contentStyles = css`
+ overflow: auto;
+ background-color: ${euiTheme.colors.backgroundBasePlain};
+ `;
+
+ return (
+ <>
+
+ setIsCondensed((v) => !v)}
+ />
+ }
+ pageSideBarProps={{
+ minWidth: isCondensed ? CONDENSED_SIDEBAR_WIDTH : SIDEBAR_WIDTH,
+ css: sidebarStyles,
+ }}
+ >
+
+ {children}
+
+
+ >
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/agent_selector.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/agent_selector.tsx
new file mode 100644
index 0000000000000..d1a37919f1d26
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/agent_selector.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 useLocalStorage from 'react-use/lib/useLocalStorage';
+
+import { i18n } from '@kbn/i18n';
+
+import { useAgentBuilderAgents } from '../../../../hooks/agents/use_agents';
+import { storageKeys } from '../../../../storage_keys';
+import { AgentSelectorDropdown } from '../../../common/agent_selector/agent_selector_dropdown';
+
+const deletedAgentLabel = i18n.translate('xpack.agentBuilder.sidebar.agentSelector.deletedAgent', {
+ defaultMessage: '(Deleted 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 currentAgent = agents.find((a) => a.id === agentId);
+
+ const handleAgentChange = useCallback(
+ (newAgentId: string) => {
+ setStoredAgentId(newAgentId);
+ navigate(getNavigationPath(newAgentId));
+ },
+ [navigate, setStoredAgentId, getNavigationPath]
+ );
+
+ return (
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_header.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_header.tsx
new file mode 100644
index 0000000000000..9e5f5d9086fdc
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_header.tsx
@@ -0,0 +1,159 @@
+/*
+ * 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 { useNavigate } from 'react-router-dom-v5-compat';
+
+import {
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ useEuiTheme,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { css } from '@emotion/react';
+
+import { useAgentBuilderAgents } from '../../../../hooks/agents/use_agents';
+import { appPaths } from '../../../../utils/app_paths';
+import { AgentAvatar } from '../../../common/agent_avatar';
+import { AgentSelector } from './agent_selector';
+
+const labels = {
+ manageComponents: i18n.translate('xpack.agentBuilder.sidebar.header.manageComponents', {
+ defaultMessage: 'Manage components',
+ }),
+ back: i18n.translate('xpack.agentBuilder.sidebar.header.back', {
+ defaultMessage: 'Back',
+ }),
+ toggleSidebar: i18n.translate('xpack.agentBuilder.sidebar.header.toggleSidebar', {
+ defaultMessage: 'Toggle sidebar',
+ }),
+};
+
+interface SidebarHeaderProps {
+ sidebarView: 'conversation' | 'manage';
+ agentId: string;
+ getNavigationPath: (newAgentId: string) => string;
+ isCondensed: boolean;
+ onToggleCondensed: () => void;
+}
+
+export const SidebarHeader: React.FC = ({
+ sidebarView,
+ agentId,
+ getNavigationPath,
+ isCondensed,
+ onToggleCondensed,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const navigate = useNavigate();
+ const { agents } = useAgentBuilderAgents();
+ const currentAgent = agents.find((a) => a.id === agentId);
+
+ const headerStyles = css`
+ gap: ${euiTheme.size.s};
+ padding: ${euiTheme.size.base} ${euiTheme.size.l};
+ flex-grow: 0;
+ `;
+
+ const condensedHeaderStyles = css`
+ padding: ${euiTheme.size.base} 0;
+ flex-grow: 0;
+ align-items: center;
+ `;
+
+ if (isCondensed) {
+ return (
+
+
+
+
+ {currentAgent && sidebarView === 'conversation' && (
+
+
+
+ )}
+
+ );
+ }
+
+ const sidebarToggle = (
+
+
+
+ );
+
+ const renderTitle = () => {
+ if (sidebarView === 'conversation') {
+ return (
+
+
+ {currentAgent && (
+
+ )}
+
+ {sidebarToggle}
+
+ );
+ }
+ if (sidebarView === 'manage') {
+ return (
+
+
+ navigate(appPaths.root)}
+ >
+ {labels.manageComponents}
+
+
+ {sidebarToggle}
+
+ );
+ }
+ return null;
+ };
+
+ return (
+
+ {renderTitle()}
+ {sidebarView === 'conversation' && (
+
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_nav_list.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_nav_list.tsx
new file mode 100644
index 0000000000000..ec0b2419158de
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_nav_list.tsx
@@ -0,0 +1,66 @@
+/*
+ * 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 { Link } from 'react-router-dom-v5-compat';
+
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+import type { SidebarNavItem } from '../../../../route_config';
+
+interface SidebarNavListProps {
+ items: SidebarNavItem[];
+ isActive: (path: string) => boolean;
+ onItemClick?: () => void;
+}
+
+export const SidebarNavList: React.FC = ({ items, isActive, onItemClick }) => {
+ const { euiTheme } = useEuiTheme();
+
+ const baseLinkStyles = css`
+ display: flex;
+ align-items: center;
+ gap: ${euiTheme.size.s};
+ width: 100%;
+ padding: 6px ${euiTheme.size.s};
+ border-radius: ${euiTheme.border.radius.small};
+ text-decoration: none;
+ color: ${euiTheme.colors.textParagraph};
+
+ &:hover {
+ background-color: ${euiTheme.colors.backgroundLightPrimary};
+ color: ${euiTheme.colors.textPrimary};
+ text-decoration: none;
+ }
+ `;
+
+ const activeLinkStyles = css`
+ ${baseLinkStyles}
+ background-color: ${euiTheme.colors.backgroundLightPrimary};
+ color: ${euiTheme.colors.textPrimary};
+ `;
+
+ return (
+
+
+ {items.map((item) => (
+
+
+ {item.icon && }
+ {item.label}
+
+
+ ))}
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.test.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.test.tsx
new file mode 100644
index 0000000000000..033913dbb75cc
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.test.tsx
@@ -0,0 +1,129 @@
+/*
+ * 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 '@testing-library/jest-dom';
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from '@kbn/shared-ux-router';
+
+jest.mock('../../../hooks/use_kibana', () => ({
+ useKibana: () => ({ services: {} }),
+}));
+
+jest.mock('../../../hooks/use_navigation', () => ({
+ useNavigation: () => ({ navigateToAgentBuilderUrl: jest.fn() }),
+}));
+
+jest.mock('../../../hooks/agents/use_agents', () => ({
+ useAgentBuilderAgents: () => ({ isFetched: true, agents: [] }),
+}));
+
+jest.mock('../../../hooks/agents/use_validate_agent_id', () => ({
+ useValidateAgentId: () => () => true,
+}));
+
+jest.mock('../../../hooks/use_last_agent_id', () => ({
+ useLastAgentId: () => 'test-agent',
+}));
+
+jest.mock('../../../hooks/use_conversation_list', () => ({
+ useConversationList: () => ({ conversations: [], isLoading: false, refresh: jest.fn() }),
+}));
+
+jest.mock('../../../hooks/use_feature_flags', () => ({
+ useFeatureFlags: () => ({ experimental: false, connectors: false }),
+}));
+
+jest.mock('./shared/sidebar_header', () => ({
+ SidebarHeader: () => null,
+}));
+
+jest.mock('react-use/lib/useLocalStorage', () => ({
+ __esModule: true,
+ default: () => [undefined, jest.fn()],
+}));
+
+import { UnifiedSidebar } from './unified_sidebar';
+
+const renderSidebar = (path: string) =>
+ render(
+
+
+
+ );
+
+describe('UnifiedSidebar', () => {
+ describe('conversation sidebar', () => {
+ it('renders for agent root route', () => {
+ renderSidebar('/agents/my-agent');
+ expect(screen.getByTestId('agentBuilderSidebar-conversation')).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-manage')).not.toBeInTheDocument();
+ });
+
+ it('renders for overview route', () => {
+ renderSidebar('/agents/my-agent/overview');
+ 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-conversation')).toBeInTheDocument();
+ expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
+ });
+
+ it('renders for plugins route', () => {
+ renderSidebar('/agents/my-agent/plugins');
+ 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-conversation')).toBeInTheDocument();
+ expect(screen.queryByTestId('agentBuilderSidebar-manage')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('manage sidebar', () => {
+ it('renders for manage agents route', () => {
+ renderSidebar('/manage/agents');
+ expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
+ expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
+ });
+
+ it('renders for manage tools route', () => {
+ renderSidebar('/manage/tools');
+ expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
+ expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
+ });
+
+ it('renders for manage skills route', () => {
+ renderSidebar('/manage/skills');
+ expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
+ expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
+ });
+
+ it('renders for manage plugins route', () => {
+ renderSidebar('/manage/plugins');
+ expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
+ expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
+ });
+
+ it('renders for manage connectors route', () => {
+ renderSidebar('/manage/connectors');
+ expect(screen.getByTestId('agentBuilderSidebar-manage')).toBeInTheDocument();
+ expect(screen.queryByTestId('agentBuilderSidebar-conversation')).not.toBeInTheDocument();
+ });
+ });
+});
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..0e4e373c87db4
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx
@@ -0,0 +1,96 @@
+/*
+ * 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, useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+
+import { EuiFlexGroup, EuiPanel, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common';
+import { storageKeys } from '../../../storage_keys';
+import { getSidebarViewForRoute, getAgentIdFromPath } from '../../../route_config';
+import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents';
+import { useValidateAgentId } from '../../../hooks/agents/use_validate_agent_id';
+import { ConversationSidebarView } from './views/conversation_view';
+import { ManageSidebarView } from './views/manage_view';
+import { SidebarHeader } from './shared/sidebar_header';
+import { appPaths } from '../../../utils/app_paths';
+
+export const SIDEBAR_WIDTH = 300;
+export const CONDENSED_SIDEBAR_WIDTH = 64;
+
+interface UnifiedSidebarProps {
+ isCondensed: boolean;
+ onToggleCondensed: () => void;
+}
+
+export const UnifiedSidebar: React.FC = ({
+ isCondensed,
+ onToggleCondensed,
+}) => {
+ const location = useLocation();
+ const sidebarView = getSidebarViewForRoute(location.pathname);
+ const agentIdFromPath = getAgentIdFromPath(location.pathname) ?? agentBuilderDefaultAgentId;
+ const [, setStoredAgentId] = useLocalStorage(storageKeys.agentId);
+ const { isFetched: isAgentsFetched } = useAgentBuilderAgents();
+ const validateAgentId = useValidateAgentId();
+ const { euiTheme } = useEuiTheme();
+
+ useEffect(() => {
+ // Wait for agents to load before validating — prevents falsely skipping valid IDs during initial load
+ if (isAgentsFetched && agentIdFromPath && validateAgentId(agentIdFromPath)) {
+ setStoredAgentId(agentIdFromPath);
+ }
+ }, [isAgentsFetched, agentIdFromPath, validateAgentId, setStoredAgentId]);
+
+ const getNavigationPath = useCallback(
+ (newAgentId: string) => appPaths.agent.root({ agentId: newAgentId }),
+ []
+ );
+
+ const sidebarStyles = css`
+ width: ${isCondensed ? CONDENSED_SIDEBAR_WIDTH : SIDEBAR_WIDTH}px;
+ height: 100%;
+ border-radius: 0;
+ border-right: 1px solid ${euiTheme.colors.borderBaseSubdued};
+ display: flex;
+ flex-direction: column;
+ `;
+
+ const sidebarContentStyles = css`
+ flex: 1;
+ position: relative;
+ overflow: hidden;
+ `;
+
+ return (
+
+
+ {!isCondensed && (
+
+ {sidebarView === 'conversation' && }
+ {sidebarView === 'manage' && }
+
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_footer.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_footer.tsx
new file mode 100644
index 0000000000000..f1d127320df0b
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_footer.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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, EuiHorizontalRule } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+
+import { appPaths } from '../../../../../utils/app_paths';
+import { useNavigation } from '../../../../../hooks/use_navigation';
+import { SidebarLink } from './sidebar_link';
+
+export const ConversationFooter: React.FC = () => {
+ const { navigateToAgentBuilderUrl } = useNavigation();
+
+ return (
+
+
+
+
+
+ {
+ e.preventDefault();
+ navigateToAgentBuilderUrl(appPaths.manage.agents);
+ }}
+ />
+
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_list.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_list.tsx
new file mode 100644
index 0000000000000..cdc25b9b6bc64
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_list.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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,
+ EuiLoadingSpinner,
+ EuiTextTruncate,
+ useEuiTheme,
+} from '@elastic/eui';
+import { appPaths } from '../../../../../utils/app_paths';
+import { useConversationList } from '../../../../../hooks/use_conversation_list';
+import {
+ createConversationListItemStyles,
+ createActiveConversationListItemStyles,
+} from '../../../../conversations/conversation_list_item_styles';
+import { NoConversationsPrompt } from '../../../../conversations/embeddable_conversation_header/no_conversations_prompt';
+
+interface ConversationListProps {
+ agentId: string;
+ currentConversationId: string | undefined;
+ onItemClick?: () => void;
+}
+
+export const ConversationList: React.FC = ({
+ agentId,
+ currentConversationId,
+ onItemClick,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const { conversations = [], isLoading } = useConversationList({ agentId });
+
+ const sortedConversations = useMemo(
+ () =>
+ [...conversations].sort(
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
+ ),
+ [conversations]
+ );
+
+ const linkStyles = createConversationListItemStyles(euiTheme);
+ const activeLinkStyles = createActiveConversationListItemStyles(euiTheme);
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (sortedConversations.length === 0) {
+ return ;
+ }
+
+ return (
+
+ {sortedConversations.map((conversation) => {
+ const isActive = currentConversationId === conversation.id;
+ return (
+
+
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/index.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/index.tsx
new file mode 100644
index 0000000000000..dc5bf9594b975
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/index.tsx
@@ -0,0 +1,279 @@
+/*
+ * 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, useMemo, useState, useRef } from 'react';
+import { useLocation } from 'react-router-dom';
+
+import {
+ EuiAccordion,
+ EuiButton,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiSpacer,
+ EuiText,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+
+import { i18n } from '@kbn/i18n';
+import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common';
+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 { useConversationList } from '../../../../../hooks/use_conversation_list';
+import { SidebarNavList } from '../../shared/sidebar_nav_list';
+
+import { ConversationFooter } from './conversation_footer';
+import { ConversationList } from './conversation_list';
+import { ConversationSearchModal } from '../../../../conversations/conversation_search_modal';
+
+const customizeLabel = i18n.translate('xpack.agentBuilder.sidebar.conversation.customize', {
+ defaultMessage: 'Customize',
+});
+
+const newLabel = i18n.translate('xpack.agentBuilder.sidebar.conversation.new', {
+ defaultMessage: 'New',
+});
+
+const searchChatsAriaLabel = i18n.translate('xpack.agentBuilder.sidebar.conversation.searchChats', {
+ defaultMessage: 'Search chats',
+});
+
+const chatsLabel = i18n.translate('xpack.agentBuilder.sidebar.conversation.chats', {
+ defaultMessage: 'Chats',
+});
+
+const containerStyles = css`
+ display: flex;
+ gap: 0;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+`;
+
+const chatsAccordionStyles = css`
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .euiAccordion__childWrapper {
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ }
+
+ .euiAccordion__children {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+`;
+
+export const ConversationSidebarView: React.FC = () => {
+ const { pathname } = useLocation();
+ const agentId = getAgentIdFromPath(pathname) ?? agentBuilderDefaultAgentId;
+ const conversationId = getConversationIdFromPath(pathname);
+ const { euiTheme } = useEuiTheme();
+ const { navigateToAgentBuilderUrl } = useNavigation();
+ 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 navItems = useMemo(
+ () => getAgentSettingsNavItems(agentId, featureFlags),
+ [agentId, featureFlags]
+ );
+
+ const isActive = (path: string) => pathname === path;
+
+ const isAnyNavItemActive = navItems.some((item) => isActive(item.path));
+
+ const [isCustomizeOpen, setIsCustomizeOpen] = useState(false);
+ const [isChatsOpen, setIsChatsOpen] = useState(true);
+ const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
+
+ // When the user refreshes on an agent settings route, ensure the Customize accordion is open
+ useEffect(() => {
+ if (isAnyNavItemActive && !isCustomizeOpen && !hasSetCustomiseAccordionFirstTime.current) {
+ setIsCustomizeOpen(true);
+ setIsChatsOpen(false);
+ hasSetCustomiseAccordionFirstTime.current = true;
+ }
+ }, [isAnyNavItemActive, isCustomizeOpen]);
+
+ useEffect(() => {
+ // Once agents have loaded, redirect to the last valid agent if the current agent ID
+ // is not recognised — but only when there is no conversation ID in the URL (new
+ // conversation route). Existing conversations for a deleted agent are intentionally
+ // shown read-only with the input disabled.
+
+ // We also check that lastAgentId itself is valid before redirecting: if local storage
+ // holds a stale/invalid ID too, navigating to it would trigger this effect again and
+ // cause an infinite redirect loop.
+ if (
+ isAgentsFetched &&
+ !conversationId &&
+ !validateAgentId(agentId) &&
+ validateAgentId(lastAgentId)
+ ) {
+ navigateToAgentBuilderUrl(appPaths.agent.root({ agentId: lastAgentId }));
+ }
+ }, [
+ isAgentsFetched,
+ conversationId,
+ agentId,
+ lastAgentId,
+ validateAgentId,
+ navigateToAgentBuilderUrl,
+ ]);
+
+ const accordionButtonStyles = css`
+ padding: ${euiTheme.size.s} ${euiTheme.size.base};
+
+ .euiAccordion__triggerWrapper {
+ padding: 0;
+ }
+
+ .euiAccordion__iconButton {
+ color: ${euiTheme.colors.textParagraph};
+ }
+ `;
+
+ const buttonStyles = css`
+ color: ${euiTheme.colors.textSubdued};
+ font-weight: ${euiTheme.font.weight.semiBold};
+ `;
+
+ const conversationListStyles = css`
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ `;
+
+ return (
+
+
+
+
+ {/* Customize accordion - with agent settings nav items */}
+
+
+ {/* Chats accordion - with conversation list */}
+
+
+
+
+ {isSearchModalOpen && (
+ setIsSearchModalOpen(false)}
+ onSelectConversation={(id) => {
+ navigateToAgentBuilderUrl(
+ appPaths.agent.conversations.byId({ agentId, conversationId: id })
+ );
+ setIsSearchModalOpen(false);
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/sidebar_link.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/sidebar_link.tsx
new file mode 100644
index 0000000000000..d49a0edb9f4d4
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/sidebar_link.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { EuiIcon, EuiText, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+interface SidebarLinkProps {
+ label: string;
+ href: string;
+ onClick: (e: React.MouseEvent) => void;
+}
+
+export const SidebarLink: React.FC = ({ label, href, onClick }) => {
+ const { euiTheme } = useEuiTheme();
+
+ const wrapperStyles = css`
+ display: flex;
+ align-items: center;
+ height: 100%;
+ padding: ${euiTheme.size.base};
+ `;
+
+ const linkStyles = css`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ text-decoration: none;
+ color: inherit;
+ padding: 6px ${euiTheme.size.s};
+ border-radius: ${euiTheme.border.radius.medium};
+
+ &:hover {
+ background-color: ${euiTheme.colors.backgroundBaseSubdued};
+ text-decoration: none;
+ }
+
+ &:focus-visible {
+ outline: ${euiTheme.focus.width} solid ${euiTheme.focus.color};
+ outline-offset: -${euiTheme.focus.width};
+ }
+ `;
+
+ return (
+
+ );
+};
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..a6f0053801132
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { EuiFlexGroup, EuiHorizontalRule, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { useFeatureFlags } from '../../../../hooks/use_feature_flags';
+import { getManageNavItems } from '../../../../route_config';
+import { SidebarNavList } from '../shared/sidebar_nav_list';
+
+interface ManageSidebarViewProps {
+ pathname: string;
+}
+
+export const ManageSidebarView: React.FC = ({ pathname }) => {
+ const featureFlags = useFeatureFlags();
+ const { euiTheme } = useEuiTheme();
+
+ const navItems = useMemo(() => getManageNavItems(featureFlags), [featureFlags]);
+
+ const isActive = (path: string) => pathname.startsWith(path);
+
+ return (
+
+
+
+
+
+
+ );
+};
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..9b573c171f00a
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/redirects/legacy_conversation_redirect.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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.conversations.byId({ agentId: lastAgentId, conversationId }), {
+ 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/context/agent_builder_tour_context.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/context/agent_builder_tour_context.tsx
deleted file mode 100644
index df3dbcea069f6..0000000000000
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/context/agent_builder_tour_context.tsx
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * 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, {
- createContext,
- useContext,
- useState,
- type PropsWithChildren,
- useEffect,
-} from 'react';
-
-import { EuiButton, EuiButtonEmpty, EuiText, type EuiTourStepProps } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import type { PopoverAnchorPosition } from '@elastic/eui/src/components/popover/popover';
-import { storageKeys } from '../storage_keys';
-import { useConversationContext } from './conversation/conversation_context';
-import { useKibana } from '../hooks/use_kibana';
-
-export enum TourStep {
- AgentSelector = 'agent-selector',
- LlmSelector = 'llm-selector',
- ConversationsHistory = 'conversations-history',
- ConversationActions = 'conversation-actions',
-}
-
-export type TourStepProps = Pick<
- EuiTourStepProps,
- | 'maxWidth'
- | 'isStepOpen'
- | 'title'
- | 'content'
- | 'onFinish'
- | 'step'
- | 'stepsTotal'
- | 'anchorPosition'
- | 'footerAction'
-> & { anchor?: undefined };
-
-interface AgentBuilderTourContextValue {
- isTourActive: boolean;
- getStepProps: (step: TourStep) => TourStepProps;
-}
-
-const AgentBuilderTourContext = createContext(undefined);
-
-const DEFAULT_STEP = 1;
-const TOUR_DELAY = 250; // 250ms
-
-const tourConfig = {
- tourPopoverWidth: 370,
- anchorPosition: 'downCenter' as PopoverAnchorPosition,
- maxSteps: 4,
-};
-
-const labels = {
- agentSelector: {
- title: i18n.translate('xpack.agentBuilder.agentBuilderTour.agentSelector.title', {
- defaultMessage: 'Meet your active agent 🕵️♂️',
- }),
- content: i18n.translate('xpack.agentBuilder.agentBuilderTour.agentSelector.content', {
- defaultMessage:
- "An agent's behavior is defined by its custom instructions and available tools. Switch agents when you need different capabilities for your tasks.",
- }),
- },
- llmSelector: {
- title: i18n.translate('xpack.agentBuilder.agentBuilderTour.llmSelector.title', {
- defaultMessage: 'Select your LLM 🧠',
- }),
- content: i18n.translate('xpack.agentBuilder.agentBuilderTour.llmSelector.content', {
- defaultMessage:
- 'Your agent uses this model to generate responses. Switch LLMs to prioritize faster responses, lower costs, or more complex reasoning.',
- }),
- },
- // TODO: Add prompts step once we have prompts.
- // prompts: {
- // title: i18n.translate('xpack.agentBuilder.agentBuilderTour.prompts.title', {
- // defaultMessage: 'Reuse your prompts ✍️',
- // }),
- // content: i18n.translate('xpack.agentBuilder.agentBuilderTour.prompts.content', {
- // defaultMessage: 'Store your favorite queries here. Pick one to drop it into the chat.',
- // }),
- // },
- conversationsHistory: {
- title: i18n.translate('xpack.agentBuilder.agentBuilderTour.conversationsHistory.title', {
- defaultMessage: 'Browse your conversations 💬',
- }),
- content: i18n.translate('xpack.agentBuilder.agentBuilderTour.conversationsHistory.content', {
- defaultMessage: 'Find all your previous conversations here.',
- }),
- },
- conversationActions: {
- title: i18n.translate('xpack.agentBuilder.agentBuilderTour.conversationActions.title', {
- defaultMessage: 'Jump to key actions ⚙️',
- }),
- content: i18n.translate('xpack.agentBuilder.agentBuilderTour.conversationActions.content', {
- defaultMessage:
- 'This menu is your hub for key management actions. You can quickly access important pages from here.',
- }),
- },
- closeTour: i18n.translate('xpack.agentBuilder.agentBuilderTour.closeTour', {
- defaultMessage: 'Close tour',
- }),
- next: i18n.translate('xpack.agentBuilder.agentBuilderTour.next', {
- defaultMessage: 'Next',
- }),
- finishTour: i18n.translate('xpack.agentBuilder.agentBuilderTour.finishTour', {
- defaultMessage: 'Finish tour',
- }),
-};
-
-interface TourStepConfig {
- step: number;
- title: string;
- content: React.ReactNode;
- footerActions: React.ReactNode[];
-}
-
-export const AgentBuilderTourProvider: React.FC> = ({ children }) => {
- const { isEmbeddedContext } = useConversationContext();
- const { notifications } = useKibana().services;
- const isTourEnabled = notifications.tours.isEnabled();
-
- const [currentStep, setCurrentStep] = useState(DEFAULT_STEP);
-
- const [isTourActive, setIsTourActive] = useState(false);
-
- useEffect(() => {
- const hasSeenTour = localStorage.getItem(storageKeys.hasSeenAgentBuilderTour);
- let timer: NodeJS.Timeout | undefined;
-
- if (!isEmbeddedContext && !hasSeenTour && isTourEnabled) {
- // We use a delay to ensure the tour is not triggered immediately when the page loads to ensure correct anchor positioning.
- timer = setTimeout(() => {
- setIsTourActive(true);
- }, TOUR_DELAY);
- }
-
- return () => {
- if (timer) {
- clearTimeout(timer);
- }
- };
- }, [isEmbeddedContext, isTourEnabled]);
-
- const handleMoveToNextStep = () => {
- setCurrentStep((prev) => prev + 1);
- };
-
- const handleFinishTour = () => {
- setIsTourActive(false);
- localStorage.setItem(storageKeys.hasSeenAgentBuilderTour, 'true');
- };
-
- const footerActions = [
-
- {labels.closeTour}
- ,
-
- {labels.next}
- ,
- ];
-
- const footerActionsFinish = [
-
- {labels.finishTour}
- ,
- ];
-
- const tourSteps: Record = {
- [TourStep.AgentSelector]: {
- title: labels.agentSelector.title,
- content: (
-
- {labels.agentSelector.content}
-
- ),
- footerActions,
- step: 1,
- },
- [TourStep.LlmSelector]: {
- title: labels.llmSelector.title,
- content: (
-
- {labels.llmSelector.content}
-
- ),
- footerActions,
- step: 2,
- },
- [TourStep.ConversationsHistory]: {
- title: labels.conversationsHistory.title,
- content: (
-
- {labels.conversationsHistory.content}
-
- ),
- footerActions,
- step: 3,
- },
- [TourStep.ConversationActions]: {
- title: labels.conversationActions.title,
- content: (
-
- {labels.conversationActions.content}
-
- ),
- footerActions: footerActionsFinish,
- step: 4,
- },
- };
-
- const getStepProps = (step: TourStep): TourStepProps => {
- const stepConfig = tourSteps[step];
-
- return {
- maxWidth: tourConfig.tourPopoverWidth,
- isStepOpen: currentStep === stepConfig.step && isTourActive,
- title: stepConfig.title,
- content: stepConfig.content,
- onFinish: handleFinishTour,
- step: stepConfig.step,
- stepsTotal: tourConfig.maxSteps,
- anchorPosition: tourConfig.anchorPosition,
- footerAction: stepConfig.footerActions,
- };
- };
-
- const contextValue: AgentBuilderTourContextValue = {
- isTourActive,
- getStepProps,
- };
-
- return (
-
- {children}
-
- );
-};
-
-export const useAgentBuilderTour = (): AgentBuilderTourContextValue => {
- const context = useContext(AgentBuilderTourContext);
- if (!context) {
- throw new Error('useAgentBuilderTour must be used within an AgentBuilderTourProvider');
- }
- return context;
-};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/conversation_context.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/conversation_context.tsx
index cd16638411c59..ca351e0063d49 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/conversation_context.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/conversation_context.tsx
@@ -25,6 +25,7 @@ interface ConversationContextValue {
removeAttachment?: (attachmentIndex: number) => void;
browserApiTools?: Array>;
setConversationId?: (conversationId?: string) => void;
+ setAgentId?: (agentId: string) => void;
conversationActions: ConversationActions;
}
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/embeddable_conversations_provider.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/embeddable_conversations_provider.tsx
index d369a0dcd0db3..3436b4d3e2905 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/embeddable_conversations_provider.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/embeddable_conversations_provider.tsx
@@ -9,6 +9,7 @@ import React, { useMemo, useEffect, useCallback, useState, useRef } from 'react'
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { QueryClient, QueryClientProvider } from '@kbn/react-query';
+import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common';
import type { AttachmentInput } from '@kbn/agent-builder-common/attachments';
import type {
EmbeddableConversationInternalProps,
@@ -21,7 +22,6 @@ import { SendMessageProvider } from '../send_message/send_message_context';
import { useConversationActions } from './use_conversation_actions';
import { usePersistedConversationId } from '../../hooks/use_persisted_conversation_id';
import { AppLeaveContext } from '../app_leave_context';
-import { AgentBuilderTourProvider } from '../agent_builder_tour_context';
const noopOnAppLeave = () => {};
interface EmbeddableConversationsProviderProps extends EmbeddableConversationInternalProps {
@@ -181,18 +181,23 @@ export const EmbeddableConversationsProvider: React.FC {
+ setCurrentProps((prev) => ({ ...prev, agentId: id, newConversation: true }));
+ }, []);
+
const conversationContextValue = useMemo(
() => ({
conversationId,
shouldStickToBottom: true,
isEmbeddedContext: true,
sessionTag: currentProps.sessionTag,
- agentId: currentProps.agentId,
+ agentId: currentProps.agentId ?? agentBuilderDefaultAgentId,
initialMessage: currentProps.initialMessage,
autoSendInitialMessage: currentProps.autoSendInitialMessage ?? false,
resetInitialMessage,
browserApiTools: currentProps.browserApiTools,
setConversationId,
+ setAgentId,
attachments: currentProps.attachments,
upsertAttachments,
resetAttachments,
@@ -210,6 +215,7 @@ export const EmbeddableConversationsProvider: React.FC
-
- {children}
-
+ {children}
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/routed_conversations_provider.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/routed_conversations_provider.tsx
index 33452a8cba2e3..a77fd71a0adb3 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/routed_conversations_provider.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/context/conversation/routed_conversations_provider.tsx
@@ -7,7 +7,6 @@
import React, { useMemo, useEffect, useRef, useCallback, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
-import { useSearchParams } from 'react-router-dom-v5-compat';
import { useQueryClient } from '@kbn/react-query';
import type { AttachmentInput } from '@kbn/agent-builder-common/attachments';
import { ConversationContext } from './conversation_context';
@@ -16,9 +15,8 @@ import { newConversationId } from '../../utils/new_conversation';
import { appPaths } from '../../utils/app_paths';
import { useNavigation } from '../../hooks/use_navigation';
import { useAgentBuilderServices } from '../../hooks/use_agent_builder_service';
-import { useAgentBuilderAgents } from '../../hooks/agents/use_agents';
-import { searchParamNames } from '../../search_param_names';
import { useConversationActions } from './use_conversation_actions';
+import { queryKeys } from '../../query_keys';
import { upsertAttachmentsIntoList } from './upsert_attachments_into_list';
interface RoutedConversationsProviderProps {
@@ -30,23 +28,23 @@ export const RoutedConversationsProvider: React.FC {
const queryClient = useQueryClient();
const { conversationsService } = useAgentBuilderServices();
- const { conversationId: conversationIdParam } = useParams<{ conversationId?: string }>();
+ const { conversationId: conversationIdParam, agentId: agentIdParam } = useParams<{
+ conversationId?: string;
+ agentId?: string;
+ }>();
const conversationId = useMemo(() => {
return conversationIdParam === newConversationId ? undefined : conversationIdParam;
}, [conversationIdParam]);
+ const agentIdFromPath = agentIdParam;
+
const location = useLocation();
const shouldStickToBottom = location.state?.shouldStickToBottom ?? true;
const initialMessage = location.state?.initialMessage;
- // Get search params for agent ID syncing
- const [searchParams] = useSearchParams();
- const { agents } = useAgentBuilderAgents();
-
const { navigateToAgentBuilderUrl } = useNavigation();
const shouldAllowConversationRedirectRef = useRef(true);
- const agentIdSyncedRef = useRef(false);
useEffect(() => {
return () => {
@@ -55,17 +53,26 @@ export const RoutedConversationsProvider: React.FC {
+ if (!conversationId) {
+ queryClient.removeQueries({ queryKey: queryKeys.conversations.byId(newConversationId) });
+ }
+ }, [agentIdFromPath, conversationId, queryClient]);
+
const navigateToConversation = useCallback(
({ nextConversationId }: { nextConversationId: string }) => {
// Navigate to the conversation if redirect is allowed
- if (shouldAllowConversationRedirectRef.current) {
- const path = appPaths.chat.conversation({ conversationId: nextConversationId });
- const params = undefined;
+ if (shouldAllowConversationRedirectRef.current && agentIdFromPath) {
+ const path = appPaths.agent.conversations.byId({
+ agentId: agentIdFromPath,
+ conversationId: nextConversationId,
+ });
const state = { shouldStickToBottom: false };
- navigateToAgentBuilderUrl(path, params, state);
+ navigateToAgentBuilderUrl(path, undefined, state);
}
},
- [shouldAllowConversationRedirectRef, navigateToAgentBuilderUrl]
+ [shouldAllowConversationRedirectRef, navigateToAgentBuilderUrl, agentIdFromPath]
);
const onConversationCreated = useCallback(
@@ -78,9 +85,8 @@ export const RoutedConversationsProvider: React.FC {
if (isCurrentConversation) {
- // If deleting current conversation, navigate to new conversation
- const path = appPaths.chat.new;
- navigateToAgentBuilderUrl(path, undefined, { shouldStickToBottom: true });
+ // If deleting current conversation, navigate to root (redirects to last used agent)
+ navigateToAgentBuilderUrl(appPaths.root, undefined, { shouldStickToBottom: true });
}
},
[navigateToAgentBuilderUrl]
@@ -113,23 +119,6 @@ export const RoutedConversationsProvider: React.FC {
- if (agentIdSyncedRef.current || conversationId) {
- return;
- }
-
- // If we don't have a selected agent id, check for a valid agent id in the search params
- // This is used for the "chat with agent" action on the Agent pages
- const agentIdParam = searchParams.get(searchParamNames.agentId);
-
- if (agentIdParam && agents.some((agent) => agent.id === agentIdParam)) {
- // Agent id passed to sync is valid, set it and mark as synced
- conversationActions.setAgentId(agentIdParam);
- agentIdSyncedRef.current = true;
- }
- }, [searchParams, agents, conversationId, conversationActions]);
-
const contextValue = useMemo(
() => ({
conversationId,
@@ -138,6 +127,7 @@ export const RoutedConversationsProvider: React.FC {
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;
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/skills/use_skills.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/skills/use_skills.ts
index 56ee2890b0dbf..ae8e892fe8ec5 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/skills/use_skills.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/skills/use_skills.ts
@@ -20,6 +20,7 @@ export const useSkillsService = () => {
const { data, isLoading, error, isError } = useQuery({
queryKey: queryKeys.skills.all,
queryFn: () => skillsService.list(),
+ keepPreviousData: true,
});
return { skills: data ?? [], isLoading, error, isError };
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_conversation.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_conversation.ts
index 3df2ccebc4af4..663346cfc8b09 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_conversation.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_conversation.ts
@@ -101,25 +101,22 @@ const useGetNewConversationAgentId = () => {
export const useAgentId = () => {
const { conversation } = useConversation();
const context = useConversationContext();
- const agentId = conversation?.agent_id;
const conversationId = useConversationId();
const isNewConversation = !conversationId;
const getNewConversationAgentId = useGetNewConversationAgentId();
- if (agentId) {
- return agentId;
- }
-
- if (context.agentId) {
- return context.agentId;
+ // For new conversations, URL (context.agentId) is the source of truth
+ if (isNewConversation) {
+ return context.agentId ?? getNewConversationAgentId();
}
- // For new conversations, agent id must be defined
- if (isNewConversation) {
- return getNewConversationAgentId();
+ // For existing conversations, use the conversation's stored agent_id
+ if (conversation?.agent_id) {
+ return conversation.agent_id;
}
- return undefined;
+ // Fallback to context (URL) for edge cases
+ return context.agentId;
};
export const useConversationTitle = () => {
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_feature_flags.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_feature_flags.ts
new file mode 100644
index 0000000000000..10db14ac706a3
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/use_feature_flags.ts
@@ -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(AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID);
+ return useMemo(() => ({ experimental, connectors }), [experimental, connectors]);
+};
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/pages/agent_overview.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_overview.tsx
new file mode 100644
index 0000000000000..cbc9256eabb10
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_overview.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 { useParams } from 'react-router-dom';
+import { AgentOverview } from '../components/agents/overview/agent_overview';
+import { useAgentBuilderAgentById } from '../hooks/agents/use_agent_by_id';
+import { useBreadcrumb } from '../hooks/use_breadcrumbs';
+
+export const AgentBuilderAgentOverviewPage: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const { agent } = useAgentBuilderAgentById(agentId);
+
+ const breadcrumbs = useMemo(() => [{ text: agent?.name ?? agentId }], [agent?.name, agentId]);
+
+ useBreadcrumb(breadcrumbs);
+
+ return ;
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_plugins.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_plugins.tsx
new file mode 100644
index 0000000000000..b9c3e64157d6e
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_plugins.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { useParams } from 'react-router-dom';
+import { AgentPlugins } from '../components/agents/plugins/agent_plugins';
+import { useAgentBuilderAgentById } from '../hooks/agents/use_agent_by_id';
+import { useBreadcrumb } from '../hooks/use_breadcrumbs';
+import { appPaths } from '../utils/app_paths';
+import { labels } from '../utils/i18n';
+
+export const AgentBuilderAgentPluginsPage: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const { agent } = useAgentBuilderAgentById(agentId);
+
+ const breadcrumbs = useMemo(
+ () => [
+ { text: agent?.name ?? agentId, path: appPaths.agent.overview({ agentId }) },
+ { text: labels.plugins.title },
+ ],
+ [agent?.name, agentId]
+ );
+
+ useBreadcrumb(breadcrumbs);
+
+ return ;
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_skills.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_skills.tsx
new file mode 100644
index 0000000000000..337453f976be5
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_skills.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { useParams } from 'react-router-dom';
+import { AgentSkills } from '../components/agents/skills/agent_skills';
+import { useAgentBuilderAgentById } from '../hooks/agents/use_agent_by_id';
+import { useBreadcrumb } from '../hooks/use_breadcrumbs';
+import { appPaths } from '../utils/app_paths';
+import { labels } from '../utils/i18n';
+
+export const AgentBuilderAgentSkillsPage: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const { agent } = useAgentBuilderAgentById(agentId);
+
+ const breadcrumbs = useMemo(
+ () => [
+ { text: agent?.name ?? agentId, path: appPaths.agent.overview({ agentId }) },
+ { text: labels.skills.title },
+ ],
+ [agent?.name, agentId]
+ );
+
+ useBreadcrumb(breadcrumbs);
+
+ return ;
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_tools.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_tools.tsx
new file mode 100644
index 0000000000000..e959188625059
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/agent_tools.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { useParams } from 'react-router-dom';
+import { AgentTools } from '../components/agents/tools/agent_tools';
+import { useAgentBuilderAgentById } from '../hooks/agents/use_agent_by_id';
+import { useBreadcrumb } from '../hooks/use_breadcrumbs';
+import { appPaths } from '../utils/app_paths';
+import { labels } from '../utils/i18n';
+
+export const AgentBuilderAgentToolsPage: React.FC = () => {
+ const { agentId } = useParams<{ agentId: string }>();
+ const { agent } = useAgentBuilderAgentById(agentId);
+
+ const breadcrumbs = useMemo(
+ () => [
+ { text: agent?.name ?? agentId, path: appPaths.agent.overview({ agentId }) },
+ { text: labels.tools.title },
+ ],
+ [agent?.name, agentId]
+ );
+
+ useBreadcrumb(breadcrumbs);
+
+ return ;
+};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/pages/conversations.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/conversations.tsx
index 1dd8ee47b6470..c1eb0de7d97b6 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/pages/conversations.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/pages/conversations.tsx
@@ -15,7 +15,7 @@ export const AgentBuilderConversationsPage: React.FC = () => {
useBreadcrumb([
{
text: labels.conversations.title,
- path: appPaths.chat.new,
+ path: appPaths.root,
},
]);
return ;
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..6aeed8cc5247c
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts
@@ -0,0 +1,103 @@
+/*
+ * 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 "conversation" for overview route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/overview')).toBe('conversation');
+ });
+
+ it('returns "conversation" for skills route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/skills')).toBe('conversation');
+ });
+
+ it('returns "conversation" for tools route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/tools')).toBe('conversation');
+ });
+
+ it('returns "conversation" for plugins route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/plugins')).toBe('conversation');
+ });
+
+ it('returns "conversation" for connectors route', () => {
+ expect(getSidebarViewForRoute('/agents/elastic-ai-agent/connectors')).toBe('conversation');
+ });
+ });
+
+ 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..8e72b676e6d2f
--- /dev/null
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx
@@ -0,0 +1,255 @@
+/*
+ * 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';
+import { AgentBuilderConversationsPage } from './pages/conversations';
+import { AgentBuilderAgentsPage } from './pages/agents';
+import { AgentBuilderAgentsCreate } from './pages/agent_create';
+import { AgentBuilderAgentsEdit } from './pages/agent_edit';
+import { AgentBuilderAgentOverviewPage } from './pages/agent_overview';
+import { AgentBuilderAgentSkillsPage } from './pages/agent_skills';
+import { AgentBuilderAgentPluginsPage } from './pages/agent_plugins';
+import { AgentBuilderAgentToolsPage } from './pages/agent_tools';
+import { AgentBuilderToolsPage } from './pages/tools';
+import { AgentBuilderToolCreatePage } from './pages/tool_create';
+import { AgentBuilderToolDetailsPage } from './pages/tool_details';
+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 { AgentBuilderConnectorsPage } from './pages/connectors';
+
+export type SidebarView = 'conversation' | 'manage';
+
+export interface FeatureFlags {
+ experimental: boolean;
+ connectors: boolean;
+}
+
+export interface RouteDefinition {
+ path: string;
+ element: React.ReactNode;
+ sidebarView: SidebarView;
+ isExperimental?: boolean;
+ isConnectors?: boolean;
+ navLabel?: string;
+ navIcon?: string;
+}
+
+const navLabels = {
+ overview: i18n.translate('xpack.agentBuilder.routeConfig.overview', {
+ defaultMessage: 'Overview',
+ }),
+ 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/overview',
+ sidebarView: 'conversation',
+ navLabel: navLabels.overview,
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/skills',
+ sidebarView: 'conversation',
+ isExperimental: true,
+ navLabel: navLabels.skills,
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/plugins',
+ sidebarView: 'conversation',
+ isExperimental: true,
+ navLabel: navLabels.plugins,
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/connectors',
+ sidebarView: 'conversation',
+ navLabel: navLabels.connectors,
+ isConnectors: true,
+ element: ,
+ },
+ {
+ path: '/agents/:agentId/tools',
+ sidebarView: 'conversation',
+ navLabel: navLabels.tools,
+ 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/agents/:agentId',
+ 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',
+ isExperimental: true,
+ navLabel: navLabels.plugins,
+ element: ,
+ },
+ {
+ path: '/manage/plugins/:pluginId',
+ sidebarView: 'manage',
+ isExperimental: true,
+ element: ,
+ },
+ {
+ path: '/manage/connectors',
+ sidebarView: 'manage',
+ navLabel: navLabels.connectors,
+ isConnectors: true,
+ 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: ,
+ },
+];
+
+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 getConversationIdFromPath = (pathname: string): string | undefined => {
+ const match = pathname.match(/^\/agents\/[^/]+\/conversations\/([^/]+)/);
+ return match ? match[1] : undefined;
+};
+
+export interface SidebarNavItem {
+ label: string;
+ path: string;
+ icon?: string;
+}
+
+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 && isRouteEnabled(route, flags))
+ .map((route) => ({
+ label: route.navLabel ?? '',
+ path: route.path.replace(':agentId', agentId),
+ icon: route.navIcon,
+ }));
+};
+
+export const getManageNavItems = (flags: FeatureFlags): SidebarNavItem[] => {
+ return manageRoutes
+ .filter((route) => route.navLabel && isRouteEnabled(route, flags))
+ .map((route) => ({
+ label: route.navLabel!,
+ path: route.path,
+ icon: route.navIcon,
+ }));
+};
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 514878aeb35af..13f78402431c2 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,104 +6,43 @@
*/
import { Route, Routes } from '@kbn/shared-ux-router';
-import React from 'react';
-import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids';
-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 { useExperimentalFeatures } from './hooks/use_experimental_features';
-import { useKibana } from './hooks/use_kibana';
-import { AgentBuilderConnectorsPage } from './pages/connectors';
+import React, { useMemo } from 'react';
-export const AgentBuilderRoutes: React.FC<{}> = () => {
- const isExperimentalFeaturesEnabled = useExperimentalFeatures();
- const {
- services: { uiSettings },
- } = useKibana();
- const isConnectorsEnabled = uiSettings.get(
- AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID,
- false
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+import { AppLayout } from './components/layout/app_layout';
+import { RootRedirect } from './components/redirects/root_redirect';
+import { LegacyConversationRedirect } from './components/redirects/legacy_conversation_redirect';
+import { getEnabledRoutes } from './route_config';
+import { useFeatureFlags } from './hooks/use_feature_flags';
-
-
-
-
-
-
-
-
-
-
-
+export const AgentBuilderRoutes: React.FC<{}> = () => {
+ const featureFlags = useFeatureFlags();
-
-
-
+ const enabledRoutes = useMemo(() => getEnabledRoutes(featureFlags), [featureFlags]);
- {isConnectorsEnabled ? (
-
-
+ return (
+
+
+ {enabledRoutes.map((route) => (
+
+ {route.element}
+
+ ))}
+
+ {/* Legacy routes - redirect to new structure */}
+
+
- ) : null}
- {isExperimentalFeaturesEnabled
- ? [
-
-
- ,
-
-
- ,
-
-
- ,
- ]
- : null}
-
- {isExperimentalFeaturesEnabled
- ? [
-
-
- ,
-
-
- ,
- ]
- : null}
+ {/* Redirect /agents to /agents/:lastAgentId */}
+
+
+
- {/* Default to conversations page */}
-
-
-
-
+ {/* Root route - redirect to last used agent */}
+
+
+
+
+
);
};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/storage_keys.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/storage_keys.ts
index b42641281dcec..fd1ac87f3c041 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/storage_keys.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/storage_keys.ts
@@ -5,13 +5,11 @@
* 2.0.
*/
-export const AGENT_BUILDER_TOUR_STORAGE_KEY = 'agentBuilder.hasSeenTour';
-
export const storageKeys = {
agentId: 'agentBuilder.agentId',
lastUsedConnector: 'agentBuilder.lastUsedConnector',
welcomeMessageDismissed: 'agentBuilder.welcomeMessageDismissed',
- hasSeenAgentBuilderTour: AGENT_BUILDER_TOUR_STORAGE_KEY,
+ autoIncludeWarningDismissed: 'agentBuilder.autoIncludeWarningDismissed',
getLastConversationKey: (sessionTag?: string, agentId?: string): string => {
const tag = sessionTag || 'default';
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 422016b33451e..38c0593561b83 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,38 +9,69 @@ import { newConversationId } from './new_conversation';
export const appPaths = {
root: '/',
- agents: {
- list: '/agents',
- new: '/agents/new',
- edit: ({ agentId }: { agentId: string }) => {
- return `/agents/${agentId}`;
+
+ // 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`,
+ tools: ({ agentId }: { agentId: string }) => `/agents/${agentId}/tools`,
+ connectors: ({ agentId }: { agentId: string }) => `/agents/${agentId}/connectors`,
+ overview: ({ agentId }: { agentId: string }) => `/agents/${agentId}/overview`,
},
- chat: {
- new: `/conversations/${newConversationId}`,
- newWithAgent: ({ agentId }: { agentId: string }) => {
- return `/conversations/${newConversationId}?agent_id=${agentId}`;
- },
- conversation: ({ conversationId }: { conversationId: string }) => {
- return `/conversations/${conversationId}`;
- },
+
+ // Manage routes (global CRUD, no agent context)
+ manage: {
+ agents: '/manage/agents',
+ agentsNew: '/manage/agents/new',
+ agentDetails: ({ agentId }: { agentId: string }) => `/manage/agents/${agentId}`,
+ 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',
+ },
+
+ // Legacy paths - redirect to new structure via LegacyConversationRedirect
+ legacy: {
+ conversation: ({ conversationId }: { conversationId: string }) =>
+ `/conversations/${conversationId}`,
+ },
+
+ // Backward compatibility aliases pointing to new routes
+ // TODO: Migrate consumers to use appPaths.agent.* or appPaths.manage.* directly and remove these aliases
+ agents: {
+ list: '/manage/agents',
+ new: '/manage/agents/new',
+ edit: ({ agentId }: { agentId: string }) => `/manage/agents/${agentId}`,
},
connectors: {
list: '/connectors',
},
tools: {
- list: '/tools',
- new: '/tools/new',
- details: ({ toolId }: { toolId: string }) => `/tools/${toolId}`,
- bulkImportMcp: '/tools/bulk_import_mcp',
+ list: '/manage/tools',
+ new: '/manage/tools/new',
+ details: ({ toolId }: { toolId: string }) => `/manage/tools/${toolId}`,
+ bulkImportMcp: '/manage/tools/bulk_import_mcp',
},
skills: {
- list: '/skills',
- new: '/skills/new',
- details: ({ skillId }: { skillId: string }) => `/skills/${skillId}`,
+ list: '/manage/skills',
+ new: '/manage/skills/new',
+ details: ({ skillId }: { skillId: string }) => `/manage/skills/${skillId}`,
},
plugins: {
- list: '/plugins',
- details: ({ pluginId }: { pluginId: string }) => `/plugins/${pluginId}`,
+ list: '/manage/plugins',
+ details: ({ pluginId }: { pluginId: string }) => `/manage/plugins/${pluginId}`,
},
};
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/i18n.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/i18n.ts
index 25cc883dcac79..bd00339c795a7 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/i18n.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/i18n.ts
@@ -633,6 +633,610 @@ export const labels = {
defaultMessage: 'Experimental',
}),
},
+ agentSkills: {
+ pageDescription: i18n.translate('xpack.agentBuilder.agentSkills.pageDescription', {
+ defaultMessage:
+ 'Capabilities that help the agent analyze data, generate queries, and perform tasks.',
+ }),
+ addSkillButton: i18n.translate('xpack.agentBuilder.agentSkills.addSkillButton', {
+ defaultMessage: 'Add skill',
+ }),
+ importFromLibraryMenuItem: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.importFromLibraryMenuItem',
+ {
+ defaultMessage: 'Import from skill library',
+ }
+ ),
+ createSkillMenuItem: i18n.translate('xpack.agentBuilder.agentSkills.createSkillMenuItem', {
+ defaultMessage: 'Create a skill',
+ }),
+ createFromChatButton: i18n.translate('xpack.agentBuilder.agentSkills.createFromChatButton', {
+ defaultMessage: 'Create from chat',
+ }),
+ searchActiveSkillsPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.searchActiveSkillsPlaceholder',
+ {
+ defaultMessage: 'Search active skills',
+ }
+ ),
+ addSkillFromLibraryTitle: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.addSkillFromLibraryTitle',
+ {
+ defaultMessage: 'Add skill from library',
+ }
+ ),
+ manageSkillLibraryLink: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.manageSkillLibraryLink',
+ {
+ defaultMessage: 'Manage skill library',
+ }
+ ),
+ searchAvailableSkillsPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.searchAvailableSkillsPlaceholder',
+ {
+ defaultMessage: 'Search available skills',
+ }
+ ),
+ availableSkillsSummary: (showing: number, total: number) =>
+ i18n.translate('xpack.agentBuilder.agentSkills.availableSkillsSummary', {
+ defaultMessage:
+ 'Showing {showing} of {total} {total, plural, one {Available skill} other {Available skills}}',
+ values: { showing, total },
+ }),
+ addButtonLabel: i18n.translate('xpack.agentBuilder.agentSkills.addButtonLabel', {
+ defaultMessage: 'Add',
+ }),
+ editSkillAriaLabel: i18n.translate('xpack.agentBuilder.agentSkills.editSkillAriaLabel', {
+ defaultMessage: 'Edit skill',
+ }),
+ removeSkillAriaLabel: i18n.translate('xpack.agentBuilder.agentSkills.removeSkillAriaLabel', {
+ defaultMessage: 'Remove skill from agent',
+ }),
+ noActiveSkillsMessage: i18n.translate('xpack.agentBuilder.agentSkills.noActiveSkillsMessage', {
+ defaultMessage: 'No skills assigned to this agent yet. Add skills from the library.',
+ }),
+ noActiveSkillsMatchMessage: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.noActiveSkillsMatchMessage',
+ {
+ defaultMessage: 'No active skills match your search.',
+ }
+ ),
+ noAvailableSkillsMessage: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.noAvailableSkillsMessage',
+ {
+ defaultMessage: 'All skills have been added to this agent.',
+ }
+ ),
+ noAvailableSkillsMatchMessage: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.noAvailableSkillsMatchMessage',
+ {
+ defaultMessage: 'No available skills match your search.',
+ }
+ ),
+ removeSkillSuccessToast: (skillName: string) =>
+ i18n.translate('xpack.agentBuilder.agentSkills.removeSkillSuccessToast', {
+ defaultMessage: 'Skill "{skillName}" removed from agent',
+ values: { skillName },
+ }),
+ addSkillSuccessToast: (skillName: string) =>
+ i18n.translate('xpack.agentBuilder.agentSkills.addSkillSuccessToast', {
+ defaultMessage: 'Skill "{skillName}" added to agent',
+ values: { skillName },
+ }),
+ updateSkillsErrorToast: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.updateSkillsErrorToast',
+ {
+ defaultMessage: 'Unable to update agent skills',
+ }
+ ),
+ editSkillFlyoutTitle: i18n.translate('xpack.agentBuilder.agentSkills.editSkillFlyoutTitle', {
+ defaultMessage: 'Edit skill',
+ }),
+ createSkillFlyoutTitle: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.createSkillFlyoutTitle',
+ {
+ defaultMessage: 'Create skill',
+ }
+ ),
+ viewSkillLibraryLink: i18n.translate('xpack.agentBuilder.agentSkills.viewSkillLibraryLink', {
+ defaultMessage: 'View skill library',
+ }),
+ sharedSkillWarning: i18n.translate('xpack.agentBuilder.agentSkills.sharedSkillWarning', {
+ defaultMessage:
+ 'You are editing a shared skill. Changes will affect all users using this skill.',
+ }),
+ newSkillLibraryInfo: i18n.translate('xpack.agentBuilder.agentSkills.newSkillLibraryInfo', {
+ defaultMessage:
+ 'This skill will be added to your library and be available for other agents to use.',
+ }),
+ advancedOptionsLabel: i18n.translate('xpack.agentBuilder.agentSkills.advancedOptionsLabel', {
+ defaultMessage: 'Advanced options',
+ }),
+ allSkillsSummary: (showing: number, total: number) =>
+ i18n.translate('xpack.agentBuilder.agentSkills.allSkillsSummary', {
+ defaultMessage: 'Showing {showing} of {total} {total, plural, one {Skill} other {Skills}}',
+ values: { showing, total },
+ }),
+ removeSkillButtonLabel: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.removeSkillButtonLabel',
+ {
+ defaultMessage: 'Remove',
+ }
+ ),
+ skillDetailInstructionsLabel: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.skillDetailInstructionsLabel',
+ {
+ defaultMessage: 'Instructions',
+ }
+ ),
+ instructionsViewModeLegend: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.instructionsViewModeLegend',
+ {
+ defaultMessage: 'Instructions view mode',
+ }
+ ),
+ instructionsViewRenderedLabel: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.instructionsViewRenderedLabel',
+ {
+ defaultMessage: 'Rendered',
+ }
+ ),
+ instructionsViewRawLabel: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.instructionsViewRawLabel',
+ {
+ defaultMessage: 'Raw',
+ }
+ ),
+ noSkillSelectedMessage: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.noSkillSelectedMessage',
+ {
+ defaultMessage: 'Select a skill to view details.',
+ }
+ ),
+ removeSkillConfirmTitle: (skillName: string) =>
+ i18n.translate('xpack.agentBuilder.agentSkills.removeSkillConfirmTitle', {
+ defaultMessage: 'Remove "{skillName}" from agent?',
+ values: { skillName },
+ }),
+ removeSkillConfirmBody: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.removeSkillConfirmBody',
+ {
+ defaultMessage: 'The skill will no longer be available to this agent.',
+ }
+ ),
+ removeSkillConfirmButton: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.removeSkillConfirmButton',
+ {
+ defaultMessage: 'Remove',
+ }
+ ),
+ removeSkillCancelButton: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.removeSkillCancelButton',
+ {
+ defaultMessage: 'Cancel',
+ }
+ ),
+ elasticCapabilitiesManagedTooltip: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.elasticCapabilitiesManagedTooltip',
+ {
+ defaultMessage:
+ 'This built-in skill is automatically included because Elastic Capabilities is enabled for this agent.',
+ }
+ ),
+ elasticCapabilitiesReadOnlyBadge: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.elasticCapabilitiesReadOnlyBadge',
+ {
+ defaultMessage: 'Auto',
+ }
+ ),
+ readOnlyBadge: i18n.translate('xpack.agentBuilder.agentSkills.readOnlyBadge', {
+ defaultMessage: 'Read only',
+ }),
+ autoIncludedBadgeLabel: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.autoIncludedBadgeLabel',
+ {
+ defaultMessage: 'Auto-included',
+ }
+ ),
+ autoIncludedTooltipTitle: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.autoIncludedTooltipTitle',
+ {
+ defaultMessage: 'Added automatically from agent settings',
+ }
+ ),
+ autoIncludedTooltipBody: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.autoIncludedTooltipBody',
+ {
+ defaultMessage: 'Turn off auto-include built-in skills to manage it yourself',
+ }
+ ),
+ elasticCapabilitiesCallout: i18n.translate(
+ 'xpack.agentBuilder.agentSkills.elasticCapabilitiesCallout',
+ {
+ defaultMessage:
+ 'Built-in skills are automatically included while Elastic Capabilities is enabled.',
+ }
+ ),
+ manageAllSkills: i18n.translate('xpack.agentBuilder.agentSkills.manageAllSkillsLink', {
+ defaultMessage: 'Manage all skills',
+ }),
+ },
+ agentPlugins: {
+ pageDescription: i18n.translate('xpack.agentBuilder.agentPlugins.pageDescription', {
+ defaultMessage: 'Pre-built packages that bundle multiple capabilities into a single install.',
+ }),
+ installPluginButton: i18n.translate('xpack.agentBuilder.agentPlugins.installPluginButton', {
+ defaultMessage: 'Install plugin',
+ }),
+ fromUrlOrZipMenuItem: i18n.translate('xpack.agentBuilder.agentPlugins.fromUrlOrZipMenuItem', {
+ defaultMessage: 'From URL or ZIP',
+ }),
+ fromLibraryMenuItem: i18n.translate('xpack.agentBuilder.agentPlugins.fromLibraryMenuItem', {
+ defaultMessage: 'From library',
+ }),
+ searchActivePluginsPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.searchActivePluginsPlaceholder',
+ {
+ defaultMessage: 'Search active plugins',
+ }
+ ),
+ noActivePluginsMessage: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.noActivePluginsMessage',
+ {
+ defaultMessage:
+ 'No plugins assigned to this agent yet. Install or add plugins from the library.',
+ }
+ ),
+ noActivePluginsMatchMessage: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.noActivePluginsMatchMessage',
+ {
+ defaultMessage: 'No active plugins match your search.',
+ }
+ ),
+ removePluginButtonLabel: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.removePluginButtonLabel',
+ {
+ defaultMessage: 'Remove',
+ }
+ ),
+ removePluginAriaLabel: i18n.translate('xpack.agentBuilder.agentPlugins.removePluginAriaLabel', {
+ defaultMessage: 'Remove plugin from agent',
+ }),
+ removePluginConfirmTitle: (pluginName: string) =>
+ i18n.translate('xpack.agentBuilder.agentPlugins.removePluginConfirmTitle', {
+ defaultMessage: 'Remove "{pluginName}" from agent?',
+ values: { pluginName },
+ }),
+ removePluginConfirmBody: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.removePluginConfirmBody',
+ {
+ defaultMessage: 'The plugin and its skills will no longer be available to this agent.',
+ }
+ ),
+ removePluginConfirmButton: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.removePluginConfirmButton',
+ {
+ defaultMessage: 'Remove',
+ }
+ ),
+ removePluginCancelButton: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.removePluginCancelButton',
+ {
+ defaultMessage: 'Cancel',
+ }
+ ),
+ addPluginSuccessToast: (pluginName: string) =>
+ i18n.translate('xpack.agentBuilder.agentPlugins.addPluginSuccessToast', {
+ defaultMessage: 'Plugin "{pluginName}" added to agent',
+ values: { pluginName },
+ }),
+ removePluginSuccessToast: (pluginName: string) =>
+ i18n.translate('xpack.agentBuilder.agentPlugins.removePluginSuccessToast', {
+ defaultMessage: 'Plugin "{pluginName}" removed from agent',
+ values: { pluginName },
+ }),
+ updatePluginsErrorToast: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.updatePluginsErrorToast',
+ {
+ defaultMessage: 'Unable to update agent plugins',
+ }
+ ),
+ noPluginSelectedMessage: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.noPluginSelectedMessage',
+ {
+ defaultMessage: 'Select a plugin to view details.',
+ }
+ ),
+ skillsCountBadge: (count: number) =>
+ i18n.translate('xpack.agentBuilder.agentPlugins.skillsCountBadge', {
+ defaultMessage: '{count} {count, plural, one {Skill} other {Skills}}',
+ values: { count },
+ }),
+ addPluginFromLibraryTitle: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.addPluginFromLibraryTitle',
+ {
+ defaultMessage: 'Add plugin from library',
+ }
+ ),
+ managePluginLibraryLink: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.managePluginLibraryLink',
+ {
+ defaultMessage: 'Manage plugin library',
+ }
+ ),
+ searchAvailablePluginsPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.searchAvailablePluginsPlaceholder',
+ {
+ defaultMessage: 'Search available plugins',
+ }
+ ),
+ availablePluginsSummary: (showing: number, total: number) =>
+ i18n.translate('xpack.agentBuilder.agentPlugins.availablePluginsSummary', {
+ defaultMessage:
+ 'Showing {showing} of {total} {total, plural, one {Available plugin} other {Available plugins}}',
+ values: { showing, total },
+ }),
+ noAvailablePluginsMatchMessage: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.noAvailablePluginsMatchMessage',
+ {
+ defaultMessage: 'No available plugins match your search.',
+ }
+ ),
+ noAvailablePluginsMessage: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.noAvailablePluginsMessage',
+ {
+ defaultMessage: 'All plugins have been added to this agent.',
+ }
+ ),
+ skillDetailInstalledVia: (source: string) =>
+ i18n.translate('xpack.agentBuilder.agentPlugins.skillDetailInstalledVia', {
+ defaultMessage: 'Installed via {source}',
+ values: { source },
+ }),
+ pluginDetailIdLabel: i18n.translate('xpack.agentBuilder.agentPlugins.pluginDetailIdLabel', {
+ defaultMessage: 'ID',
+ }),
+ pluginDetailNameLabel: i18n.translate('xpack.agentBuilder.agentPlugins.pluginDetailNameLabel', {
+ defaultMessage: 'Name',
+ }),
+ pluginDetailDescriptionLabel: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.pluginDetailDescriptionLabel',
+ {
+ defaultMessage: 'Description',
+ }
+ ),
+ pluginDetailSkillsLabel: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.pluginDetailSkillsLabel',
+ {
+ defaultMessage: 'Skills',
+ }
+ ),
+ pluginDetailAuthorLabel: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.pluginDetailAuthorLabel',
+ {
+ defaultMessage: 'Author',
+ }
+ ),
+ pluginDetailSourceLabel: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.pluginDetailSourceLabel',
+ {
+ defaultMessage: 'Source',
+ }
+ ),
+ installPluginFlyoutTitle: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.installPluginFlyoutTitle',
+ {
+ defaultMessage: 'Install plugin...',
+ }
+ ),
+ installPluginUrlTab: i18n.translate('xpack.agentBuilder.agentPlugins.installPluginUrlTab', {
+ defaultMessage: 'URL',
+ }),
+ installPluginUploadTab: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.installPluginUploadTab',
+ {
+ defaultMessage: 'Upload ZIP',
+ }
+ ),
+ autoBadge: i18n.translate('xpack.agentBuilder.agentPlugins.autoBadge', {
+ defaultMessage: 'Auto',
+ }),
+ autoPluginManagedTooltip: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.autoPluginManagedTooltip',
+ {
+ defaultMessage:
+ 'This plugin is automatically included while Elastic Capabilities is enabled.',
+ }
+ ),
+ autoIncludedBadgeLabel: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.autoIncludedBadgeLabel',
+ {
+ defaultMessage: 'Auto-included',
+ }
+ ),
+ autoIncludedTooltipTitle: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.autoIncludedTooltipTitle',
+ {
+ defaultMessage: 'Added automatically from agent settings',
+ }
+ ),
+ autoIncludedTooltipBody: i18n.translate(
+ 'xpack.agentBuilder.agentPlugins.autoIncludedTooltipBody',
+ {
+ defaultMessage: 'Turn off auto-include built-in plugins to manage it yourself',
+ }
+ ),
+ manageAllPlugins: i18n.translate('xpack.agentBuilder.agentPlugins.manageAllSkillsLink', {
+ defaultMessage: 'Manage all plugins',
+ }),
+ },
+ agentTools: {
+ pageDescription: i18n.translate('xpack.agentBuilder.agentTools.pageDescription', {
+ defaultMessage:
+ 'Modular, reusable Elasticsearch operations. Agents use them to search, retrieve, and analyze your data.',
+ }),
+ addToolButton: i18n.translate('xpack.agentBuilder.agentTools.addToolButton', {
+ defaultMessage: 'Add tool',
+ }),
+ fromLibraryMenuItem: i18n.translate('xpack.agentBuilder.agentTools.fromLibraryMenuItem', {
+ defaultMessage: 'From library',
+ }),
+ createNewToolMenuItem: i18n.translate('xpack.agentBuilder.agentTools.createNewToolMenuItem', {
+ defaultMessage: 'Create new tool',
+ }),
+ searchActiveToolsPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.agentTools.searchActiveToolsPlaceholder',
+ {
+ defaultMessage: 'Search active tools',
+ }
+ ),
+ noActiveToolsMessage: i18n.translate('xpack.agentBuilder.agentTools.noActiveToolsMessage', {
+ defaultMessage:
+ 'No tools assigned to this agent yet. Add tools from the library or create a new one.',
+ }),
+ noActiveToolsMatchMessage: i18n.translate(
+ 'xpack.agentBuilder.agentTools.noActiveToolsMatchMessage',
+ {
+ defaultMessage: 'No active tools match your search.',
+ }
+ ),
+ removeToolButtonLabel: i18n.translate('xpack.agentBuilder.agentTools.removeToolButtonLabel', {
+ defaultMessage: 'Remove',
+ }),
+ removeToolAriaLabel: i18n.translate('xpack.agentBuilder.agentTools.removeToolAriaLabel', {
+ defaultMessage: 'Remove tool from agent',
+ }),
+ removeToolConfirmTitle: (toolId: string) =>
+ i18n.translate('xpack.agentBuilder.agentTools.removeToolConfirmTitle', {
+ defaultMessage: 'Remove "{toolId}" from agent?',
+ values: { toolId },
+ }),
+ removeToolConfirmBody: i18n.translate('xpack.agentBuilder.agentTools.removeToolConfirmBody', {
+ defaultMessage: 'The tool will no longer be available to this agent.',
+ }),
+ removeToolConfirmButton: i18n.translate(
+ 'xpack.agentBuilder.agentTools.removeToolConfirmButton',
+ {
+ defaultMessage: 'Remove',
+ }
+ ),
+ removeToolCancelButton: i18n.translate('xpack.agentBuilder.agentTools.removeToolCancelButton', {
+ defaultMessage: 'Cancel',
+ }),
+ addToolSuccessToast: (toolId: string) =>
+ i18n.translate('xpack.agentBuilder.agentTools.addToolSuccessToast', {
+ defaultMessage: 'Tool "{toolId}" added to agent',
+ values: { toolId },
+ }),
+ removeToolSuccessToast: (toolId: string) =>
+ i18n.translate('xpack.agentBuilder.agentTools.removeToolSuccessToast', {
+ defaultMessage: 'Tool "{toolId}" removed from agent',
+ values: { toolId },
+ }),
+ updateToolsErrorToast: i18n.translate('xpack.agentBuilder.agentTools.updateToolsErrorToast', {
+ defaultMessage: 'Unable to update agent tools',
+ }),
+ noToolSelectedMessage: i18n.translate('xpack.agentBuilder.agentTools.noToolSelectedMessage', {
+ defaultMessage: 'Select a tool to view details.',
+ }),
+ readOnlyBadge: i18n.translate('xpack.agentBuilder.agentTools.readOnlyBadge', {
+ defaultMessage: 'Read only',
+ }),
+ addToolFromLibraryTitle: i18n.translate(
+ 'xpack.agentBuilder.agentTools.addToolFromLibraryTitle',
+ {
+ defaultMessage: 'Add tool from library',
+ }
+ ),
+ manageToolLibraryLink: i18n.translate('xpack.agentBuilder.agentTools.manageToolLibraryLink', {
+ defaultMessage: 'Manage tool library',
+ }),
+ editInLibraryLink: i18n.translate('xpack.agentBuilder.agentTools.editInLibraryLink', {
+ defaultMessage: 'Edit in library',
+ }),
+ searchAvailableToolsPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.agentTools.searchAvailableToolsPlaceholder',
+ {
+ defaultMessage: 'Search available tools',
+ }
+ ),
+ availableToolsSummary: (showing: number, total: number) =>
+ i18n.translate('xpack.agentBuilder.agentTools.availableToolsSummary', {
+ defaultMessage: 'Showing {showing} of {total} {total, plural, one {Tool} other {Tools}}',
+ values: { showing, total },
+ }),
+ noAvailableToolsMatchMessage: i18n.translate(
+ 'xpack.agentBuilder.agentTools.noAvailableToolsMatchMessage',
+ {
+ defaultMessage: 'No available tools match your search.',
+ }
+ ),
+ noAvailableToolsMessage: i18n.translate(
+ 'xpack.agentBuilder.agentTools.noAvailableToolsMessage',
+ {
+ defaultMessage: 'All tools have been added to this agent.',
+ }
+ ),
+ toolDetailIdLabel: i18n.translate('xpack.agentBuilder.agentTools.toolDetailIdLabel', {
+ defaultMessage: 'ID',
+ }),
+ toolDetailTypeLabel: i18n.translate('xpack.agentBuilder.agentTools.toolDetailTypeLabel', {
+ defaultMessage: 'Type',
+ }),
+ toolDetailDescriptionLabel: i18n.translate(
+ 'xpack.agentBuilder.agentTools.toolDetailDescriptionLabel',
+ {
+ defaultMessage: 'Description',
+ }
+ ),
+ toolDetailTagsLabel: i18n.translate('xpack.agentBuilder.agentTools.toolDetailTagsLabel', {
+ defaultMessage: 'Tags',
+ }),
+ noTagsLabel: i18n.translate('xpack.agentBuilder.agentTools.noTagsLabel', {
+ defaultMessage: 'No tags',
+ }),
+ autoIncludedTooltip: i18n.translate('xpack.agentBuilder.agentTools.autoIncludedTooltip', {
+ defaultMessage: 'This tool is automatically included and cannot be removed.',
+ }),
+ autoIncludedBadgeLabel: i18n.translate('xpack.agentBuilder.agentTools.autoIncludedBadgeLabel', {
+ defaultMessage: 'Auto-included',
+ }),
+ autoIncludedTooltipTitle: i18n.translate(
+ 'xpack.agentBuilder.agentTools.autoIncludedTooltipTitle',
+ {
+ defaultMessage: 'Added automatically from agent settings',
+ }
+ ),
+ autoIncludedTooltipBody: i18n.translate(
+ 'xpack.agentBuilder.agentTools.autoIncludedTooltipBody',
+ {
+ defaultMessage: 'Turn off auto-include built-in tools to manage it yourself',
+ }
+ ),
+ elasticCapabilitiesManagedTooltip: i18n.translate(
+ 'xpack.agentBuilder.agentTools.elasticCapabilitiesManagedTooltip',
+ {
+ defaultMessage:
+ 'This built-in tool is automatically included because Elastic Capabilities is enabled for this agent.',
+ }
+ ),
+ elasticCapabilitiesReadOnlyBadge: i18n.translate(
+ 'xpack.agentBuilder.agentTools.elasticCapabilitiesReadOnlyBadge',
+ {
+ defaultMessage: 'Auto',
+ }
+ ),
+ elasticCapabilitiesCallout: i18n.translate(
+ 'xpack.agentBuilder.agentTools.elasticCapabilitiesCallout',
+ {
+ defaultMessage:
+ 'Built-in tools are automatically included while Elastic Capabilities is enabled.',
+ }
+ ),
+ manageAllTools: i18n.translate('xpack.agentBuilder.agentTools.manageAllToolsLink', {
+ defaultMessage: 'Manage all tools',
+ }),
+ },
plugins: {
title: i18n.translate('xpack.agentBuilder.plugins.title', { defaultMessage: 'Plugins' }),
pluginsTableCaption: (pluginsCount: number) =>
@@ -845,6 +1449,144 @@ export const labels = {
),
},
},
+ agentOverview: {
+ autoIncludeEnabledToast: i18n.translate(
+ 'xpack.agentBuilder.overview.autoInclude.enabledToast',
+ {
+ defaultMessage: 'Built-in capabilities enabled',
+ }
+ ),
+ autoIncludeDisabledToast: i18n.translate(
+ 'xpack.agentBuilder.overview.autoInclude.disabledToast',
+ {
+ defaultMessage: 'Built-in capabilities disabled',
+ }
+ ),
+ autoIncludeErrorToast: i18n.translate('xpack.agentBuilder.overview.autoInclude.errorToast', {
+ defaultMessage: 'Unable to update capabilities setting',
+ }),
+ instructionsSavedToast: i18n.translate('xpack.agentBuilder.overview.instructions.savedToast', {
+ defaultMessage: 'Instructions saved',
+ }),
+ instructionsErrorToast: i18n.translate('xpack.agentBuilder.overview.instructions.errorToast', {
+ defaultMessage: 'Unable to save instructions',
+ }),
+ docsLink: i18n.translate('xpack.agentBuilder.overview.docsLink', {
+ defaultMessage: 'Docs',
+ }),
+ editDetailsButton: i18n.translate('xpack.agentBuilder.overview.editDetailsButton', {
+ defaultMessage: 'Edit details',
+ }),
+ capabilitiesTitle: i18n.translate('xpack.agentBuilder.overview.capabilities.title', {
+ defaultMessage: 'Capabilities',
+ }),
+ capabilitiesDescription: i18n.translate(
+ 'xpack.agentBuilder.overview.capabilities.description',
+ {
+ defaultMessage: 'Manage the capabilities this agent uses to perform tasks and activities.',
+ }
+ ),
+ skillsDescription: i18n.translate(
+ 'xpack.agentBuilder.overview.capabilities.skillsDescription',
+ {
+ defaultMessage: 'Combine prompts and tools into reusable logic your agent can invoke.',
+ }
+ ),
+ addSkill: i18n.translate('xpack.agentBuilder.overview.capabilities.addSkill', {
+ defaultMessage: 'Add a skill',
+ }),
+ customizeSkills: i18n.translate('xpack.agentBuilder.overview.capabilities.customizeSkills', {
+ defaultMessage: 'Customize',
+ }),
+ pluginsDescription: i18n.translate(
+ 'xpack.agentBuilder.overview.capabilities.pluginsDescription',
+ {
+ defaultMessage:
+ 'Add packaged sets of skills from external sources to quickly extend your agent.',
+ }
+ ),
+ addPlugin: i18n.translate('xpack.agentBuilder.overview.capabilities.addPlugin', {
+ defaultMessage: 'Add a plugin',
+ }),
+ customizePlugins: i18n.translate('xpack.agentBuilder.overview.capabilities.customizePlugins', {
+ defaultMessage: 'Customize',
+ }),
+ connectorsDescription: i18n.translate(
+ 'xpack.agentBuilder.overview.capabilities.connectorsDescription',
+ {
+ defaultMessage: 'Connect external services to give your agent access to data and actions.',
+ }
+ ),
+ addConnector: i18n.translate('xpack.agentBuilder.overview.capabilities.addConnector', {
+ defaultMessage: 'Add a connector',
+ }),
+ settingsTitle: i18n.translate('xpack.agentBuilder.overview.settings.title', {
+ defaultMessage: 'Settings',
+ }),
+ settingsDescription: i18n.translate('xpack.agentBuilder.overview.settings.description', {
+ defaultMessage: 'Configure how this agent behaves and how its capabilities are managed.',
+ }),
+ autoIncludeTitle: i18n.translate('xpack.agentBuilder.overview.settings.autoIncludeTitle', {
+ defaultMessage: 'Include built-in capabilities automatically',
+ }),
+ autoIncludeDescription: i18n.translate(
+ 'xpack.agentBuilder.overview.settings.autoIncludeDescription',
+ {
+ defaultMessage:
+ 'Automatically include all current and future Elastic-built skills, plugins, and tools. Turn off to manage them manually.',
+ }
+ ),
+ autoIncludeLabel: i18n.translate('xpack.agentBuilder.overview.settings.autoIncludeLabel', {
+ defaultMessage: 'Include built-in capabilities automatically',
+ }),
+ instructionsTitle: i18n.translate('xpack.agentBuilder.overview.settings.instructionsTitle', {
+ defaultMessage: 'Use custom instructions',
+ }),
+ instructionsDescription: i18n.translate(
+ 'xpack.agentBuilder.overview.settings.instructionsDescription',
+ {
+ defaultMessage:
+ 'Define how the agent should behave, what it should prioritize, and any rules it should follow when responding.',
+ }
+ ),
+ instructionsPlaceholder: i18n.translate(
+ 'xpack.agentBuilder.overview.settings.instructionsPlaceholder',
+ {
+ defaultMessage: 'No custom instructions.',
+ }
+ ),
+ saveInstructionsButton: i18n.translate(
+ 'xpack.agentBuilder.overview.settings.saveInstructionsButton',
+ {
+ defaultMessage: 'Save instructions',
+ }
+ ),
+ byAuthor: (author: string) =>
+ i18n.translate('xpack.agentBuilder.overview.byAuthor', {
+ defaultMessage: 'By {author}',
+ values: { author },
+ }),
+ agentId: (id: string) =>
+ i18n.translate('xpack.agentBuilder.overview.agentId', {
+ defaultMessage: 'ID {id}',
+ values: { id },
+ }),
+ skillsLabel: (count: number) =>
+ i18n.translate('xpack.agentBuilder.overview.capabilities.skills', {
+ defaultMessage: '{count, plural, one {Skill} other {Skills}}',
+ values: { count },
+ }),
+ pluginsLabel: (count: number) =>
+ i18n.translate('xpack.agentBuilder.overview.capabilities.plugins', {
+ defaultMessage: '{count, plural, one {Plugin} other {Plugins}}',
+ values: { count },
+ }),
+ connectorsLabel: (count: number) =>
+ i18n.translate('xpack.agentBuilder.overview.capabilities.connectors', {
+ defaultMessage: '{count, plural, one {Connector} other {Connectors}}',
+ values: { count },
+ }),
+ },
navigationAbort: {
title: i18n.translate('xpack.agentBuilder.navigationAbort.title', {
defaultMessage: 'Abort chat request?',
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.test.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.test.ts
index 456141c1c9c24..c4e6907fe7551 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.test.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.test.ts
@@ -14,6 +14,7 @@ import { ToolType, allToolsSelectionWildcard } from '@kbn/agent-builder-common';
import {
toggleToolSelection,
isToolSelected,
+ getActiveTools,
cleanInvalidToolReferences,
} from './tool_selection_utils';
import type { AgentEditState } from '../hooks/agents/use_agent_edit';
@@ -79,6 +80,53 @@ describe('tool_selection_utils', () => {
});
});
+ describe('getActiveTools', () => {
+ const defaultToolIds = new Set(['tool1', 'tool3']);
+
+ it('should return only explicitly selected tools when elastic capabilities are disabled', () => {
+ const selections: ToolSelection[] = [{ tool_ids: ['tool2'] }];
+ const result = getActiveTools(mockTools, selections, false, defaultToolIds);
+
+ expect(result.map((t) => t.id)).toEqual(['tool2']);
+ });
+
+ it('should include default tools when elastic capabilities are enabled', () => {
+ const selections: ToolSelection[] = [{ tool_ids: ['tool2'] }];
+ const result = getActiveTools(mockTools, selections, true, defaultToolIds);
+
+ expect(result.map((t) => t.id)).toEqual(['tool2', 'tool1', 'tool3']);
+ });
+
+ it('should not duplicate tools that are both explicit and default', () => {
+ const selections: ToolSelection[] = [{ tool_ids: ['tool1', 'tool2'] }];
+ const result = getActiveTools(mockTools, selections, true, defaultToolIds);
+
+ expect(result.map((t) => t.id)).toEqual(['tool1', 'tool2', 'tool3']);
+ });
+
+ it('should return empty array when no tools match selections', () => {
+ const selections: ToolSelection[] = [{ tool_ids: [] }];
+ const result = getActiveTools(mockTools, selections, false, defaultToolIds);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return only default tools when no explicit selections and elastic capabilities are enabled', () => {
+ const selections: ToolSelection[] = [{ tool_ids: [] }];
+ const result = getActiveTools(mockTools, selections, true, defaultToolIds);
+
+ expect(result.map((t) => t.id)).toEqual(['tool1', 'tool3']);
+ });
+
+ it('should handle wildcard selections with elastic capabilities', () => {
+ const selections: ToolSelection[] = [{ tool_ids: [allToolsSelectionWildcard] }];
+ const result = getActiveTools(mockTools, selections, true, defaultToolIds);
+
+ // All tools already selected via wildcard, no defaults to append
+ expect(result.map((t) => t.id)).toEqual(['tool1', 'tool2', 'tool3']);
+ });
+ });
+
describe('cleanInvalidToolReferences', () => {
const mockToolDefinitions: ToolDefinition[] = [
{
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.ts
index 2081bbc0f7258..168a6b64fe2db 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.ts
+++ b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/tool_selection_utils.ts
@@ -93,6 +93,28 @@ export const toggleToolSelection = (
}
};
+/**
+ * Returns the list of active tools for an agent, combining explicitly selected tools
+ * with default tools when elastic capabilities are enabled. Default tools that are
+ * already explicitly selected are not duplicated.
+ */
+export const getActiveTools = (
+ allTools: T[],
+ agentToolSelections: ToolSelection[],
+ enableElasticCapabilities: boolean,
+ defaultToolIds: Set
+): T[] => {
+ const explicitTools = allTools.filter((t) => isToolSelected(t, agentToolSelections));
+ if (enableElasticCapabilities) {
+ const explicitIdSet = new Set(explicitTools.map((t) => t.id));
+ const defaultToolsNotExplicit = allTools.filter(
+ (t) => defaultToolIds.has(t.id) && !explicitIdSet.has(t.id)
+ );
+ return [...explicitTools, ...defaultToolsNotExplicit];
+ }
+ return explicitTools;
+};
+
/**
* Removes invalid tool references from the agent configuration.
* Filters out tool IDs that don't exist in the available tools list,
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/embeddable/embeddable_conversation.tsx b/x-pack/platform/plugins/shared/agent_builder/public/embeddable/embeddable_conversation.tsx
index 952c8713340ce..8867f4467853b 100644
--- a/x-pack/platform/plugins/shared/agent_builder/public/embeddable/embeddable_conversation.tsx
+++ b/x-pack/platform/plugins/shared/agent_builder/public/embeddable/embeddable_conversation.tsx
@@ -11,7 +11,7 @@ import { css } from '@emotion/react';
import type { EmbeddableConversationInternalProps } from './types';
import { EmbeddableConversationsProvider } from '../application/context/conversation/embeddable_conversations_provider';
import { Conversation } from '../application/components/conversations/conversation';
-import { ConversationHeader } from '../application/components/conversations/conversation_header/conversation_header';
+import { EmbeddableConversationHeader } from '../application/components/conversations/embeddable_conversation_header/embeddable_conversation_header';
import {
conversationBackgroundStyles,
headerHeight,
@@ -66,7 +66,7 @@ export const EmbeddableConversationInternal: React.FC
-
+
diff --git a/x-pack/platform/plugins/shared/agent_builder/tsconfig.json b/x-pack/platform/plugins/shared/agent_builder/tsconfig.json
index 01dfe1dbda314..659c02eae9d2b 100644
--- a/x-pack/platform/plugins/shared/agent_builder/tsconfig.json
+++ b/x-pack/platform/plugins/shared/agent_builder/tsconfig.json
@@ -107,7 +107,6 @@
"@kbn/core-elasticsearch-server-mocks",
"@kbn/core-data-streams-server",
"@kbn/task-manager-plugin",
- "@kbn/deeplinks-data-sources",
"@kbn/evals-plugin",
"@kbn/usage-api-plugin",
"@kbn/es-query",
diff --git a/x-pack/platform/test/agent_builder_functional/tests/agents/agents_list.ts b/x-pack/platform/test/agent_builder_functional/tests/agents/agents_list.ts
index ff4c9bb4295ff..cfb2637cfb09f 100644
--- a/x-pack/platform/test/agent_builder_functional/tests/agents/agents_list.ts
+++ b/x-pack/platform/test/agent_builder_functional/tests/agents/agents_list.ts
@@ -24,7 +24,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('renders', async function () {
- await agentBuilder.navigateToApp('agents');
+ await agentBuilder.navigateToApp('manage/agents');
const titleSelector = 'agentBuilderAgentsListPageTitle';
const newAgentButtonSelector = 'agentBuilderNewAgentButton';
@@ -62,11 +62,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('chats with agent', async function () {
const agent = agents[0];
await agentBuilder.clickAgentChat(agent.id);
- await browser.waitForUrlToBe(`/app/agent_builder/conversations/new?agent_id=${agent.id}`);
+ await browser.waitForUrlToBe(`/app/agent_builder/agents/${agent.id}/conversations/new`);
const agentText = await testSubjects.getVisibleText('agentBuilderAgentSelectorButton');
expect(agentText).to.contain(agent.name);
// go back to agents list
- await agentBuilder.navigateToApp('agents');
+ await agentBuilder.navigateToApp('manage/agents');
});
it('has edit link with correct href', async function () {
diff --git a/x-pack/platform/test/agent_builder_functional/tests/agents/edit_agent.ts b/x-pack/platform/test/agent_builder_functional/tests/agents/edit_agent.ts
index 2665a2a2276b7..bbb3ba96053d2 100644
--- a/x-pack/platform/test/agent_builder_functional/tests/agents/edit_agent.ts
+++ b/x-pack/platform/test/agent_builder_functional/tests/agents/edit_agent.ts
@@ -26,7 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('should navigate to agent edit form', async function () {
await agentBuilder.clickAgentEdit(agent.id);
- await browser.waitForUrlToBe(`/app/agent_builder/agents/${agent.id}`);
+ await browser.waitForUrlToBe(`/app/agent_builder/manage/agents/${agent.id}`);
});
it('should show agent name as page title', async function () {
@@ -56,7 +56,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('should clone agent', async function () {
agent = agents[1];
await agentBuilder.clickAgentClone(agent.id);
- await browser.waitForUrlToBe(`/app/agent_builder/agents/new?source_id=${agent.id}`);
+ await browser.waitForUrlToBe(`/app/agent_builder/manage/agents/new?source_id=${agent.id}`);
expect(await agentBuilder.getAgentFormDisplayName()).to.be(agent.name);
const idInput = agentBuilder.getAgentIdInput();
expect(await idInput.getValue()).to.be('test_agent_3');
diff --git a/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_error_handling.ts b/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_error_handling.ts
index 36995a9abb75f..6c1358f260be8 100644
--- a/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_error_handling.ts
+++ b/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_error_handling.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { AGENT_BUILDER_TOUR_STORAGE_KEY } from '@kbn/agent-builder-plugin/public/application/storage_keys';
+import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common';
import type { LlmProxy } from '../../../agent_builder_api_integration/utils/llm_proxy';
import { createLlmProxy } from '../../../agent_builder_api_integration/utils/llm_proxy';
import {
@@ -31,8 +31,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
before(async () => {
llmProxy = await createLlmProxy(log);
await createConnector(llmProxy, supertest);
- await agentBuilder.navigateToApp('conversations/new');
- await browser.setLocalStorageItem(AGENT_BUILDER_TOUR_STORAGE_KEY, 'true');
+ await agentBuilder.navigateToApp();
});
after(async () => {
@@ -52,7 +51,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const MOCKED_RESPONSE = 'This is a successful response after retry';
const MOCKED_TITLE = 'Error Handling Test';
- await agentBuilder.navigateToApp('conversations/new');
+ await agentBuilder.navigateToApp();
// setup interceptors to return 400 error
await setupAgentDirectError({
@@ -103,7 +102,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const INVALID_ID = 'this-id-does-not-exist-12345';
const initialUrl = await browser.getCurrentUrl();
- await agentBuilder.navigateToApp(`conversations/${INVALID_ID}`);
+ await agentBuilder.navigateToApp(
+ `agents/${agentBuilderDefaultAgentId}/conversations/${INVALID_ID}`
+ );
await testSubjects.existOrFail('errorPrompt');
@@ -126,7 +127,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const MOCKED_RESPONSE = 'This is a successful response in new conversation';
const MOCKED_TITLE = 'New Conversation After Error';
- await agentBuilder.navigateToApp('conversations/new');
+ await agentBuilder.navigateToApp();
// setup interceptors to return 400 error
await setupAgentDirectError({
@@ -232,7 +233,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const NEW_RESPONSE = 'This is a successful response after error';
const MOCKED_TITLE = 'Error Cleared Test';
- await agentBuilder.navigateToApp('conversations/new');
+ await agentBuilder.navigateToApp();
// setup interceptors to return 400 error
await setupAgentDirectError({
diff --git a/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_flow.ts b/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_flow.ts
index 7985d9850ff8a..3fa1590bbfa67 100644
--- a/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_flow.ts
+++ b/x-pack/platform/test/agent_builder_functional/tests/converse/conversation_flow.ts
@@ -41,7 +41,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('navigates to new conversation page and shows initial state', async () => {
- await agentBuilder.navigateToApp('conversations/new');
+ await agentBuilder.navigateToApp();
// Assert the welcome page is displayed
await testSubjects.existOrFail('agentBuilderWelcomePage');
@@ -76,15 +76,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const responseText = await responseElement.getVisibleText();
expect(responseText).to.contain(MOCKED_RESPONSE);
- const titleElement = await testSubjects.find('agentBuilderConversationTitle');
+ const titleElement = await testSubjects.find('agentBuilderConversationTitleButton');
const titleText = await titleElement.getVisibleText();
expect(titleText).to.contain(MOCKED_TITLE);
- await agentBuilder.openConversationsHistory();
- // Wait for the conversation to appear in the list
+ // Wait for the conversation to appear in the sidebar
await retry.try(async () => {
- const conversationList = await testSubjects.find('agentBuilderConversationList');
- const conversationText = await conversationList.getVisibleText();
+ const conversationItem = await testSubjects.find(
+ `agentBuilderSidebarConversation-${conversationId}`
+ );
+ const conversationText = await conversationItem.getVisibleText();
expect(conversationText).to.contain(MOCKED_TITLE);
});
});
diff --git a/x-pack/platform/test/agent_builder_functional/tests/tools/landing_page.ts b/x-pack/platform/test/agent_builder_functional/tests/tools/landing_page.ts
index 33e67a9839d46..641fb2c57dce9 100644
--- a/x-pack/platform/test/agent_builder_functional/tests/tools/landing_page.ts
+++ b/x-pack/platform/test/agent_builder_functional/tests/tools/landing_page.ts
@@ -11,12 +11,15 @@ export default function ({ getPageObjects, getService }: AgentBuilderUiFtrProvid
const { agentBuilder } = getPageObjects(['agentBuilder']);
const testSubjects = getService('testSubjects');
const supertest = getService('supertest');
+ const retry = getService('retry');
describe('tools landing page', function () {
it('should render', async () => {
await agentBuilder.navigateToToolsLanding();
- await testSubjects.existOrFail('agentBuilderToolsPage');
- await testSubjects.existOrFail('agentBuilderToolsTable');
+ await retry.try(async () => {
+ await testSubjects.existOrFail('agentBuilderToolsPage');
+ await testSubjects.existOrFail('agentBuilderToolsTable');
+ });
});
it('should bulk delete tools from the table', async () => {
@@ -37,14 +40,18 @@ export default function ({ getPageObjects, getService }: AgentBuilderUiFtrProvid
}
await agentBuilder.navigateToToolsLanding();
- await testSubjects.existOrFail('agentBuilderToolsTable');
+ await retry.try(async () => {
+ await testSubjects.existOrFail('agentBuilderToolsTable');
+ });
// Search for our specific tools to filter the table (avoids pagination issues)
const search = agentBuilder.toolsSearch();
await search.type(`ftr.esql.${timestamp}`);
- // Wait for the first tool to appear (ensures search has filtered the results)
- await testSubjects.existOrFail(`agentBuilderToolsTableRow-${ids[0]}`);
+ // Wait for the tools data to load and the first row to appear before proceeding
+ await retry.try(async () => {
+ await testSubjects.existOrFail(`agentBuilderToolsTableRow-${ids[0]}`);
+ });
await agentBuilder.bulkDeleteTools(ids);
diff --git a/x-pack/platform/test/functional/page_objects/agent_builder_page.ts b/x-pack/platform/test/functional/page_objects/agent_builder_page.ts
index 18209a97fd857..3d9530adf9d2a 100644
--- a/x-pack/platform/test/functional/page_objects/agent_builder_page.ts
+++ b/x-pack/platform/test/functional/page_objects/agent_builder_page.ts
@@ -6,6 +6,7 @@
*/
import type { ToolType } from '@kbn/agent-builder-common';
+import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common';
import { subj } from '@kbn/test-subj-selector';
import { AGENT_BUILDER_APP_ID } from '../../agent_builder/common/constants';
import type { LlmProxy } from '../../agent_builder_api_integration/utils/llm_proxy';
@@ -31,7 +32,7 @@ export class AgentBuilderPageObject extends FtrService {
/**
* Navigate to the AgentBuilder app
*/
- async navigateToApp(path: string = 'conversations/new') {
+ async navigateToApp(path: string = `agents/${agentBuilderDefaultAgentId}/conversations/new`) {
await this.common.navigateToApp(AGENT_BUILDER_APP_ID, { path });
}
@@ -58,7 +59,7 @@ export class AgentBuilderPageObject extends FtrService {
async getCurrentConversationIdFromUrl(): Promise {
return await this.retry.try(async () => {
const url = await this.browser.getCurrentUrl();
- // URL should be something like: /app/agent_builder/conversations/{conversationId}
+ // URL should be something like: /app/agent_builder/agents/{agentId}/conversations/{conversationId}
const match = url.match(/\/conversations\/([^\/\?]+)/);
if (!match) {
throw new Error('Could not extract conversation ID from URL');
@@ -78,7 +79,7 @@ export class AgentBuilderPageObject extends FtrService {
withToolCall: boolean = false
): Promise {
// Navigate to new conversation
- await this.navigateToApp('conversations/new');
+ await this.navigateToApp();
await (withToolCall
? setupAgentCallSearchToolWithNoIndexSelectedThenAnswer({
@@ -112,37 +113,15 @@ export class AgentBuilderPageObject extends FtrService {
return await this.getCurrentConversationIdFromUrl();
}
- async openConversationsHistory() {
- // Only open if not already open
- if (await this.isConversationsHistoryOpen()) {
- return;
- }
-
- const conversationsHistoryToggleBtn = await this.testSubjects.find(
- 'agentBuilderConversationsHistoryToggleBtn'
- );
- await conversationsHistoryToggleBtn.click();
-
- // Wait for the conversations history popover to be visible and populated
- await this.retry.try(async () => {
- const conversationList = await this.testSubjects.find('agentBuilderConversationList');
- // Verify the list is actually visible and has content
- const isDisplayed = await conversationList.isDisplayed();
- if (!isDisplayed) {
- throw new Error('Conversation list is not displayed');
- }
- });
- }
-
/**
- * Check if the conversations history popover is currently open
+ * Ensure the Chats accordion in the sidebar is expanded.
+ * It defaults to open but can be collapsed by the user or when agent settings routes are active.
*/
- async isConversationsHistoryOpen(): Promise {
- try {
- const conversationList = await this.testSubjects.find('agentBuilderConversationList');
- return await conversationList.isDisplayed();
- } catch {
- return false;
+ private async ensureChatsAccordionOpen() {
+ const toggle = await this.testSubjects.find('agentBuilderSidebarChatsToggle');
+ const isExpanded = await toggle.getAttribute('aria-expanded');
+ if (isExpanded !== 'true') {
+ await toggle.click();
}
}
@@ -150,9 +129,10 @@ export class AgentBuilderPageObject extends FtrService {
* Navigate to an existing conversation by clicking on it in the history sidebar
*/
async navigateToConversationViaHistory(conversationId: string) {
- await this.openConversationsHistory();
-
- const conversationItem = await this.testSubjects.find(`conversationItem-${conversationId}`);
+ await this.ensureChatsAccordionOpen();
+ const conversationItem = await this.testSubjects.find(
+ `agentBuilderSidebarConversation-${conversationId}`
+ );
await conversationItem.click();
}
@@ -160,7 +140,9 @@ export class AgentBuilderPageObject extends FtrService {
* Navigate to an existing conversation using the conversation ID in the URL
*/
async navigateToConversationById(conversationId: string) {
- await this.navigateToApp(`conversations/${conversationId}`);
+ await this.navigateToApp(
+ `agents/${agentBuilderDefaultAgentId}/conversations/${conversationId}`
+ );
}
/**
@@ -187,18 +169,20 @@ export class AgentBuilderPageObject extends FtrService {
}
/**
- * Delete a conversation by clicking the more actions button and then the delete button
+ * Delete a conversation by clicking the title button popover and then the delete button
*/
async deleteConversation(conversationId: string) {
- await this.openConversationsHistory();
+ await this.ensureChatsAccordionOpen();
// Click on conversation to open it
- const conversationItem = await this.testSubjects.find(`conversationItem-${conversationId}`);
+ const conversationItem = await this.testSubjects.find(
+ `agentBuilderSidebarConversation-${conversationId}`
+ );
await conversationItem.click();
- // Click on the more actions button
- const moreActionsButton = await this.testSubjects.find('agentBuilderMoreActionsButton');
- await moreActionsButton.click();
+ // Click the title button to open the popover with rename/delete actions
+ const titleButton = await this.testSubjects.find('agentBuilderConversationTitleButton');
+ await titleButton.click();
// Click on the delete button from the popover
const deleteButton = await this.testSubjects.find('agentBuilderConversationDeleteButton');
@@ -207,9 +191,9 @@ export class AgentBuilderPageObject extends FtrService {
const confirmButton = await this.testSubjects.find('confirmModalConfirmButton');
await confirmButton.click();
- // Wait for the conversation to be removed
+ // Wait for the conversation to be removed from the sidebar
await this.retry.try(async () => {
- await this.testSubjects.missingOrFail(`conversationItem-${conversationId}`);
+ await this.testSubjects.missingOrFail(`agentBuilderSidebarConversation-${conversationId}`);
});
}
@@ -217,10 +201,10 @@ export class AgentBuilderPageObject extends FtrService {
* Check if a conversation exists in the history by conversation ID
*/
async isConversationInHistory(conversationId: string): Promise {
- await this.openConversationsHistory();
+ await this.ensureChatsAccordionOpen();
try {
- await this.testSubjects.find(`conversationItem-${conversationId}`);
+ await this.testSubjects.find(`agentBuilderSidebarConversation-${conversationId}`);
return true;
} catch (error) {
return false;
@@ -239,28 +223,34 @@ export class AgentBuilderPageObject extends FtrService {
* Click the new conversation button
*/
async clickNewConversationButton() {
- const newButton = await this.testSubjects.find('agentBuilderNewConversationButton');
+ const newButton = await this.testSubjects.find('agentBuilderSidebarNewConversationButton');
await newButton.click();
}
/**
- * Get the current conversation title text
+ * Get the current conversation title text.
+ * For persisted conversations the title is inside a button (popover trigger);
+ * for unsaved ones it is a plain h4.
*/
async getConversationTitle(): Promise {
- const titleElement = await this.testSubjects.find('agentBuilderConversationTitle');
+ const isPersisted = await this.testSubjects.exists('agentBuilderConversationTitleButton');
+ const selector = isPersisted
+ ? 'agentBuilderConversationTitleButton'
+ : 'agentBuilderConversationTitle';
+ const titleElement = await this.testSubjects.find(selector);
return await titleElement.getVisibleText();
}
/**
- * Rename a conversation by hovering over the title, clicking the pencil icon,
- * entering the new name, and submitting
+ * Rename a conversation by clicking the title button to open the popover,
+ * then selecting rename, entering the new name, and submitting.
*/
async renameConversation(newTitle: string): Promise {
- // Hover over the conversation title to reveal the pencil icon
- const titleElement = await this.testSubjects.find('agentBuilderConversationTitle');
- await titleElement.moveMouseTo();
+ // Click the title button to open the popover
+ const titleButton = await this.testSubjects.find('agentBuilderConversationTitleButton');
+ await titleButton.click();
- // Click the pencil icon to enter edit mode
+ // Click the rename button from the popover
const renameButton = await this.testSubjects.find('agentBuilderConversationRenameButton');
await renameButton.click();
@@ -313,15 +303,15 @@ export class AgentBuilderPageObject extends FtrService {
* ==========================
*/
async navigateToToolsLanding() {
- await this.navigateToApp('tools');
+ await this.navigateToApp('manage/tools');
}
async navigateToNewTool() {
- await this.navigateToApp('tools/new');
+ await this.navigateToApp('manage/tools/new');
}
async navigateToTool(toolId: string) {
- await this.navigateToApp(`tools/${toolId}`);
+ await this.navigateToApp(`manage/tools/${toolId}`);
}
/*
@@ -390,7 +380,7 @@ export class AgentBuilderPageObject extends FtrService {
* ==========================
*/
async navigateToBulkImportMcp() {
- await this.navigateToApp('tools/bulk_import_mcp');
+ await this.navigateToApp('manage/tools/bulk_import_mcp');
}
async openManageMcpMenu() {
@@ -540,7 +530,7 @@ export class AgentBuilderPageObject extends FtrService {
* ==========================
*/
async createAgentViaUI({ id, name, labels }: { id: string; name: string; labels: string[] }) {
- await this.navigateToApp('agents/new');
+ await this.navigateToApp('manage/agents/new');
const selectors = {
inputs: {
id: 'agentSettingsIdInput',