diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx
index 2131b7f55..40175562d 100644
--- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx
+++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx
@@ -3,15 +3,29 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
import patternflyAvatar from './patternfly_avatar.jpg';
export const MessageWithDeepThinkingExample: FunctionComponent = () => (
-
+ <>
+
+
+ >
);
diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx
index 3498e3abb..ac9a9952a 100644
--- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx
+++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx
@@ -31,7 +31,7 @@ export const MessageWithToolCallExample: FunctionComponent = () => {
name="Bot"
role="bot"
avatar={patternflyAvatar}
- content="This example has an expandable tool call title, with an additional description::"
+ content="This example has an expandable tool call title, with an additional description:"
toolCall={{
titleText: "Calling 'awesome_tool_expansion'",
expandableContent: 'This is the expandable content for the tool call.',
@@ -39,6 +39,19 @@ export const MessageWithToolCallExample: FunctionComponent = () => {
loadingText: "Loading 'awesome_tool_expansion'"
}}
/>
+
);
diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx
index 6ae583dd0..7303f9a14 100644
--- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx
+++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx
@@ -23,113 +23,230 @@ export const MessageWithToolResponseExample: FunctionComponent = () => {
};
return (
-
-
-
-
-
-
-
-
- toolName
-
-
-
-
- Execution time:
- 0.12 seconds
-
-
-
-
-
- }
- >
-
-
- ),
- cardBody: (
- <>
-
+
-
+
+
+
+
+
+
+ toolName
+
+
+
+
+ Execution time:
+ 0.12 seconds
+
+
+
+
+
+ }
+ >
+
+
+ ),
+ cardBody: (
+ <>
+
- Parameters
-
-
- Optional description text for parameters.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Parameters
+
+
+ Optional description text for parameters.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Response
+
+
+ Descriptive text about the tool response, including completion status, details on the data that
+ was processed, or anything else relevant to the use case.
+
+
+
+
+ >
+ )
+ }}
+ />
+
+
+
+
+
+
+
+
+ toolName
+
+
+
+
+ Execution time:
+ 0.12 seconds
+
+
+
+
+
+ }
+ >
+
+
+ ),
+ cardBody: (
+ <>
+
- Response
-
-
- Descriptive text about the tool response, including completion status, details on the data that was
- processed, or anything else relevant to the use case.
-
-
-
-
- >
- )
- }}
- />
+
+ Parameters
+
+
+ Optional description text for parameters.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Response
+
+
+ Descriptive text about the tool response, including completion status, details on the data that
+ was processed, or anything else relevant to the use case.
+
+
+
+
+ >
+ )
+ }}
+ />
+ >
);
};
diff --git a/packages/module/src/DeepThinking/DeepThinking.test.tsx b/packages/module/src/DeepThinking/DeepThinking.test.tsx
index 03f333385..1621e4e02 100644
--- a/packages/module/src/DeepThinking/DeepThinking.test.tsx
+++ b/packages/module/src/DeepThinking/DeepThinking.test.tsx
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import DeepThinking from './DeepThinking';
@@ -58,4 +59,64 @@ describe('DeepThinking', () => {
const subheadingContainer = container.querySelector('.pf-chatbot__tool-response-subheading');
expect(subheadingContainer).toBeFalsy();
});
+
+ it('should pass through cardBodyProps', () => {
+ render(
+
+ );
+
+ const cardBody = screen.getByText('Thinking content').closest('.pf-v6-c-card__body');
+ expect(cardBody).toHaveClass('custom-card-body-class');
+ });
+
+ it('Renders expanded by default', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
+ expect(screen.getByText('Thinking content')).toBeVisible();
+ });
+
+ it('Renders collapsed when isDefaultExpanded is false', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
+ expect(screen.getByText('Thinking content')).not.toBeVisible();
+ });
+
+ it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
+ expect(screen.getByText('Thinking content')).toBeVisible();
+ });
+
+ it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => {
+ const user = userEvent.setup();
+ const customOnToggle = jest.fn();
+
+ render(
+
+ );
+
+ const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent });
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
+
+ await user.click(toggleButton);
+
+ expect(customOnToggle).toHaveBeenCalled();
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
+ expect(screen.getByText('Thinking content')).not.toBeVisible();
+ });
});
diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx
index 9b485cb6b..11c29549d 100644
--- a/packages/module/src/DeepThinking/DeepThinking.tsx
+++ b/packages/module/src/DeepThinking/DeepThinking.tsx
@@ -14,6 +14,8 @@ import { useState, type FunctionComponent } from 'react';
export interface DeepThinkingProps {
/** Toggle content shown for expandable section */
toggleContent: React.ReactNode;
+ /** Flag indicating whether the expandable content is expanded by default. */
+ isDefaultExpanded?: boolean;
/** Additional props passed to expandable section */
expandableSectionProps?: Omit;
/** Subheading rendered inside expandable section */
@@ -32,9 +34,10 @@ export const DeepThinking: FunctionComponent = ({
expandableSectionProps,
subheading,
toggleContent,
+ isDefaultExpanded = true,
cardBodyProps
}: DeepThinkingProps) => {
- const [isExpanded, setIsExpanded] = useState(true);
+ const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
setIsExpanded(isExpanded);
diff --git a/packages/module/src/ToolCall/ToolCall.test.tsx b/packages/module/src/ToolCall/ToolCall.test.tsx
index a43c9d64c..91f4e7432 100644
--- a/packages/module/src/ToolCall/ToolCall.test.tsx
+++ b/packages/module/src/ToolCall/ToolCall.test.tsx
@@ -181,4 +181,55 @@ describe('ToolCall', () => {
render();
expect(screen.getByRole('button', { name: 'Run tool' }).closest('#card-footer-test-id')).toBeVisible();
});
+
+ it('Renders collapsed by default when expandableContent is provided', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'false');
+ expect(screen.queryByText('Expandable Content')).not.toBeVisible();
+ });
+
+ it('Renders expanded when isDefaultExpanded is true', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
+ expect(screen.getByText('Expandable Content')).toBeVisible();
+ });
+
+ it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
+ expect(screen.getByText('Expandable Content')).toBeVisible();
+ });
+
+ it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => {
+ const user = userEvent.setup();
+ const customOnToggle = jest.fn();
+
+ render(
+
+ );
+
+ const toggleButton = screen.getByRole('button', { name: defaultProps.titleText });
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
+
+ await user.click(toggleButton);
+
+ expect(customOnToggle).toHaveBeenCalledTimes(1);
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
+ expect(screen.queryByText('Expandable Content')).not.toBeVisible();
+ });
});
diff --git a/packages/module/src/ToolCall/ToolCall.tsx b/packages/module/src/ToolCall/ToolCall.tsx
index a3e6e67d8..774984813 100644
--- a/packages/module/src/ToolCall/ToolCall.tsx
+++ b/packages/module/src/ToolCall/ToolCall.tsx
@@ -1,4 +1,4 @@
-import { type FunctionComponent } from 'react';
+import { useState, type FunctionComponent } from 'react';
import {
ActionList,
ActionListProps,
@@ -31,6 +31,8 @@ export interface ToolCallProps {
spinnerProps?: SpinnerProps;
/** Content to render within an expandable section. */
expandableContent?: React.ReactNode;
+ /** Flag indicating whether the expandable content is expanded by default. */
+ isDefaultExpanded?: boolean;
/** Text content for the "run" action button. */
runButtonText?: string;
/** Additional props for the "run" action button. */
@@ -66,6 +68,7 @@ export const ToolCall: FunctionComponent = ({
loadingText,
isLoading,
expandableContent,
+ isDefaultExpanded = false,
runButtonText = 'Run tool',
runButtonProps,
runActionItemProps,
@@ -82,6 +85,12 @@ export const ToolCall: FunctionComponent = ({
expandableSectionProps,
spinnerProps
}: ToolCallProps) => {
+ const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
+
+ const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
+ setIsExpanded(isExpanded);
+ };
+
const titleContent = (
{isLoading ? (
@@ -124,6 +133,8 @@ export const ToolCall: FunctionComponent = ({
diff --git a/packages/module/src/ToolResponse/ToolResponse.test.tsx b/packages/module/src/ToolResponse/ToolResponse.test.tsx
index 91cdd6950..8d4e3135d 100644
--- a/packages/module/src/ToolResponse/ToolResponse.test.tsx
+++ b/packages/module/src/ToolResponse/ToolResponse.test.tsx
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import ToolResponse from './ToolResponse';
@@ -105,4 +106,47 @@ describe('ToolResponse', () => {
const { container } = render();
expect(container.querySelector('.pf-v6-c-divider')).toBeFalsy();
});
+
+ it('Renders expanded by default', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
+ expect(screen.getByText(defaultProps.cardTitle)).toBeVisible();
+ expect(screen.getByText(defaultProps.cardBody)).toBeVisible();
+ });
+
+ it('Renders collapsed when isDefaultExpanded is false', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
+ expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
+ expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible();
+ });
+
+ it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
+ expect(screen.getByText(defaultProps.cardTitle)).toBeVisible();
+ expect(screen.getByText(defaultProps.cardBody)).toBeVisible();
+ });
+
+ it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => {
+ const user = userEvent.setup();
+ const customOnToggle = jest.fn();
+
+ render(
+
+ );
+
+ const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent });
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
+
+ await user.click(toggleButton);
+
+ expect(customOnToggle).toHaveBeenCalledTimes(1);
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
+ expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
+ expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible();
+ });
});
diff --git a/packages/module/src/ToolResponse/ToolResponse.tsx b/packages/module/src/ToolResponse/ToolResponse.tsx
index d7dc7d608..39bfdb269 100644
--- a/packages/module/src/ToolResponse/ToolResponse.tsx
+++ b/packages/module/src/ToolResponse/ToolResponse.tsx
@@ -18,6 +18,8 @@ import { useState, type FunctionComponent } from 'react';
export interface ToolResponseProps {
/** Toggle content shown for expandable section */
toggleContent: React.ReactNode;
+ /** Flag indicating whether the expandable content is expanded by default. */
+ isDefaultExpanded?: boolean;
/** Additional props passed to expandable section */
expandableSectionProps?: Omit;
/** Subheading rendered inside expandable section */
@@ -51,12 +53,13 @@ export const ToolResponse: FunctionComponent = ({
cardTitle,
cardBodyProps,
toggleContent,
+ isDefaultExpanded = true,
toolResponseCardBodyProps,
toolResponseCardDividerProps,
toolResponseCardProps,
toolResponseCardTitleProps
}: ToolResponseProps) => {
- const [isExpanded, setIsExpanded] = useState(true);
+ const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
setIsExpanded(isExpanded);