mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat(cli): add /features command and lifecycle visualization
Add /features slash command to list Alpha, Beta, and Deprecated features with their status, version info, and descriptions. Add FeaturesList UI component with grouped table display by stage. Update SettingsDialog to show feature stage badges ([ALPHA], [BETA], [DEPRECATED]) next to feature toggle labels.
This commit is contained in:
@@ -31,6 +31,7 @@ import { docsCommand } from '../ui/commands/docsCommand.js';
|
||||
import { directoryCommand } from '../ui/commands/directoryCommand.js';
|
||||
import { editorCommand } from '../ui/commands/editorCommand.js';
|
||||
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
|
||||
import { featuresCommand } from '../ui/commands/featuresCommand.js';
|
||||
import { footerCommand } from '../ui/commands/footerCommand.js';
|
||||
import { helpCommand } from '../ui/commands/helpCommand.js';
|
||||
import { shortcutsCommand } from '../ui/commands/shortcutsCommand.js';
|
||||
@@ -119,6 +120,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
},
|
||||
]
|
||||
: [extensionsCommand(this.config?.getEnableExtensionReloading())]),
|
||||
featuresCommand,
|
||||
helpCommand,
|
||||
footerCommand,
|
||||
shortcutsCommand,
|
||||
|
||||
64
packages/cli/src/ui/commands/featuresCommand.test.ts
Normal file
64
packages/cli/src/ui/commands/featuresCommand.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, type vi } from 'vitest';
|
||||
import { featuresCommand } from './featuresCommand.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { MessageType, type HistoryItemFeaturesList } from '../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
describe('featuresCommand', () => {
|
||||
it('should display an error if the feature gate is unavailable', async () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getFeatureGate: () => undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!featuresCommand.action) throw new Error('Action not defined');
|
||||
await featuresCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Could not retrieve feature gate.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should list alpha, beta, and deprecated features', async () => {
|
||||
const mockFeatures = [
|
||||
{ key: 'alphaFeat', enabled: false, stage: FeatureStage.Alpha },
|
||||
{ key: 'betaFeat', enabled: true, stage: FeatureStage.Beta },
|
||||
{ key: 'deprecatedFeat', enabled: false, stage: FeatureStage.Deprecated },
|
||||
{ key: 'gaFeat', enabled: true, stage: FeatureStage.GA },
|
||||
];
|
||||
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getFeatureGate: () => ({
|
||||
getFeatureInfo: () => mockFeatures,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!featuresCommand.action) throw new Error('Action not defined');
|
||||
await featuresCommand.action(mockContext, '');
|
||||
|
||||
const [message] = (mockContext.ui.addItem as ReturnType<typeof vi.fn>).mock
|
||||
.calls[0];
|
||||
const featuresList = message as HistoryItemFeaturesList;
|
||||
expect(featuresList.type).toBe(MessageType.FEATURES_LIST);
|
||||
expect(featuresList.features).toHaveLength(3);
|
||||
expect(featuresList.features.map((f) => f.key)).toEqual([
|
||||
'alphaFeat',
|
||||
'betaFeat',
|
||||
'deprecatedFeat',
|
||||
]);
|
||||
});
|
||||
});
|
||||
46
packages/cli/src/ui/commands/featuresCommand.ts
Normal file
46
packages/cli/src/ui/commands/featuresCommand.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import { MessageType, type HistoryItemFeaturesList } from '../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
export const featuresCommand: SlashCommand = {
|
||||
name: 'features',
|
||||
altNames: ['feature'],
|
||||
description: 'List alpha, beta, and deprecated Gemini CLI features.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context: CommandContext): Promise<void> => {
|
||||
const featureGate = context.services.config?.getFeatureGate();
|
||||
if (!featureGate) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Could not retrieve feature gate.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const allFeatures = featureGate.getFeatureInfo();
|
||||
const filteredFeatures = allFeatures.filter(
|
||||
(f) =>
|
||||
f.stage === FeatureStage.Alpha ||
|
||||
f.stage === FeatureStage.Beta ||
|
||||
f.stage === FeatureStage.Deprecated,
|
||||
);
|
||||
|
||||
const featuresListItem: HistoryItemFeaturesList = {
|
||||
type: MessageType.FEATURES_LIST,
|
||||
features: filteredFeatures,
|
||||
};
|
||||
|
||||
context.ui.addItem(featuresListItem);
|
||||
},
|
||||
};
|
||||
@@ -29,6 +29,7 @@ import { ExtensionsList } from './views/ExtensionsList.js';
|
||||
import { getMCPServerStatus } from '@google/gemini-cli-core';
|
||||
import { ToolsList } from './views/ToolsList.js';
|
||||
import { SkillsList } from './views/SkillsList.js';
|
||||
import { FeaturesList } from './views/FeaturesList.js';
|
||||
import { AgentsStatus } from './views/AgentsStatus.js';
|
||||
import { McpStatus } from './views/McpStatus.js';
|
||||
import { ChatList } from './views/ChatList.js';
|
||||
@@ -205,6 +206,9 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
showDescriptions={itemForDisplay.showDescriptions}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'features_list' && (
|
||||
<FeaturesList features={itemForDisplay.features} />
|
||||
)}
|
||||
{itemForDisplay.type === 'agents_list' && (
|
||||
<AgentsStatus
|
||||
agents={itemForDisplay.agents}
|
||||
|
||||
@@ -415,7 +415,7 @@ describe('SettingsDialog', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Should wrap to last setting (without relying on exact bullet character)
|
||||
expect(lastFrame()).toContain('Hook Notifications');
|
||||
expect(lastFrame()).toContain('Zed Integration');
|
||||
});
|
||||
|
||||
unmount();
|
||||
@@ -753,6 +753,33 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
|
||||
describe('Specific Settings Behavior', () => {
|
||||
it('should render feature stage badges', async () => {
|
||||
const featureSettings = createMockSettings({
|
||||
'features.allAlpha': false,
|
||||
'features.allBeta': true,
|
||||
});
|
||||
|
||||
const onSelect = vi.fn();
|
||||
const { lastFrame, stdin } = render(
|
||||
<KeypressProvider>
|
||||
<SettingsDialog
|
||||
settings={featureSettings}
|
||||
onSelect={onSelect}
|
||||
availableTerminalHeight={100}
|
||||
/>
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
stdin.write('Plan');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Plan Mode');
|
||||
expect(lastFrame()).toContain('[ALPHA]');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show correct display values for settings with different states', async () => {
|
||||
const settings = createMockSettings({
|
||||
user: {
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
type SettingsValue,
|
||||
TOGGLE_TYPES,
|
||||
} from '../../config/settingsSchema.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { debugLogger, FeatureDefinitions } from '@google/gemini-cli-core';
|
||||
|
||||
import { useSearchBuffer } from '../hooks/useSearchBuffer.js';
|
||||
import {
|
||||
@@ -244,6 +244,14 @@ export function SettingsDialog({
|
||||
// The inline editor needs a string but non primitive settings like Arrays and Objects exist
|
||||
const editValue = getEditValue(type, rawValue);
|
||||
|
||||
let stage: string | undefined;
|
||||
const definitionKey = key.replace(/^features\./, '');
|
||||
if (key.startsWith('features.') && FeatureDefinitions[definitionKey]) {
|
||||
const specs = FeatureDefinitions[definitionKey];
|
||||
const latest = specs[specs.length - 1];
|
||||
stage = latest.preRelease;
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
label: definition?.label || key,
|
||||
@@ -254,6 +262,7 @@ export function SettingsDialog({
|
||||
scopeMessage,
|
||||
rawValue,
|
||||
editValue,
|
||||
stage,
|
||||
};
|
||||
});
|
||||
}, [settingKeys, selectedScope, settings]);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
/**
|
||||
* Represents a single item in the settings dialog.
|
||||
@@ -49,6 +50,8 @@ export interface SettingsDialogItem {
|
||||
rawValue?: SettingsValue;
|
||||
/** Optional pre-formatted edit buffer value for complex types */
|
||||
editValue?: string;
|
||||
/** Feature stage (e.g. ALPHA, BETA) */
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -553,6 +556,20 @@ export function BaseSettingsDialog({
|
||||
color={isActive ? theme.ui.focus : theme.text.primary}
|
||||
>
|
||||
{item.label}
|
||||
{item.stage && item.stage !== FeatureStage.GA && (
|
||||
<Text
|
||||
color={
|
||||
item.stage === FeatureStage.Deprecated
|
||||
? theme.status.error
|
||||
: item.stage === FeatureStage.Beta
|
||||
? theme.text.accent
|
||||
: theme.status.warning
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
[{item.stage}]{' '}
|
||||
</Text>
|
||||
)}
|
||||
{item.scopeMessage && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{' '}
|
||||
|
||||
54
packages/cli/src/ui/components/views/FeaturesList.test.tsx
Normal file
54
packages/cli/src/ui/components/views/FeaturesList.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FeaturesList } from './FeaturesList.js';
|
||||
import { type FeatureInfo } from '../../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
|
||||
const mockFeatures: FeatureInfo[] = [
|
||||
{
|
||||
key: 'alphaFeat',
|
||||
enabled: false,
|
||||
stage: FeatureStage.Alpha,
|
||||
since: '0.30.0',
|
||||
description: 'An alpha feature.',
|
||||
},
|
||||
{
|
||||
key: 'betaFeat',
|
||||
enabled: true,
|
||||
stage: FeatureStage.Beta,
|
||||
since: '0.29.0',
|
||||
description: 'A beta feature.',
|
||||
},
|
||||
{
|
||||
key: 'deprecatedFeat',
|
||||
enabled: false,
|
||||
stage: FeatureStage.Deprecated,
|
||||
since: '0.28.0',
|
||||
until: '0.31.0',
|
||||
description: 'A deprecated feature.',
|
||||
},
|
||||
];
|
||||
|
||||
describe('<FeaturesList />', () => {
|
||||
it('renders correctly with features', async () => {
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<FeaturesList features={mockFeatures} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with no features', async () => {
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<FeaturesList features={[]} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
174
packages/cli/src/ui/components/views/FeaturesList.tsx
Normal file
174
packages/cli/src/ui/components/views/FeaturesList.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type FeatureInfo } from '../../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
interface FeaturesListProps {
|
||||
features: FeatureInfo[];
|
||||
}
|
||||
|
||||
export const FeaturesList: React.FC<FeaturesListProps> = ({ features }) => {
|
||||
if (features.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.text.primary}> No features found</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const alphaFeatures = features.filter((f) => f.stage === FeatureStage.Alpha);
|
||||
const betaFeatures = features.filter((f) => f.stage === FeatureStage.Beta);
|
||||
const deprecatedFeatures = features.filter(
|
||||
(f) => f.stage === FeatureStage.Deprecated,
|
||||
);
|
||||
|
||||
// Column widths as percentages/proportions
|
||||
const colWidths = {
|
||||
feature: '40%',
|
||||
status: '20%',
|
||||
since: '20%',
|
||||
until: '20%',
|
||||
};
|
||||
|
||||
const renderSection = (
|
||||
title: string,
|
||||
sectionFeatures: FeatureInfo[],
|
||||
stageColor: string,
|
||||
) => {
|
||||
if (sectionFeatures.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text bold color={stageColor}>
|
||||
┃
|
||||
</Text>
|
||||
<Box marginLeft={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{' '}
|
||||
({sectionFeatures.length})
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Table Header */}
|
||||
<Box
|
||||
flexDirection="row"
|
||||
paddingX={1}
|
||||
borderStyle="single"
|
||||
borderTop={true}
|
||||
borderBottom={true}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Box width={colWidths.feature}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
FEATURE
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.status}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
ENABLED
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.since}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
SINCE
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.until}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
UNTIL
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Table Rows */}
|
||||
{sectionFeatures.map((feature) => (
|
||||
<Box
|
||||
key={feature.key}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingTop={1}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<Box width={colWidths.feature}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{feature.key}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.status}>
|
||||
<Box flexDirection="row">
|
||||
<Text>{feature.enabled ? '🟢 ' : '🔴 '}</Text>
|
||||
<Text
|
||||
color={
|
||||
feature.enabled
|
||||
? theme.status.success
|
||||
: theme.status.error
|
||||
}
|
||||
>
|
||||
{feature.enabled ? 'true' : 'false'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box width={colWidths.since}>
|
||||
<Text color={theme.text.secondary}>{feature.since || '—'}</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.until}>
|
||||
<Text color={theme.text.secondary}>{feature.until || '—'}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{feature.description && (
|
||||
<Box marginLeft={2} marginBottom={1}>
|
||||
<Text dimColor color={theme.text.primary}>
|
||||
{feature.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{renderSection('Alpha Features', alphaFeatures, theme.status.error)}
|
||||
{renderSection('Beta Features', betaFeatures, theme.status.warning)}
|
||||
{renderSection(
|
||||
'Deprecated Features',
|
||||
deprecatedFeatures,
|
||||
theme.text.secondary,
|
||||
)}
|
||||
|
||||
<Box flexDirection="row" marginTop={1} paddingX={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
💡 Use{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
/settings
|
||||
</Text>{' '}
|
||||
to enable or disable features. You can also use stage toggles like{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
allAlpha=true
|
||||
</Text>{' '}
|
||||
or{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
allBeta=false
|
||||
</Text>{' '}
|
||||
to toggle entire stages.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<FeaturesList /> > renders correctly with features 1`] = `
|
||||
"┃ Alpha Features (1)
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
FEATURE ENABLED SINCE UNTIL
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
alphaFeat 🔴 false 0.30.0 —
|
||||
An alpha feature.
|
||||
|
||||
|
||||
┃ Beta Features (1)
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
FEATURE ENABLED SINCE UNTIL
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
betaFeat 🟢 true 0.29.0 —
|
||||
A beta feature.
|
||||
|
||||
|
||||
┃ Deprecated Features (1)
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
FEATURE ENABLED SINCE UNTIL
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
deprecatedFeat 🔴 false 0.28.0 0.31.0
|
||||
A deprecated feature.
|
||||
|
||||
|
||||
|
||||
💡 Use /settings to enable or disable features. You can also use stage toggles like allAlpha=true
|
||||
or allBeta=false to toggle entire stages.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<FeaturesList /> > renders correctly with no features 1`] = `
|
||||
" No features found
|
||||
"
|
||||
`;
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
type SkillDefinition,
|
||||
type AgentDefinition,
|
||||
type ApprovalMode,
|
||||
type FeatureInfo,
|
||||
CoreToolCallStatus,
|
||||
checkExhaustive,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { PartListUnion } from '@google/genai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export type { ThoughtSummary, SkillDefinition };
|
||||
export type { ThoughtSummary, SkillDefinition, FeatureInfo };
|
||||
|
||||
export enum AuthState {
|
||||
// Attempting to authenticate or re-authenticate
|
||||
@@ -288,6 +289,11 @@ export type HistoryItemSkillsList = HistoryItemBase & {
|
||||
showDescriptions: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItemFeaturesList = HistoryItemBase & {
|
||||
type: 'features_list';
|
||||
features: FeatureInfo[];
|
||||
};
|
||||
|
||||
export type AgentDefinitionJson = Pick<
|
||||
AgentDefinition,
|
||||
'name' | 'displayName' | 'description' | 'kind'
|
||||
@@ -374,6 +380,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemExtensionsList
|
||||
| HistoryItemToolsList
|
||||
| HistoryItemSkillsList
|
||||
| HistoryItemFeaturesList
|
||||
| HistoryItemAgentsList
|
||||
| HistoryItemMcpStatus
|
||||
| HistoryItemChatList
|
||||
@@ -399,6 +406,7 @@ export enum MessageType {
|
||||
EXTENSIONS_LIST = 'extensions_list',
|
||||
TOOLS_LIST = 'tools_list',
|
||||
SKILLS_LIST = 'skills_list',
|
||||
FEATURES_LIST = 'features_list',
|
||||
AGENTS_LIST = 'agents_list',
|
||||
MCP_STATUS = 'mcp_status',
|
||||
CHAT_LIST = 'chat_list',
|
||||
|
||||
Reference in New Issue
Block a user