@@ -49,6 +49,8 @@ import type {
4949} from '@/app/home/mcp/components/mcp-form/MCPForm' ;
5050import SkillZipPreviewPanel from '@/app/home/skills/components/SkillZipPreviewPanel' ;
5151import PluginLocalPreviewPanel from '@/app/home/plugins/components/PluginLocalPreviewPanel' ;
52+ import { useWorkspaceQuotaStatus } from '@/app/home/components/workspace-quota/useWorkspaceQuotaStatus' ;
53+ import { WorkspaceQuotaTooltip } from '@/app/home/components/workspace-quota/WorkspaceQuotaTooltip' ;
5254
5355type PopoverView = 'menu' | 'mcp' | 'github' ;
5456
@@ -154,6 +156,12 @@ function AddExtensionContent() {
154156 const navigate = useNavigate ( ) ;
155157 const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
156158 const { refreshPlugins, refreshMCPServers, refreshSkills } = useSidebarData ( ) ;
159+ const { extensions : extensionQuota , extensionsReached } =
160+ useWorkspaceQuotaStatus ( ) ;
161+ const extensionQuotaTooltip = t ( 'limitation.createDisabledTooltip' , {
162+ resource : t ( 'sidebar.extensions' ) ,
163+ max : extensionQuota . max ,
164+ } ) ;
157165
158166 // Localized label for an extension type, used in the install dialog.
159167 const extensionTypeLabel = ( type : string ) =>
@@ -344,23 +352,28 @@ function AddExtensionContent() {
344352 t ,
345353 ] ) ;
346354
347- const handleInstallPlugin = useCallback ( async ( plugin : PluginV4 ) => {
348- setInstallInfo ( {
349- plugin_author : plugin . author ,
350- plugin_name : plugin . name ,
351- plugin_version : plugin . latest_version ,
352- plugin_label : extractI18nObject ( plugin . label ) || plugin . name ,
353- plugin_description : extractI18nObject ( plugin . description ) || '' ,
354- plugin_icon : plugin . icon || '' ,
355- } ) ;
356- setInstallExtensionType ( plugin . type || 'plugin' ) ;
357- setPluginInstallStatus ( PluginInstallStatus . ASK_CONFIRM ) ;
358- setInstallError ( null ) ;
359- setInstallIconFailed ( false ) ;
360- setModalOpen ( true ) ;
361- } , [ ] ) ;
355+ const handleInstallPlugin = useCallback (
356+ async ( plugin : PluginV4 ) => {
357+ if ( extensionsReached ) return ;
358+ setInstallInfo ( {
359+ plugin_author : plugin . author ,
360+ plugin_name : plugin . name ,
361+ plugin_version : plugin . latest_version ,
362+ plugin_label : extractI18nObject ( plugin . label ) || plugin . name ,
363+ plugin_description : extractI18nObject ( plugin . description ) || '' ,
364+ plugin_icon : plugin . icon || '' ,
365+ } ) ;
366+ setInstallExtensionType ( plugin . type || 'plugin' ) ;
367+ setPluginInstallStatus ( PluginInstallStatus . ASK_CONFIRM ) ;
368+ setInstallError ( null ) ;
369+ setInstallIconFailed ( false ) ;
370+ setModalOpen ( true ) ;
371+ } ,
372+ [ extensionsReached ] ,
373+ ) ;
362374
363375 function handleModalConfirm ( ) {
376+ if ( extensionsReached ) return ;
364377 setPluginInstallStatus ( PluginInstallStatus . INSTALLING ) ;
365378 const pluginDisplayName = `${ installInfo . plugin_author } /${ installInfo . plugin_name } ` ;
366379 httpClient
@@ -402,6 +415,7 @@ function AddExtensionContent() {
402415
403416 const uploadFile = useCallback (
404417 async ( file : File ) => {
418+ if ( extensionsReached ) return ;
405419 if ( ! validateFileType ( file ) ) {
406420 toast . error ( t ( 'addExtension.unsupportedFileType' ) ) ;
407421 return ;
@@ -421,14 +435,15 @@ function AddExtensionContent() {
421435 setSkillUploadPreviewOpen ( true ) ;
422436 }
423437 } ,
424- [ t , setSelectedTaskId ] ,
438+ [ extensionsReached , t , setSelectedTaskId ] ,
425439 ) ;
426440
427441 const handleFileSelect = useCallback ( ( ) => {
442+ if ( extensionsReached ) return ;
428443 if ( fileInputRef . current ) {
429444 fileInputRef . current . click ( ) ;
430445 }
431- } , [ ] ) ;
446+ } , [ extensionsReached ] ) ;
432447
433448 const handleFileChange = useCallback (
434449 ( event : React . ChangeEvent < HTMLInputElement > ) => {
@@ -455,12 +470,13 @@ function AddExtensionContent() {
455470 ( event : React . DragEvent ) => {
456471 event . preventDefault ( ) ;
457472 setIsDragOver ( false ) ;
473+ if ( extensionsReached ) return ;
458474 const files = Array . from ( event . dataTransfer . files ) ;
459475 if ( files . length > 0 ) {
460476 uploadFile ( files [ 0 ] ) ;
461477 }
462478 } ,
463- [ uploadFile ] ,
479+ [ extensionsReached , uploadFile ] ,
464480 ) ;
465481
466482 function handleMCPCreated ( _serverName : string ) {
@@ -490,7 +506,8 @@ function AddExtensionContent() {
490506 return false ;
491507 }
492508 } catch {
493- // If we can't check, let backend handle it
509+ toast . error ( t ( 'limitation.quotaCheckFailed' ) ) ;
510+ return false ;
494511 }
495512 return true ;
496513 }
@@ -630,9 +647,11 @@ function AddExtensionContent() {
630647
631648 async function handleGithubConfirm ( ) {
632649 if ( ! selectedAsset || ! selectedRelease ) return ;
633- if ( ! ( await checkExtensionsLimit ( ) ) ) return ;
634-
635650 setGithubInstallStatus ( GithubInstallStatus . INSTALLING ) ;
651+ if ( ! ( await checkExtensionsLimit ( ) ) ) {
652+ setGithubInstallStatus ( GithubInstallStatus . ASK_CONFIRM ) ;
653+ return ;
654+ }
636655 const pluginDisplayName = `${ githubOwner } /${ githubRepo } ` ;
637656 httpClient
638657 . installPluginFromGithub (
@@ -664,9 +683,11 @@ function AddExtensionContent() {
664683
665684 async function handleGithubSkillConfirm ( ) {
666685 if ( ! githubSkillInfo ) return ;
667- if ( ! ( await checkExtensionsLimit ( ) ) ) return ;
668-
669686 setGithubInstallStatus ( GithubInstallStatus . SKILL_INSTALLING ) ;
687+ if ( ! ( await checkExtensionsLimit ( ) ) ) {
688+ setGithubInstallStatus ( GithubInstallStatus . SKILL_PREVIEW ) ;
689+ return ;
690+ }
670691 try {
671692 await httpClient . installSkillFromGithub (
672693 githubURL . trim ( ) ,
@@ -726,17 +747,24 @@ function AddExtensionContent() {
726747 setPopoverOpen ( open ) ;
727748 } }
728749 >
729- < PopoverTrigger asChild >
730- < Button
731- variant = "default"
732- className = "px-3 sm:px-4 py-2 cursor-pointer flex-shrink-0"
733- >
734- < PlusIcon className = "w-4 h-4" />
735- < span className = "whitespace-nowrap" >
736- { t ( 'addExtension.manualAdd' ) }
737- </ span >
738- </ Button >
739- </ PopoverTrigger >
750+ < WorkspaceQuotaTooltip
751+ quota = { extensionQuota }
752+ resource = { t ( 'sidebar.extensions' ) }
753+ >
754+ < PopoverTrigger asChild >
755+ < Button
756+ variant = "default"
757+ disabled = { extensionsReached }
758+ aria-disabled = { extensionsReached }
759+ className = "px-3 sm:px-4 py-2 cursor-pointer flex-shrink-0 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-100"
760+ >
761+ < PlusIcon className = "w-4 h-4" />
762+ < span className = "whitespace-nowrap" >
763+ { t ( 'addExtension.manualAdd' ) }
764+ </ span >
765+ </ Button >
766+ </ PopoverTrigger >
767+ </ WorkspaceQuotaTooltip >
740768 < PopoverContent
741769 forceMount
742770 className = { `${ getPopoverWidth ( ) } max-h-[min(720px,80vh)] overflow-hidden p-0` }
@@ -745,9 +773,19 @@ function AddExtensionContent() {
745773 { /* ===== Menu View ===== */ }
746774 { popoverView === 'menu' && (
747775 < div className = "space-y-4 p-4" >
776+ { extensionsReached && (
777+ < div className = "rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-800 dark:text-amber-200" >
778+ { extensionQuotaTooltip }
779+ </ div >
780+ ) }
748781 { /* File upload area */ }
749782 < div
750- className = { `border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
783+ aria-disabled = { extensionsReached }
784+ className = { `border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
785+ extensionsReached
786+ ? 'cursor-not-allowed opacity-50'
787+ : 'cursor-pointer'
788+ } ${
751789 isDragOver
752790 ? 'border-primary bg-primary/5'
753791 : 'border-muted-foreground/25 hover:border-primary/50'
@@ -777,7 +815,8 @@ function AddExtensionContent() {
777815 < div className = "space-y-2" >
778816 < button
779817 type = "button"
780- className = "group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
818+ disabled = { extensionsReached }
819+ className = "group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
781820 onClick = { ( ) => setPopoverView ( 'mcp' ) }
782821 >
783822 < span className = "flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground" >
@@ -796,7 +835,8 @@ function AddExtensionContent() {
796835
797836 < button
798837 type = "button"
799- className = "group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
838+ disabled = { extensionsReached }
839+ className = "group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
800840 onClick = { ( ) => setPopoverView ( 'github' ) }
801841 >
802842 < span className = "flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground" >
@@ -815,7 +855,8 @@ function AddExtensionContent() {
815855
816856 < button
817857 type = "button"
818- className = "group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
858+ disabled = { extensionsReached }
859+ className = "group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
819860 onClick = { async ( ) => {
820861 if ( ! ( await checkExtensionsLimit ( ) ) ) return ;
821862 setPopoverOpen ( false ) ;
@@ -882,6 +923,7 @@ function AddExtensionContent() {
882923 type = "submit"
883924 form = "mcp-form"
884925 size = "sm"
926+ disabled = { extensionsReached }
885927 onClick = { async ( e ) => {
886928 if ( ! ( await checkExtensionsLimit ( ) ) ) {
887929 e . preventDefault ( ) ;
@@ -946,6 +988,7 @@ function AddExtensionContent() {
946988 className = "w-full"
947989 onClick = { handleGithubAddressSubmit }
948990 disabled = {
991+ extensionsReached ||
949992 ! githubURL . trim ( ) ||
950993 fetchingReleases ||
951994 fetchingSkillPreview
@@ -1102,7 +1145,11 @@ function AddExtensionContent() {
11021145 </ div >
11031146 </ div >
11041147 ) }
1105- < Button className = "w-full" onClick = { handleGithubConfirm } >
1148+ < Button
1149+ className = "w-full"
1150+ onClick = { handleGithubConfirm }
1151+ disabled = { extensionsReached }
1152+ >
11061153 { t ( 'common.confirm' ) }
11071154 </ Button >
11081155 </ div >
@@ -1184,6 +1231,7 @@ function AddExtensionContent() {
11841231 < Button
11851232 className = "w-full"
11861233 onClick = { handleGithubSkillConfirm }
1234+ disabled = { extensionsReached }
11871235 >
11881236 { t ( 'common.confirm' ) }
11891237 </ Button >
@@ -1240,6 +1288,8 @@ function AddExtensionContent() {
12401288 < MarketPage
12411289 installPlugin = { handleInstallPlugin }
12421290 headerActions = { extensionActions }
1291+ installDisabled = { extensionsReached }
1292+ installDisabledTooltip = { extensionQuotaTooltip }
12431293 />
12441294 </ div >
12451295 </ div >
@@ -1325,9 +1375,17 @@ function AddExtensionContent() {
13251375 < Button variant = "outline" onClick = { ( ) => setModalOpen ( false ) } >
13261376 { t ( 'common.cancel' ) }
13271377 </ Button >
1328- < Button onClick = { handleModalConfirm } >
1329- { t ( 'common.confirm' ) }
1330- </ Button >
1378+ < WorkspaceQuotaTooltip
1379+ quota = { extensionQuota }
1380+ resource = { t ( 'sidebar.extensions' ) }
1381+ >
1382+ < Button
1383+ onClick = { handleModalConfirm }
1384+ disabled = { extensionsReached }
1385+ >
1386+ { t ( 'common.confirm' ) }
1387+ </ Button >
1388+ </ WorkspaceQuotaTooltip >
13311389 </ >
13321390 ) }
13331391 { pluginInstallStatus === PluginInstallStatus . ERROR && (
@@ -1359,6 +1417,8 @@ function AddExtensionContent() {
13591417 { pluginUploadPreviewFile && (
13601418 < PluginLocalPreviewPanel
13611419 file = { pluginUploadPreviewFile }
1420+ quota = { extensionQuota }
1421+ quotaResource = { t ( 'sidebar.extensions' ) }
13621422 onCancel = { ( ) => {
13631423 setPluginUploadPreviewOpen ( false ) ;
13641424 setPluginUploadPreviewFile ( null ) ;
@@ -1392,6 +1452,8 @@ function AddExtensionContent() {
13921452 { skillUploadPreviewFile && (
13931453 < SkillZipPreviewPanel
13941454 file = { skillUploadPreviewFile }
1455+ quota = { extensionQuota }
1456+ quotaResource = { t ( 'sidebar.extensions' ) }
13951457 onCancel = { ( ) => {
13961458 setSkillUploadPreviewOpen ( false ) ;
13971459 setSkillUploadPreviewFile ( null ) ;
0 commit comments