Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { ComponentProps, ReactNode, Ref } from 'react';

const mockSetFloatingPosition = jest.fn();
const mockContainerProps = jest.fn();
const mockContainerMounts = jest.fn();
const mockInputProps = jest.fn();
const mockImageUploaderProps = jest.fn();
const mockHasAiChatEntry = jest.fn();
Expand All @@ -36,6 +37,9 @@ jest.mock(
) => void;
} ) {
mockContainerProps( { floatingChatState } );
React.useEffect( () => {
mockContainerMounts();
}, [] );
Comment thread
wellyshen marked this conversation as resolved.
return (
<div>
{ emptyView }
Expand Down Expand Up @@ -212,8 +216,8 @@ jest.mock( '../../hooks/use-has-ai-chat-entry-button', () => ( {

import AgentChat from '../agent-chat';

function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
return render(
function getAgentChatElement( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
return (
<AgentChat
messages={ [] }
suggestions={ [] }
Expand All @@ -234,6 +238,10 @@ function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > =
);
}

function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
return render( getAgentChatElement( props ) );
}

describe( 'AgentChat', () => {
beforeEach( () => {
jest.clearAllMocks();
Expand Down Expand Up @@ -522,4 +530,16 @@ describe( 'AgentChat', () => {

expect( mockContainerProps ).toHaveBeenLastCalledWith( { floatingChatState: 'minimized' } );
} );

it( 'remounts the container only when the dock state changes', () => {
const { rerender } = renderAgentChat( { isDocked: false } );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 1 );

// The remount re-applies the mount-only position/size seeds.
rerender( getAgentChatElement( { isDocked: true } ) );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 2 );

rerender( getAgentChatElement( { isDocked: true, isOpen: true } ) );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 2 );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const mockSetIsOpen = jest.fn();
const mockSetIsDocked = jest.fn();
const mockSetIsMinimized = jest.fn();
const mockSetIsSplitScreen = jest.fn();
const mockSetFloatingPosition = jest.fn();
const mockSetFreeDragPosition = jest.fn();
const mockSetFloatingSize = jest.fn();
const mockUseAgentLayoutManager = jest.fn();
const mockResumeActiveChat = jest.fn();
const mockCloseSidebar = jest.fn();
Expand All @@ -21,6 +24,7 @@ let mockAgentsManagerState: {
isDocked?: boolean;
isMinimized?: boolean;
isSplitScreen?: boolean;
floatingPosition?: 'left' | 'right';
} = { isOpen: true, isDocked: false };
let mockHasAdminBar = false;
let mockShouldUseUnifiedAgent = false;
Expand All @@ -41,6 +45,9 @@ jest.mock( '@wordpress/data', () => ( {
setIsDocked: mockSetIsDocked,
setIsMinimized: mockSetIsMinimized,
setIsSplitScreen: mockSetIsSplitScreen,
setFloatingPosition: mockSetFloatingPosition,
setFreeDragPosition: mockSetFreeDragPosition,
setFloatingSize: mockSetFloatingSize,
} ),
useSelect: () => mockAgentsManagerState,
} ) );
Expand Down Expand Up @@ -453,4 +460,43 @@ describe( 'AgentDock', () => {
expect( mockSetIsSplitScreen ).toHaveBeenCalledWith( nextState );
}
);

it( 'persists the right-side default floating state on the responsive undock', () => {
useWpAdminAgent();
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'left' };

renderAgentDock();
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
act( () => onUndock( true ) );

expect( mockSetFloatingPosition ).toHaveBeenCalledWith( 'right' );
expect( mockSetFreeDragPosition ).toHaveBeenCalledWith( null );
expect( mockSetFloatingSize ).toHaveBeenCalledWith( null );
expect( localStorage.getItem( 'agenttic-chat-position' ) ).toBe( 'right' );
} );

it( 'skips the position save when the persisted side is already right', () => {
useWpAdminAgent();
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'right' };

renderAgentDock();
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
act( () => onUndock( true ) );

expect( mockSetFloatingPosition ).not.toHaveBeenCalled();
expect( mockSetFreeDragPosition ).toHaveBeenCalledWith( null );
} );

it( 'leaves the floating state alone on a manual undock', () => {
useWpAdminAgent();
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'left' };

renderAgentDock();
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
act( () => onUndock( false ) );

expect( mockSetFloatingPosition ).not.toHaveBeenCalled();
expect( mockSetFreeDragPosition ).not.toHaveBeenCalled();
expect( mockSetFloatingSize ).not.toHaveBeenCalled();
} );
} );
20 changes: 5 additions & 15 deletions packages/agents-manager/src/components/agent-chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import {
type ChatState,
type UploadedImage,
} from '@automattic/agenttic-ui';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useMemo, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { formatWritingSuggestionLabels } from '../../hooks/use-empty-view-suggestions';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import { AGENTS_MANAGER_STORE } from '../../stores';
import { getAgentsManagerInlineData } from '../../utils/get-agents-manager-inline-data';
import { isEditorPage } from '../../utils/is-editor-page';
import { isReaderChatHost } from '../../utils/is-reader-chat-agent';
Expand All @@ -32,7 +31,6 @@ import GroupedEmptyView from './grouped-empty-view';
import type { UseImageUploadResult } from '../../hooks/use-image-upload';
import type { ExternalContextCard, ExternalContextCardAction } from '../../utils/external-context';
import type { Message, NoticeConfig } from '@automattic/agenttic-ui/dist/types';
import type { AgentsManagerSelect } from '@automattic/data-stores';
import type { ComponentProps, RefObject } from 'react';

interface Props {
Expand Down Expand Up @@ -184,14 +182,9 @@ export default function AgentChat( {
onContextCardAction,
onContextCardDismiss,
}: Props ) {
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const conversationViewRef = useRef< HTMLDivElement >( null );
const imageUploaderRef = useRef< ImageUploaderHandle >( null );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const floatingPanelProps = useFloatingPanelProps();

const mergedComponents = useMemo(
() => ( { a: CustomALink, ...markdownComponents } ),
Expand Down Expand Up @@ -289,12 +282,9 @@ export default function AgentChat( {

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
// Remount on dock/undock so the mount-only seed props re-apply.
key={ isDocked ? 'embedded' : 'floating' }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My agent says there could be a problem with this key in Zendesk as it's based on undefined data. I don't know if this is still used or how to confirm it.

A user is chatting with a Happiness Engineer through Zendesk in the docked sidebar and has typed a half-finished message. They drag the browser window narrower past the desktop breakpoint (or click "Move to sidebar" / the dock toggle). shouldRenderSidebar flips, the key goes 'embedded' → 'floating', React unmounts and remounts the container, and the internal input state reinitializes to ''. The typed message is gone with no way to recover it.

{ ...floatingPanelProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ messages }
isProcessing={ isProcessing }
Expand Down
34 changes: 31 additions & 3 deletions packages/agents-manager/src/components/agent-dock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { columns, comment, drawerRight, login } from '@wordpress/icons';
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { AGENTTIC_CHAT_POSITION_STORAGE_KEY } from '../../constants';
import { useAgentsManagerContext } from '../../contexts';
import { useSetupCustomActions } from '../../hooks/custom-actions';
import useAdminBarIntegration from '../../hooks/use-admin-bar-integration';
Expand Down Expand Up @@ -87,13 +88,21 @@ export default function AgentDock( {
window.__agentsManagerActions?.desktopMediaQuery
);
const [ isOrchestratorChatEmpty, setIsOrchestratorChatEmpty ] = useState( true );
const { setIsOpen, setIsDocked, setIsMinimized, setIsSplitScreen } =
useDispatch( AGENTS_MANAGER_STORE );
const {
setIsOpen,
setIsDocked,
setIsMinimized,
setIsSplitScreen,
setFloatingPosition,
setFreeDragPosition,
setFloatingSize,
} = useDispatch( AGENTS_MANAGER_STORE );
const {
isOpen: isPersistedOpen,
isDocked: isPersistedDocked,
isMinimized,
isSplitScreen,
floatingPosition,
} = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
Expand Down Expand Up @@ -141,8 +150,27 @@ export default function AgentDock( {
onDock: () => {
recordBigSkyTracksEvent( 'ai_chat_docked' );
},
onUndock: () => {
onUndock: ( isResponsiveUndock ) => {
recordBigSkyTracksEvent( 'ai_chat_undocked' );

// The responsive undock opens right at the default size; persist that
// as the new floating state. Manual pop-outs keep the persisted values.
if ( ! isResponsiveUndock ) {
return;
}

if ( floatingPosition !== 'right' ) {
setFloatingPosition( 'right' );
}

setFreeDragPosition( null );
setFloatingSize( null );

try {
localStorage.setItem( AGENTTIC_CHAT_POSITION_STORAGE_KEY, 'right' );
} catch {
// `localStorage` unavailable.
}
},
isSplitScreen,
} );
Expand Down
21 changes: 5 additions & 16 deletions packages/agents-manager/src/components/agent-history/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { AgentUI } from '@automattic/agenttic-ui';
import { AgentsManagerSelect } from '@automattic/data-stores';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { useAgentsManagerContext } from '../../contexts';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import { AGENTS_MANAGER_STORE } from '../../stores';
import { LocalConversationListItem } from '../../types';
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
import ConversationHistoryView from '../conversation-history-view';
Expand Down Expand Up @@ -37,13 +35,7 @@ export default function AgentHistory( {
onSelectConversation,
}: Props ) {
const { resumeActiveChat } = useAgentsManagerContext();

const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const floatingPanelProps = useFloatingPanelProps();

// Without the AI chat entry button, use `collapsed` (a FAB) instead of `minimized`.
const closedChatState = useHasAiChatEntryButton() ? 'minimized' : 'collapsed';
Expand All @@ -53,12 +45,9 @@ export default function AgentHistory( {

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
// Remount on dock/undock so the mount-only seed props re-apply.
key={ isDocked ? 'embedded' : 'floating' }
{ ...floatingPanelProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ [] }
isProcessing={ false }
Expand Down
20 changes: 5 additions & 15 deletions packages/agents-manager/src/components/support-guide/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { AgentUI } from '@automattic/agenttic-ui';
import { AgentsManagerSelect } from '@automattic/data-stores';
import { HelpCenterArticle } from '@automattic/support-articles';
import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAgentsManagerContext } from '../../contexts';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import { AGENTS_MANAGER_STORE } from '../../stores';
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
import './style.scss';

Expand Down Expand Up @@ -38,12 +36,7 @@ export default function SupportGuide( {
const { site, sectionName, isEligibleForChat } = useAgentsManagerContext();
const navigate = useNavigate();
const { state } = useLocation();
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const floatingPanelProps = useFloatingPanelProps();

// Without the AI chat entry button, use `collapsed` (a FAB) instead of `minimized`.
const closedChatState = useHasAiChatEntryButton() ? 'minimized' : 'collapsed';
Expand All @@ -63,12 +56,9 @@ export default function SupportGuide( {

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
// Remount on dock/undock so the mount-only seed props re-apply.
key={ isDocked ? 'embedded' : 'floating' }
{ ...floatingPanelProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ [] }
isProcessing={ false }
Expand Down
20 changes: 5 additions & 15 deletions packages/agents-manager/src/components/support-guides/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AgentUI } from '@automattic/agenttic-ui';
import { AgentsManagerSelect } from '@automattic/data-stores';
import {
Button,
SearchControl,
Expand All @@ -9,13 +8,12 @@ import {
Spinner,
} from '@wordpress/components';
import { useDebouncedInput } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { Link, useLocation } from 'react-router-dom';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import useHelpSearchQuery from '../../hooks/use-help-search-query';
import { AGENTS_MANAGER_STORE } from '../../stores';
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
import './style.scss';

Expand Down Expand Up @@ -120,25 +118,17 @@ export default function SupportGuides( {
const [ searchInput, setSearchInput, debouncedSearchInput ] = useDebouncedInput(
state?.searchQuery ?? ''
);
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const floatingPanelProps = useFloatingPanelProps();

// Without the AI chat entry button, use `collapsed` (a FAB) instead of `minimized`.
const closedChatState = useHasAiChatEntryButton() ? 'minimized' : 'collapsed';
const title = __( 'Support Guides', __i18n_text_domain__ );

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
// Remount on dock/undock so the mount-only seed props re-apply.
key={ isDocked ? 'embedded' : 'floating' }
{ ...floatingPanelProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ [] }
isProcessing={ false }
Expand Down
7 changes: 7 additions & 0 deletions packages/agents-manager/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ export const ORCHESTRATOR_AGENT_ID = 'wp-orchestrator';
export const UNIFIED_CHAT_AGENT_ID = 'wpcom-workflow-unified_chat';

export const LOCAL_TOOL_RUNNING_MESSAGE = 'local_tool_running';

// `agenttic-ui` reads this key ahead of `initialChatPosition` when seeding the
// panel side. Keep in sync with `STORAGE_KEY` in its `chatStorage.ts`.
export const AGENTTIC_CHAT_POSITION_STORAGE_KEY = 'agenttic-chat-position';

// Free-drag seed `agenttic-ui` clamps into the viewport — lands the panel at the right corner.
export const FLOATING_RIGHT_CORNER_SEED = Object.freeze( { x: Number.MAX_SAFE_INTEGER, y: 0 } );
Loading
Loading