Skip to content

Commit 1cb2ec4

Browse files
eoigalclaude
andauthored
Jetpack AI: Share the group card and toolbar between the MCP Read and Write views (#53862)
* AI: Share the group card and toolbar between the MCP Read and Write views Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * AI: Restore the MCP group title weight and tool row spacing Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * AI: Tighten the MCP tool row description gap Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * AI: Match the MCP card padding to the previous styles Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * AI: Match the MCP card gap and group title size to the previous styles Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 4dc2c0d commit 1cb2ec4

9 files changed

Lines changed: 370 additions & 281 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Button that copies a link to one tool on an MCP view.
3+
*/
4+
5+
import { Button } from '@wordpress/components';
6+
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
7+
import { __ } from '@wordpress/i18n';
8+
import { check, link } from '@wordpress/icons';
9+
10+
const COPIED_RESET_MS = 2000;
11+
12+
/**
13+
* Build the shareable URL. The hash is the router, so the anchor goes in the path.
14+
*
15+
* @param {string} view - View path, e.g. `mcp/read`.
16+
* @param {string} anchor - Tool anchor.
17+
* @return {string} Absolute URL.
18+
*/
19+
function buildAnchorUrl( view, anchor ) {
20+
const { origin, pathname, search } = window.location;
21+
return `${ origin }${ pathname }${ search }#/${ view }/${ anchor }`;
22+
}
23+
24+
/**
25+
* Copy-link button with a transient "Copied" state.
26+
*
27+
* @param {object} props - Component props.
28+
* @param {string} props.view - View path, e.g. `mcp/read`.
29+
* @param {string} props.anchor - Tool anchor to link to.
30+
* @return {object} Component markup.
31+
*/
32+
export default function AnchorCopyButton( { view, anchor } ) {
33+
const [ copied, setCopied ] = useState( false );
34+
const timeout = useRef();
35+
36+
useEffect( () => () => clearTimeout( timeout.current ), [] );
37+
38+
const handleClick = useCallback( () => {
39+
window.navigator.clipboard
40+
?.writeText( buildAnchorUrl( view, anchor ) )
41+
.then( () => {
42+
setCopied( true );
43+
clearTimeout( timeout.current );
44+
timeout.current = setTimeout( () => setCopied( false ), COPIED_RESET_MS );
45+
} )
46+
.catch( () => {} );
47+
}, [ view, anchor ] );
48+
49+
return (
50+
<Button
51+
size="small"
52+
icon={ copied ? check : link }
53+
label={ copied ? __( 'Copied', 'jetpack' ) : __( 'Copy link to this tool', 'jetpack' ) }
54+
showTooltip
55+
onClick={ handleClick }
56+
/>
57+
);
58+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* Collapsible card shared by the MCP Read and Write views.
3+
*/
4+
5+
import { Button } from '@wordpress/components';
6+
import { Fragment, useCallback, useState } from '@wordpress/element';
7+
import { __ } from '@wordpress/i18n';
8+
import { chevronDown, chevronUp } from '@wordpress/icons';
9+
import { Card, Stack, SwitchControl, Text } from '@wordpress/ui';
10+
import { groupToolsBySubCategory } from './groups';
11+
12+
const LABEL_SHOW_OPERATIONS = __( 'Show operations', 'jetpack' );
13+
const LABEL_HIDE_OPERATIONS = __( 'Hide operations', 'jetpack' );
14+
15+
/**
16+
* A single tool toggle row.
17+
*
18+
* @param {object} props - Component props.
19+
* @param {string} props.toolId - Tool identifier.
20+
* @param {object} props.tool - Tool descriptor from the API.
21+
* @param {Set} props.savingToolIds - Set of toolIds currently being saved.
22+
* @param {Function} props.onToggle - Called with (toolId, enabled).
23+
* @return {object} Component markup.
24+
*/
25+
function ToolToggle( { toolId, tool, savingToolIds, onToggle } ) {
26+
const handleChange = useCallback( checked => onToggle( toolId, checked ), [ toolId, onToggle ] );
27+
return (
28+
<SwitchControl
29+
className="jetpack-ai-mcp__tool-switch"
30+
checked={ tool.enabled }
31+
disabled={ savingToolIds.has( toolId ) }
32+
label={ tool.title }
33+
description={ tool.description }
34+
onCheckedChange={ handleChange }
35+
/>
36+
);
37+
}
38+
39+
/**
40+
* Tool toggles split into sub-category sections.
41+
*
42+
* @param {object} props - Component props.
43+
* @param {Array} props.tools - Tool entries, each as [toolId, ability].
44+
* @param {Set} props.savingToolIds - Set of toolIds currently being saved.
45+
* @param {Function} props.onToolChange - Called with (toolId, enabled).
46+
* @return {object} Component markup.
47+
*/
48+
export function McpToolSections( { tools, savingToolIds, onToolChange } ) {
49+
return groupToolsBySubCategory( tools ).map( ( { subCategory, tools: subTools }, subIndex ) => (
50+
<Fragment key={ subCategory ?? '__ungrouped__' }>
51+
{ subIndex > 0 && <div className="jetpack-ai-mcp__sub-divider" /> }
52+
<div
53+
className={
54+
subIndex === 0 ? 'jetpack-ai-mcp__group-body' : 'jetpack-ai-mcp__sub-group-body'
55+
}
56+
>
57+
<Stack direction="column" gap="md">
58+
{ subTools.map( ( [ toolId, tool ] ) => (
59+
<ToolToggle
60+
key={ toolId }
61+
toolId={ toolId }
62+
tool={ tool }
63+
savingToolIds={ savingToolIds }
64+
onToggle={ onToolChange }
65+
/>
66+
) ) }
67+
</Stack>
68+
</div>
69+
</Fragment>
70+
) );
71+
}
72+
73+
/**
74+
* Collapsible card with a group-wide "Enable all" toggle.
75+
*
76+
* @param {object} props - Component props.
77+
* @param {string} [props.id] - DOM id for the card.
78+
* @param {string} props.label - Group title.
79+
* @param {string} [props.description] - Muted text under the title.
80+
* @param {boolean} props.allEnabled - Whether every tool in the group is on.
81+
* @param {boolean} props.isSaving - Disables the toggle while a save is in flight.
82+
* @param {boolean} [props.forceOpen] - Keep the card open regardless of the chevron.
83+
* @param {*} [props.group] - Opaque value handed back to onEnableAll.
84+
* @param {Function} props.onEnableAll - Called with (enabled, group).
85+
* @param {object} props.children - Card body, rendered while open.
86+
* @return {object} Component markup.
87+
*/
88+
export default function McpGroupCard( {
89+
id,
90+
label,
91+
description,
92+
allEnabled,
93+
isSaving,
94+
forceOpen = false,
95+
group,
96+
onEnableAll,
97+
children,
98+
} ) {
99+
const [ isToggledOpen, setIsToggledOpen ] = useState( false );
100+
const isOpen = forceOpen || isToggledOpen;
101+
const handleEnableAll = useCallback(
102+
enabled => onEnableAll( enabled, group ),
103+
[ onEnableAll, group ]
104+
);
105+
const handleChevron = useCallback( () => setIsToggledOpen( open => ! open ), [] );
106+
107+
return (
108+
<Card.Root id={ id }>
109+
<Card.Content className="jetpack-ai-mcp__card-content">
110+
<div className="jetpack-ai-mcp__group-header">
111+
<Stack direction="column" gap="xs" className="jetpack-ai-mcp__group-info">
112+
<Text className="jetpack-ai-mcp__group-title">{ label }</Text>
113+
{ description && (
114+
<Text variant="body-sm" className="jetpack-ai-mcp__muted">
115+
{ description }
116+
</Text>
117+
) }
118+
</Stack>
119+
<div className="jetpack-ai-mcp__group-toggle">
120+
<SwitchControl
121+
checked={ allEnabled }
122+
disabled={ isSaving }
123+
label={ __( 'Enable all', 'jetpack' ) }
124+
onCheckedChange={ handleEnableAll }
125+
/>
126+
</div>
127+
<Button
128+
className="jetpack-ai-mcp__group-chevron"
129+
icon={ isOpen ? chevronUp : chevronDown }
130+
label={ isOpen ? LABEL_HIDE_OPERATIONS : LABEL_SHOW_OPERATIONS }
131+
aria-expanded={ isOpen }
132+
disabled={ forceOpen }
133+
onClick={ handleChevron }
134+
/>
135+
</div>
136+
{ isOpen && children }
137+
</Card.Content>
138+
</Card.Root>
139+
);
140+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Page-level toolbar shared by the MCP Read and Write views.
3+
*/
4+
5+
import { __ } from '@wordpress/i18n';
6+
import { Card, Stack, SwitchControl, Text } from '@wordpress/ui';
7+
8+
/**
9+
* Intro text and a page-wide "Enable all" toggle.
10+
*
11+
* @param {object} props - Component props.
12+
* @param {boolean} props.allEnabled - Whether every tool on the page is on.
13+
* @param {boolean} props.isDisabled - Disables the toggle.
14+
* @param {Function} props.onEnableAll - Called with the new enabled state.
15+
* @param {object} [props.children] - Extra controls below the toggle, e.g. search.
16+
* @return {object} Component markup.
17+
*/
18+
export default function McpToolbar( { allEnabled, isDisabled, onEnableAll, children } ) {
19+
return (
20+
<Card.Root>
21+
<Card.Content className="jetpack-ai-mcp__card-content">
22+
<Stack direction="column" gap="md">
23+
<Text render={ <p /> } className="jetpack-ai-mcp__muted">
24+
{ __( 'Turn individual tools on or off. Changes save automatically.', 'jetpack' ) }
25+
</Text>
26+
<SwitchControl
27+
checked={ allEnabled }
28+
disabled={ isDisabled }
29+
label={ __( 'Enable all', 'jetpack' ) }
30+
onCheckedChange={ onEnableAll }
31+
/>
32+
{ children }
33+
</Stack>
34+
</Card.Content>
35+
</Card.Root>
36+
);
37+
}

0 commit comments

Comments
 (0)