Skip to content

Commit 67cec9c

Browse files
Welly Shenclaude
andcommitted
Agents Manager: open the floating chat right at default size on responsive undock
When the docked sidebar is forced into floating mode by the viewport narrowing below the desktop media query, the chat now opens at the right corner (where the sidebar was) at the default size, and persists that as the new floating state. Manual pop-outs and fullscreen-gate undocks keep restoring the user's persisted position and size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5c2ca56 commit 67cec9c

14 files changed

Lines changed: 317 additions & 90 deletions

File tree

packages/agents-manager/src/components/__tests__/agent-chat.test.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ComponentProps, ReactNode, Ref } from 'react';
1010

1111
const mockSetFloatingPosition = jest.fn();
1212
const mockContainerProps = jest.fn();
13+
const mockContainerMounts = jest.fn();
1314
const mockInputProps = jest.fn();
1415
const mockImageUploaderProps = jest.fn();
1516
const mockHasAiChatEntry = jest.fn();
@@ -36,6 +37,9 @@ jest.mock(
3637
) => void;
3738
} ) {
3839
mockContainerProps( { floatingChatState } );
40+
React.useEffect( () => {
41+
mockContainerMounts();
42+
}, [] );
3943
return (
4044
<div>
4145
{ emptyView }
@@ -212,8 +216,8 @@ jest.mock( '../../hooks/use-has-ai-chat-entry-button', () => ( {
212216

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

215-
function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
216-
return render(
219+
function getAgentChatElement( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
220+
return (
217221
<AgentChat
218222
messages={ [] }
219223
suggestions={ [] }
@@ -234,6 +238,10 @@ function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > =
234238
);
235239
}
236240

241+
function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
242+
return render( getAgentChatElement( props ) );
243+
}
244+
237245
describe( 'AgentChat', () => {
238246
beforeEach( () => {
239247
jest.clearAllMocks();
@@ -522,4 +530,16 @@ describe( 'AgentChat', () => {
522530

523531
expect( mockContainerProps ).toHaveBeenLastCalledWith( { floatingChatState: 'minimized' } );
524532
} );
533+
534+
it( 'remounts the container only when the dock state changes', () => {
535+
const { rerender } = renderAgentChat( { isDocked: false } );
536+
expect( mockContainerMounts ).toHaveBeenCalledTimes( 1 );
537+
538+
// The remount re-applies the mount-only position/size seeds.
539+
rerender( getAgentChatElement( { isDocked: true } ) );
540+
expect( mockContainerMounts ).toHaveBeenCalledTimes( 2 );
541+
542+
rerender( getAgentChatElement( { isDocked: true, isOpen: true } ) );
543+
expect( mockContainerMounts ).toHaveBeenCalledTimes( 2 );
544+
} );
525545
} );

packages/agents-manager/src/components/__tests__/agent-dock.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const mockSetIsOpen = jest.fn();
1111
const mockSetIsDocked = jest.fn();
1212
const mockSetIsMinimized = jest.fn();
1313
const mockSetIsSplitScreen = jest.fn();
14+
const mockSetFloatingPosition = jest.fn();
15+
const mockSetFreeDragPosition = jest.fn();
16+
const mockSetFloatingSize = jest.fn();
1417
const mockUseAgentLayoutManager = jest.fn();
1518
const mockResumeActiveChat = jest.fn();
1619
const mockCloseSidebar = jest.fn();
@@ -21,6 +24,7 @@ let mockAgentsManagerState: {
2124
isDocked?: boolean;
2225
isMinimized?: boolean;
2326
isSplitScreen?: boolean;
27+
floatingPosition?: 'left' | 'right';
2428
} = { isOpen: true, isDocked: false };
2529
let mockHasAdminBar = false;
2630
let mockShouldUseUnifiedAgent = false;
@@ -41,6 +45,9 @@ jest.mock( '@wordpress/data', () => ( {
4145
setIsDocked: mockSetIsDocked,
4246
setIsMinimized: mockSetIsMinimized,
4347
setIsSplitScreen: mockSetIsSplitScreen,
48+
setFloatingPosition: mockSetFloatingPosition,
49+
setFreeDragPosition: mockSetFreeDragPosition,
50+
setFloatingSize: mockSetFloatingSize,
4451
} ),
4552
useSelect: () => mockAgentsManagerState,
4653
} ) );
@@ -453,4 +460,43 @@ describe( 'AgentDock', () => {
453460
expect( mockSetIsSplitScreen ).toHaveBeenCalledWith( nextState );
454461
}
455462
);
463+
464+
it( 'persists the right-side default floating state on the responsive undock', () => {
465+
useWpAdminAgent();
466+
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'left' };
467+
468+
renderAgentDock();
469+
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
470+
act( () => onUndock( true ) );
471+
472+
expect( mockSetFloatingPosition ).toHaveBeenCalledWith( 'right' );
473+
expect( mockSetFreeDragPosition ).toHaveBeenCalledWith( null );
474+
expect( mockSetFloatingSize ).toHaveBeenCalledWith( null );
475+
expect( localStorage.getItem( 'agenttic-chat-position' ) ).toBe( 'right' );
476+
} );
477+
478+
it( 'skips the position save when the persisted side is already right', () => {
479+
useWpAdminAgent();
480+
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'right' };
481+
482+
renderAgentDock();
483+
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
484+
act( () => onUndock( true ) );
485+
486+
expect( mockSetFloatingPosition ).not.toHaveBeenCalled();
487+
expect( mockSetFreeDragPosition ).toHaveBeenCalledWith( null );
488+
} );
489+
490+
it( 'leaves the floating state alone on a manual undock', () => {
491+
useWpAdminAgent();
492+
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'left' };
493+
494+
renderAgentDock();
495+
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
496+
act( () => onUndock( false ) );
497+
498+
expect( mockSetFloatingPosition ).not.toHaveBeenCalled();
499+
expect( mockSetFreeDragPosition ).not.toHaveBeenCalled();
500+
expect( mockSetFloatingSize ).not.toHaveBeenCalled();
501+
} );
456502
} );

packages/agents-manager/src/components/agent-chat/index.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ import {
99
type ChatState,
1010
type UploadedImage,
1111
} from '@automattic/agenttic-ui';
12-
import { useDispatch, useSelect } from '@wordpress/data';
1312
import { useCallback, useMemo, useRef } from '@wordpress/element';
1413
import { __ } from '@wordpress/i18n';
1514
import clsx from 'clsx';
1615
import { formatWritingSuggestionLabels } from '../../hooks/use-empty-view-suggestions';
16+
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
1717
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
18-
import { AGENTS_MANAGER_STORE } from '../../stores';
1918
import { getAgentsManagerInlineData } from '../../utils/get-agents-manager-inline-data';
2019
import { isEditorPage } from '../../utils/is-editor-page';
2120
import { isReaderChatHost } from '../../utils/is-reader-chat-agent';
@@ -32,7 +31,6 @@ import GroupedEmptyView from './grouped-empty-view';
3231
import type { UseImageUploadResult } from '../../hooks/use-image-upload';
3332
import type { ExternalContextCard, ExternalContextCardAction } from '../../utils/external-context';
3433
import type { Message, NoticeConfig } from '@automattic/agenttic-ui/dist/types';
35-
import type { AgentsManagerSelect } from '@automattic/data-stores';
3634
import type { ComponentProps, RefObject } from 'react';
3735

3836
interface Props {
@@ -184,14 +182,9 @@ export default function AgentChat( {
184182
onContextCardAction,
185183
onContextCardDismiss,
186184
}: Props ) {
187-
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
188-
useDispatch( AGENTS_MANAGER_STORE );
189185
const conversationViewRef = useRef< HTMLDivElement >( null );
190186
const imageUploaderRef = useRef< ImageUploaderHandle >( null );
191-
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
192-
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
193-
return store.getAgentsManagerState();
194-
}, [] );
187+
const floatingPanelProps = useFloatingPanelProps();
195188

196189
const mergedComponents = useMemo(
197190
() => ( { a: CustomALink, ...markdownComponents } ),
@@ -289,12 +282,10 @@ export default function AgentChat( {
289282

290283
return (
291284
<AgentUI.Container
292-
initialChatPosition={ floatingPosition }
293-
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
294-
initialFreeDragPosition={ freeDragPosition ?? undefined }
295-
onFreeDragEnd={ setFreeDragPosition }
296-
defaultSize={ floatingSize ?? undefined }
297-
onResizeEnd={ setFloatingSize }
285+
// Remount on dock/undock so the floating panel re-seeds — the seed
286+
// props are read at mount only.
287+
key={ isDocked ? 'embedded' : 'floating' }
288+
{ ...floatingPanelProps }
298289
className={ clsx( 'agenttic', { dark: isDocked } ) }
299290
messages={ messages }
300291
isProcessing={ isProcessing }

packages/agents-manager/src/components/agent-dock/index.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,21 @@ export default function AgentDock( {
8787
window.__agentsManagerActions?.desktopMediaQuery
8888
);
8989
const [ isOrchestratorChatEmpty, setIsOrchestratorChatEmpty ] = useState( true );
90-
const { setIsOpen, setIsDocked, setIsMinimized, setIsSplitScreen } =
91-
useDispatch( AGENTS_MANAGER_STORE );
90+
const {
91+
setIsOpen,
92+
setIsDocked,
93+
setIsMinimized,
94+
setIsSplitScreen,
95+
setFloatingPosition,
96+
setFreeDragPosition,
97+
setFloatingSize,
98+
} = useDispatch( AGENTS_MANAGER_STORE );
9299
const {
93100
isOpen: isPersistedOpen,
94101
isDocked: isPersistedDocked,
95102
isMinimized,
96103
isSplitScreen,
104+
floatingPosition,
97105
} = useSelect( ( select ) => {
98106
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
99107
return store.getAgentsManagerState();
@@ -141,8 +149,30 @@ export default function AgentDock( {
141149
onDock: () => {
142150
recordBigSkyTracksEvent( 'ai_chat_docked' );
143151
},
144-
onUndock: () => {
152+
onUndock: ( isResponsiveUndock ) => {
145153
recordBigSkyTracksEvent( 'ai_chat_undocked' );
154+
155+
// The responsive undock opens the chat at the right corner (where
156+
// the sidebar was) at the default size — persist that as the new
157+
// floating state. Manual pop-outs keep the persisted values.
158+
if ( ! isResponsiveUndock ) {
159+
return;
160+
}
161+
162+
if ( floatingPosition !== 'right' ) {
163+
setFloatingPosition( 'right' );
164+
}
165+
166+
setFreeDragPosition( null );
167+
setFloatingSize( null );
168+
169+
// `agenttic-ui` seeds its side from this key ahead of `initialChatPosition`.
170+
// Keep in sync with `STORAGE_KEY` in `agenttic-ui/src/utils/chatStorage.ts`.
171+
try {
172+
localStorage.setItem( 'agenttic-chat-position', 'right' );
173+
} catch {
174+
// `localStorage` unavailable.
175+
}
146176
},
147177
isSplitScreen,
148178
} );

packages/agents-manager/src/components/agent-history/index.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { AgentUI } from '@automattic/agenttic-ui';
2-
import { AgentsManagerSelect } from '@automattic/data-stores';
3-
import { useDispatch, useSelect } from '@wordpress/data';
42
import { __ } from '@wordpress/i18n';
53
import clsx from 'clsx';
64
import { useAgentsManagerContext } from '../../contexts';
5+
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
76
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
8-
import { AGENTS_MANAGER_STORE } from '../../stores';
97
import { LocalConversationListItem } from '../../types';
108
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
119
import ConversationHistoryView from '../conversation-history-view';
@@ -37,13 +35,7 @@ export default function AgentHistory( {
3735
onSelectConversation,
3836
}: Props ) {
3937
const { resumeActiveChat } = useAgentsManagerContext();
40-
41-
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
42-
useDispatch( AGENTS_MANAGER_STORE );
43-
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
44-
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
45-
return store.getAgentsManagerState();
46-
}, [] );
38+
const floatingPanelProps = useFloatingPanelProps();
4739

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

5446
return (
5547
<AgentUI.Container
56-
initialChatPosition={ floatingPosition }
57-
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
58-
initialFreeDragPosition={ freeDragPosition ?? undefined }
59-
onFreeDragEnd={ setFreeDragPosition }
60-
defaultSize={ floatingSize ?? undefined }
61-
onResizeEnd={ setFloatingSize }
48+
// Remount on dock/undock so the floating panel re-seeds — the seed
49+
// props are read at mount only.
50+
key={ isDocked ? 'embedded' : 'floating' }
51+
{ ...floatingPanelProps }
6252
className={ clsx( 'agenttic', { dark: isDocked } ) }
6353
messages={ [] }
6454
isProcessing={ false }

packages/agents-manager/src/components/support-guide/index.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { AgentUI } from '@automattic/agenttic-ui';
2-
import { AgentsManagerSelect } from '@automattic/data-stores';
32
import { HelpCenterArticle } from '@automattic/support-articles';
43
import { Button } from '@wordpress/components';
5-
import { useDispatch, useSelect } from '@wordpress/data';
64
import { __ } from '@wordpress/i18n';
75
import clsx from 'clsx';
86
import { useLocation, useNavigate } from 'react-router-dom';
97
import { useAgentsManagerContext } from '../../contexts';
8+
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
109
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
11-
import { AGENTS_MANAGER_STORE } from '../../stores';
1210
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
1311
import './style.scss';
1412

@@ -38,12 +36,7 @@ export default function SupportGuide( {
3836
const { site, sectionName, isEligibleForChat } = useAgentsManagerContext();
3937
const navigate = useNavigate();
4038
const { state } = useLocation();
41-
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
42-
useDispatch( AGENTS_MANAGER_STORE );
43-
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
44-
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
45-
return store.getAgentsManagerState();
46-
}, [] );
39+
const floatingPanelProps = useFloatingPanelProps();
4740

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

6457
return (
6558
<AgentUI.Container
66-
initialChatPosition={ floatingPosition }
67-
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
68-
initialFreeDragPosition={ freeDragPosition ?? undefined }
69-
onFreeDragEnd={ setFreeDragPosition }
70-
defaultSize={ floatingSize ?? undefined }
71-
onResizeEnd={ setFloatingSize }
59+
// Remount on dock/undock so the floating panel re-seeds — the seed
60+
// props are read at mount only.
61+
key={ isDocked ? 'embedded' : 'floating' }
62+
{ ...floatingPanelProps }
7263
className={ clsx( 'agenttic', { dark: isDocked } ) }
7364
messages={ [] }
7465
isProcessing={ false }

packages/agents-manager/src/components/support-guides/index.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { AgentUI } from '@automattic/agenttic-ui';
2-
import { AgentsManagerSelect } from '@automattic/data-stores';
32
import {
43
Button,
54
SearchControl,
@@ -9,13 +8,12 @@ import {
98
Spinner,
109
} from '@wordpress/components';
1110
import { useDebouncedInput } from '@wordpress/compose';
12-
import { useDispatch, useSelect } from '@wordpress/data';
1311
import { __ } from '@wordpress/i18n';
1412
import clsx from 'clsx';
1513
import { Link, useLocation } from 'react-router-dom';
14+
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
1615
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
1716
import useHelpSearchQuery from '../../hooks/use-help-search-query';
18-
import { AGENTS_MANAGER_STORE } from '../../stores';
1917
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
2018
import './style.scss';
2119

@@ -120,25 +118,18 @@ export default function SupportGuides( {
120118
const [ searchInput, setSearchInput, debouncedSearchInput ] = useDebouncedInput(
121119
state?.searchQuery ?? ''
122120
);
123-
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
124-
useDispatch( AGENTS_MANAGER_STORE );
125-
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
126-
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
127-
return store.getAgentsManagerState();
128-
}, [] );
121+
const floatingPanelProps = useFloatingPanelProps();
129122

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

134127
return (
135128
<AgentUI.Container
136-
initialChatPosition={ floatingPosition }
137-
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
138-
initialFreeDragPosition={ freeDragPosition ?? undefined }
139-
onFreeDragEnd={ setFreeDragPosition }
140-
defaultSize={ floatingSize ?? undefined }
141-
onResizeEnd={ setFloatingSize }
129+
// Remount on dock/undock so the floating panel re-seeds — the seed
130+
// props are read at mount only.
131+
key={ isDocked ? 'embedded' : 'floating' }
132+
{ ...floatingPanelProps }
142133
className={ clsx( 'agenttic', { dark: isDocked } ) }
143134
messages={ [] }
144135
isProcessing={ false }

packages/agents-manager/src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ export const ORCHESTRATOR_AGENT_ID = 'wp-orchestrator';
55
export const UNIFIED_CHAT_AGENT_ID = 'wpcom-workflow-unified_chat';
66

77
export const LOCAL_TOOL_RUNNING_MESSAGE = 'local_tool_running';
8+
9+
// Free-drag seed far past the right edge — `agenttic-ui` clamps it into the
10+
// inset viewport, landing the floating panel exactly at the right corner.
11+
export const FLOATING_RIGHT_CORNER_SEED = { x: Number.MAX_SAFE_INTEGER, y: 0 };

0 commit comments

Comments
 (0)