Clean up MCP Monaco editor (#21643)

Rebuild the MCP setup block on the shared Monaco editor with theme-driven colors, padding, and auto-height; add the settings description line-height token; revert the clipboard util to the shared navigator.clipboard implementation; and consolidate the code editor's auto-height to a single disposed, reactive effect.
This commit is contained in:
Thomas des Francs
2026-06-16 13:57:44 +02:00
committed by GitHub
parent ce92a1f1e1
commit a67fdf2dac
14 changed files with 393 additions and 149 deletions
@@ -79,6 +79,7 @@ const StyledIconChevronRightContainer = styled.span`
`;
const StyledDescription = styled.div`
line-height: ${themeCssVariables.text.lineHeight.lg};
padding-bottom: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[7]};
`;
@@ -48,6 +48,7 @@ const StyledLabel = styled.span`
const StyledDescription = styled.span`
color: ${themeCssVariables.font.color.light};
font-weight: ${themeCssVariables.font.weight.regular};
line-height: ${themeCssVariables.text.lineHeight.lg};
padding-left: ${themeCssVariables.spacing[1]};
`;
@@ -70,6 +70,7 @@ const StyledLabel = styled.span`
const StyledDescription = styled.span`
color: ${themeCssVariables.font.color.light};
font-size: ${themeCssVariables.font.size.sm};
line-height: ${themeCssVariables.text.lineHeight.lg};
`;
const StyledError = styled.span`
@@ -41,7 +41,7 @@ export const StyledSettingsCardTextContainer = styled.div`
export const StyledSettingsCardDescription = styled.div`
color: ${themeCssVariables.font.color.secondary};
font-size: ${themeCssVariables.font.size.sm};
line-height: 1.5;
line-height: ${themeCssVariables.text.lineHeight.lg};
overflow: hidden;
a {
@@ -40,6 +40,7 @@ const StyledTitle = styled.div`
const StyledDescription = styled.div`
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
line-height: ${themeCssVariables.text.lineHeight.lg};
`;
type SettingsRadioCardProps = {
@@ -0,0 +1,12 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_19_13)">
<path d="M18 84.8528L85.8822 16.9706C95.2548 7.59798 110.451 7.59798 119.823 16.9706C129.196 26.3431 129.196 41.5391 119.823 50.9117L68.5581 102.177" stroke="currentColor" stroke-width="12" stroke-linecap="round" />
<path d="M69.2652 101.47L119.823 50.9117C129.196 41.5391 144.392 41.5391 153.765 50.9117L154.118 51.2652C163.491 60.6378 163.491 75.8338 154.118 85.2063L92.7248 146.6C89.6006 149.724 89.6006 154.789 92.7248 157.913L105.331 170.52" stroke="currentColor" stroke-width="12" stroke-linecap="round" />
<path d="M102.853 33.9411L52.6482 84.1457C43.2756 93.5183 43.2756 108.714 52.6482 118.087C62.0208 127.459 77.2167 127.459 86.5893 118.087L136.794 67.8822" stroke="currentColor" stroke-width="12" stroke-linecap="round" />
</g>
<defs>
<clipPath id="clip0_19_13">
<rect width="180" height="180" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1016 B

@@ -1,42 +1,24 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { type ReactNode } from 'react';
import { H2Title, IconCopy } from 'twenty-ui-deprecated/display';
import { LightIconButton } from 'twenty-ui-deprecated/input';
import { Card, CardContent, Section } from 'twenty-ui-deprecated/layout';
import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton';
import ModelContextProtocolLogo from '@/settings/playground/assets/model-context-protocol-logo.svg?react';
import { H2Title } from 'twenty-ui-deprecated/display';
import { CodeEditor, CoreEditorHeader } from 'twenty-ui-deprecated/input';
import { Section } from 'twenty-ui-deprecated/layout';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
const StyledMcpConfigContainer = styled.div`
position: relative;
const StyledMcpEditorHeaderTitle = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledCopyButtonContainer = styled.div`
position: absolute;
right: 0;
top: 0;
`;
const StyledPre = styled.pre`
color: ${themeCssVariables.font.color.secondary};
font-family: monospace;
line-height: 1.5;
margin: 0;
overflow-x: auto;
white-space: pre;
`;
const StyledJsonKey = styled.span`
color: ${themeCssVariables.color.blue};
`;
const StyledJsonString = styled.span`
color: ${themeCssVariables.color.green};
`;
const StyledJsonPlaceholder = styled.span`
color: ${themeCssVariables.color.orange};
const StyledMcpIcon = styled(ModelContextProtocolLogo)`
color: inherit;
flex-shrink: 0;
height: calc(${themeCssVariables.icon.size.md} * 1px);
width: calc(${themeCssVariables.icon.size.md} * 1px);
`;
const buildMcpConfig = (serverUrl: string) =>
@@ -51,91 +33,43 @@ const buildMcpConfig = (serverUrl: string) =>
}
}`;
const renderJsonString = (token: string, key: string) => {
const apiKeyPlaceholder = '<YOUR_API_KEY>';
if (!token.includes(apiKeyPlaceholder)) {
return <StyledJsonString key={key}>{token}</StyledJsonString>;
}
const [beforePlaceholder, afterPlaceholder] = token.split(apiKeyPlaceholder);
return (
<span key={key}>
<StyledJsonString>{beforePlaceholder}</StyledJsonString>
<StyledJsonPlaceholder>{apiKeyPlaceholder}</StyledJsonPlaceholder>
<StyledJsonString>{afterPlaceholder}</StyledJsonString>
</span>
);
};
const getHighlightedMcpConfig = (mcpConfig: string) => {
const jsonStringTokenRegex = /("(?:[^"\\]|\\.)*")(\s*:)?/g;
const tokens: ReactNode[] = [];
let previousMatchEndIndex = 0;
let tokenIndex = 0;
let match: RegExpExecArray | null;
while ((match = jsonStringTokenRegex.exec(mcpConfig)) !== null) {
const [fullMatch, quotedString, trailingColon] = match;
const matchIndex = match.index;
if (matchIndex > previousMatchEndIndex) {
tokens.push(mcpConfig.slice(previousMatchEndIndex, matchIndex));
}
if (trailingColon) {
tokens.push(
<StyledJsonKey key={`json-key-${tokenIndex}`}>
{quotedString}
</StyledJsonKey>,
);
tokens.push(trailingColon);
} else {
tokens.push(renderJsonString(quotedString, `json-string-${tokenIndex}`));
}
previousMatchEndIndex = matchIndex + fullMatch.length;
tokenIndex += 1;
}
if (previousMatchEndIndex < mcpConfig.length) {
tokens.push(mcpConfig.slice(previousMatchEndIndex));
}
return tokens;
};
export const SettingsMcpSetup = () => {
const { t } = useLingui();
const { copyToClipboard } = useCopyToClipboard();
const mcpConfig = buildMcpConfig(REACT_APP_SERVER_BASE_URL);
return (
<Section>
<H2Title
title={t`Connect your AI assistant`}
description={t`Add Twenty as a Model Context Protocol (MCP) server. Paste this config into Claude Desktop, Cursor, Cline, Continue, Zed, or any other MCP-aware client.`}
description={t`Paste this configuration into MCP-aware clients like Claude, Codex, or Cursor.`}
/>
<CoreEditorHeader
leftNodes={[
<StyledMcpEditorHeaderTitle>
<StyledMcpIcon aria-hidden focusable={false} />
<span>{t`MCP client configuration`}</span>
</StyledMcpEditorHeaderTitle>,
]}
rightNodes={[<LightCopyIconButton copyText={mcpConfig} />]}
/>
<CodeEditor
value={mcpConfig}
language="json"
variant="with-header"
contentPadding="comfortable"
autoHeight
options={{
readOnly: true,
domReadOnly: true,
lineNumbers: 'off',
lineNumbersMinChars: 0,
folding: false,
glyphMargin: false,
scrollBeyondLastLine: false,
renderLineHighlight: 'none',
wordWrap: 'on',
}}
/>
<Card rounded>
<CardContent>
<StyledMcpConfigContainer>
<StyledPre>{getHighlightedMcpConfig(mcpConfig)}</StyledPre>
<StyledCopyButtonContainer>
<LightIconButton
Icon={IconCopy}
size="small"
accent="tertiary"
aria-label={t`Copy MCP config`}
title={t`Copy MCP config`}
onClick={() =>
copyToClipboard(mcpConfig, t`MCP config copied to clipboard`)
}
/>
</StyledCopyButtonContainer>
</StyledMcpConfigContainer>
</CardContent>
</Card>
</Section>
);
};
@@ -358,7 +358,6 @@ export const WorkflowEditActionCode = ({
readOnly: actionOptions.readonly,
domReadOnly: actionOptions.readonly,
scrollBeyondLastLine: false,
padding: { top: 4, bottom: 4 },
}}
/>
</StyledFullScreenCodeEditorContainer>
@@ -397,7 +396,6 @@ export const WorkflowEditActionCode = ({
readOnly: actionOptions.readonly,
domReadOnly: actionOptions.readonly,
scrollBeyondLastLine: false,
padding: { top: 4, bottom: 4 },
lineNumbersMinChars: 2,
fixedOverflowWidgets: true,
}}