mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 08:24:10 -07:00
feat(ui): add /colors slash command to demo theme colors
This commit is contained in:
@@ -22,6 +22,7 @@ import { agentsCommand } from '../ui/commands/agentsCommand.js';
|
|||||||
import { authCommand } from '../ui/commands/authCommand.js';
|
import { authCommand } from '../ui/commands/authCommand.js';
|
||||||
import { bugCommand } from '../ui/commands/bugCommand.js';
|
import { bugCommand } from '../ui/commands/bugCommand.js';
|
||||||
import { chatCommand, debugCommand } from '../ui/commands/chatCommand.js';
|
import { chatCommand, debugCommand } from '../ui/commands/chatCommand.js';
|
||||||
|
import { colorsCommand } from '../ui/commands/colorsCommand.js';
|
||||||
import { clearCommand } from '../ui/commands/clearCommand.js';
|
import { clearCommand } from '../ui/commands/clearCommand.js';
|
||||||
import { commandsCommand } from '../ui/commands/commandsCommand.js';
|
import { commandsCommand } from '../ui/commands/commandsCommand.js';
|
||||||
import { compressCommand } from '../ui/commands/compressCommand.js';
|
import { compressCommand } from '../ui/commands/compressCommand.js';
|
||||||
@@ -83,6 +84,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
|||||||
...(this.config?.isAgentsEnabled() ? [agentsCommand] : []),
|
...(this.config?.isAgentsEnabled() ? [agentsCommand] : []),
|
||||||
authCommand,
|
authCommand,
|
||||||
bugCommand,
|
bugCommand,
|
||||||
|
colorsCommand,
|
||||||
{
|
{
|
||||||
...chatCommand,
|
...chatCommand,
|
||||||
subCommands: isNightlyBuild
|
subCommands: isNightlyBuild
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { colorsCommand } from './colorsCommand.js';
|
||||||
|
import { CommandKind, type CommandContext } from './types.js';
|
||||||
|
|
||||||
|
describe('colorsCommand', () => {
|
||||||
|
it('should have the correct metadata', () => {
|
||||||
|
expect(colorsCommand.name).toBe('colors');
|
||||||
|
expect(colorsCommand.description).toBe(
|
||||||
|
'Visualize the current theme colors',
|
||||||
|
);
|
||||||
|
expect(colorsCommand.kind).toBe(CommandKind.BUILT_IN);
|
||||||
|
expect(colorsCommand.autoExecute).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a COLORS message to the UI', () => {
|
||||||
|
const mockAddItem = vi.fn();
|
||||||
|
const mockContext = {
|
||||||
|
ui: {
|
||||||
|
addItem: mockAddItem,
|
||||||
|
},
|
||||||
|
} as unknown as CommandContext;
|
||||||
|
|
||||||
|
void colorsCommand.action?.(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockAddItem).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'colors',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SlashCommand } from './types.js';
|
||||||
|
import { CommandKind } from './types.js';
|
||||||
|
import type { HistoryItemColors } from '../types.js';
|
||||||
|
|
||||||
|
export const colorsCommand: SlashCommand = {
|
||||||
|
name: 'colors',
|
||||||
|
description: 'Visualize the current theme colors',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
autoExecute: true,
|
||||||
|
action: (context) => {
|
||||||
|
context.ui.addItem({
|
||||||
|
type: 'colors',
|
||||||
|
timestamp: new Date(),
|
||||||
|
} as HistoryItemColors);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
|
import { ColorsDisplay } from './ColorsDisplay.js';
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { themeManager } from '../themes/theme-manager.js';
|
||||||
|
import type { Theme, ColorsTheme } from '../themes/theme.js';
|
||||||
|
import type { SemanticColors } from '../themes/semantic-tokens.js';
|
||||||
|
|
||||||
|
describe('ColorsDisplay', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(themeManager, 'getSemanticColors').mockReturnValue({
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#cccccc',
|
||||||
|
link: '#0000ff',
|
||||||
|
accent: '#ff00ff',
|
||||||
|
response: '#ffffff',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
primary: '#000000',
|
||||||
|
message: '#111111',
|
||||||
|
input: '#222222',
|
||||||
|
diff: {
|
||||||
|
added: '#003300',
|
||||||
|
removed: '#330000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
default: '#555555',
|
||||||
|
focused: '#0000ff',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
comment: '#666666',
|
||||||
|
symbol: '#cccccc',
|
||||||
|
dark: '#333333',
|
||||||
|
gradient: undefined,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
error: '#ff0000',
|
||||||
|
success: '#00ff00',
|
||||||
|
warning: '#ffff00',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.spyOn(themeManager, 'getActiveTheme').mockReturnValue({
|
||||||
|
name: 'Test Theme',
|
||||||
|
type: 'dark',
|
||||||
|
colors: {} as unknown as ColorsTheme,
|
||||||
|
semanticColors: {} as unknown as SemanticColors,
|
||||||
|
} as unknown as Theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', async () => {
|
||||||
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
|
<ColorsDisplay />,
|
||||||
|
);
|
||||||
|
await waitUntilReady();
|
||||||
|
const output = lastFrame();
|
||||||
|
|
||||||
|
// Check for title and description
|
||||||
|
expect(output).toContain('/colors - Theme Colors Demo');
|
||||||
|
expect(output).toContain('visualize how colors are used');
|
||||||
|
|
||||||
|
// Check for Background section
|
||||||
|
expect(output).toContain('Background Colors');
|
||||||
|
|
||||||
|
// Check for active theme name
|
||||||
|
expect(output).toContain('Test Theme');
|
||||||
|
|
||||||
|
// Check for some color names and values
|
||||||
|
expect(output).toContain('text.primary');
|
||||||
|
expect(output).toContain('#ffffff');
|
||||||
|
expect(output).toContain('background.diff.added');
|
||||||
|
expect(output).toContain('#003300');
|
||||||
|
expect(output).toContain('border.default');
|
||||||
|
expect(output).toContain('#555555');
|
||||||
|
|
||||||
|
// Check for some descriptions
|
||||||
|
expect(output).toContain('Primary text color');
|
||||||
|
expect(output).toContain('Standard border color');
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { themeManager } from '../themes/theme-manager.js';
|
||||||
|
import { theme } from '../semantic-colors.js';
|
||||||
|
|
||||||
|
const COLOR_DESCRIPTIONS: Record<string, string> = {
|
||||||
|
'text.primary': 'Primary text color',
|
||||||
|
'text.secondary': 'Secondary/dimmed text color',
|
||||||
|
'text.link': 'Hyperlink and highlighting color',
|
||||||
|
'text.accent': 'Accent color for emphasis',
|
||||||
|
'text.response': 'Color for model response text',
|
||||||
|
'background.primary': 'Main terminal background color',
|
||||||
|
'background.message': 'Subtle background for message blocks',
|
||||||
|
'background.input': 'Background for the input prompt',
|
||||||
|
'background.diff.added': 'Background for added lines in diffs',
|
||||||
|
'background.diff.removed': 'Background for removed lines in diffs',
|
||||||
|
'border.default': 'Standard border color',
|
||||||
|
'border.focused': 'Border color when an element is focused',
|
||||||
|
'ui.comment': 'Color for code comments and metadata',
|
||||||
|
'ui.symbol': 'Color for technical symbols and UI icons',
|
||||||
|
'ui.dark': 'Deeply dimmed color for subtle UI elements',
|
||||||
|
'ui.gradient': 'Array of colors used for UI gradients',
|
||||||
|
'status.error': 'Color for error messages and critical status',
|
||||||
|
'status.success': 'Color for success messages and positive status',
|
||||||
|
'status.warning': 'Color for warnings and cautionary status',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StandardColorRow {
|
||||||
|
type: 'standard';
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GradientColorRow {
|
||||||
|
type: 'gradient';
|
||||||
|
name: string;
|
||||||
|
value: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackgroundColorRow {
|
||||||
|
type: 'background';
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorsDisplay: React.FC = () => {
|
||||||
|
const semanticColors = themeManager.getSemanticColors();
|
||||||
|
const activeTheme = themeManager.getActiveTheme();
|
||||||
|
|
||||||
|
const standardRows: StandardColorRow[] = [];
|
||||||
|
const backgroundRows: BackgroundColorRow[] = [];
|
||||||
|
const gradientRow: GradientColorRow | null =
|
||||||
|
semanticColors.ui.gradient && semanticColors.ui.gradient.length > 0
|
||||||
|
? {
|
||||||
|
type: 'gradient',
|
||||||
|
name: 'ui.gradient',
|
||||||
|
value: semanticColors.ui.gradient,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Flatten and categorize the SemanticColors object
|
||||||
|
for (const [category, subColors] of Object.entries(semanticColors)) {
|
||||||
|
if (category === 'ui' && 'gradient' in subColors) {
|
||||||
|
// Handled separately
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(subColors)) {
|
||||||
|
const fullName = `${category}.${name}`;
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
for (const [diffName, diffValue] of Object.entries(value)) {
|
||||||
|
if (typeof diffValue === 'string') {
|
||||||
|
if (category === 'background') {
|
||||||
|
backgroundRows.push({
|
||||||
|
type: 'background',
|
||||||
|
name: `${fullName}.${diffName}`,
|
||||||
|
value: diffValue,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
standardRows.push({
|
||||||
|
type: 'standard',
|
||||||
|
name: `${fullName}.${diffName}`,
|
||||||
|
value: diffValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
if (category === 'background') {
|
||||||
|
backgroundRows.push({
|
||||||
|
type: 'background',
|
||||||
|
name: fullName,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
standardRows.push({
|
||||||
|
type: 'standard',
|
||||||
|
name: fullName,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingX={1} marginY={1}>
|
||||||
|
<Box marginBottom={1} flexDirection="column">
|
||||||
|
<Text bold color={theme.text.accent}>
|
||||||
|
/colors - Theme Colors Demo
|
||||||
|
</Text>
|
||||||
|
<Text color={theme.text.secondary}>
|
||||||
|
The purpose of this feature is to visualize how colors are used in the
|
||||||
|
app, test across a variety of Terminals (Mac Terminal, Ghostty,
|
||||||
|
iTerm2, VSCode, etc), and see how the colors change across different
|
||||||
|
themes.
|
||||||
|
</Text>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color={theme.text.secondary}>
|
||||||
|
Active Theme:{' '}
|
||||||
|
<Text color={theme.text.primary} bold>
|
||||||
|
{activeTheme.name}
|
||||||
|
</Text>{' '}
|
||||||
|
({activeTheme.type})
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<Box
|
||||||
|
flexDirection="row"
|
||||||
|
marginBottom={1}
|
||||||
|
borderStyle="single"
|
||||||
|
borderColor={theme.border.default}
|
||||||
|
paddingX={1}
|
||||||
|
>
|
||||||
|
<Box width="15%">
|
||||||
|
<Text bold color={theme.text.link}>
|
||||||
|
Value
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box width="30%">
|
||||||
|
<Text bold color={theme.text.link}>
|
||||||
|
Name
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<Text bold color={theme.text.link}>
|
||||||
|
Usage
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Standard Section */}
|
||||||
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
|
{standardRows.map((row) => renderStandardRow(row))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Gradient Section */}
|
||||||
|
{gradientRow && (
|
||||||
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
|
{renderGradientRow(gradientRow)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Background Section */}
|
||||||
|
<Box flexDirection="column" marginTop={1}>
|
||||||
|
<Box marginBottom={1}>
|
||||||
|
<Text bold color={theme.text.accent}>
|
||||||
|
Background Colors
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{backgroundRows.map((row) => renderBackgroundRow(row))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderStandardRow({ name, value }: StandardColorRow) {
|
||||||
|
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||||
|
const isHex = value.startsWith('#');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={name} flexDirection="row" paddingX={1}>
|
||||||
|
<Box width="15%">
|
||||||
|
<Text color={isHex ? value : theme.text.primary}>{value}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box width="30%">
|
||||||
|
<Text color={theme.text.primary}>{name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<Text color={theme.text.secondary}>{description}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGradientRow({ name, value }: GradientColorRow) {
|
||||||
|
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={name} flexDirection="row" paddingX={1}>
|
||||||
|
<Box width="15%" flexDirection="row">
|
||||||
|
{value.map((c, i) => (
|
||||||
|
<Text key={i} color={c}>
|
||||||
|
{c}
|
||||||
|
{i < value.length - 1 ? ', ' : ''}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box width="30%">
|
||||||
|
<Text color={theme.text.primary}>{name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<Text color={theme.text.secondary}>{description}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBackgroundRow({ name, value }: BackgroundColorRow) {
|
||||||
|
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={name} flexDirection="row" paddingX={1} marginBottom={1}>
|
||||||
|
<Box
|
||||||
|
width="15%"
|
||||||
|
backgroundColor={value}
|
||||||
|
justifyContent="center"
|
||||||
|
paddingX={1}
|
||||||
|
>
|
||||||
|
<Text color={theme.text.primary} bold>
|
||||||
|
{value || 'default'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box width="30%" paddingLeft={1}>
|
||||||
|
<Text color={theme.text.primary}>{name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<Text color={theme.text.secondary}>{description}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import { HooksList } from './views/HooksList.js';
|
|||||||
import { ModelMessage } from './messages/ModelMessage.js';
|
import { ModelMessage } from './messages/ModelMessage.js';
|
||||||
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||||
import { HintMessage } from './messages/HintMessage.js';
|
import { HintMessage } from './messages/HintMessage.js';
|
||||||
|
import { ColorsDisplay } from './ColorsDisplay.js';
|
||||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
tier={itemForDisplay.tier}
|
tier={itemForDisplay.tier}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{itemForDisplay.type === 'colors' && <ColorsDisplay />}
|
||||||
{itemForDisplay.type === 'help' && commands && (
|
{itemForDisplay.type === 'help' && commands && (
|
||||||
<Help commands={commands} />
|
<Help commands={commands} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ export const useSlashCommandProcessor = (
|
|||||||
type: 'help',
|
type: 'help',
|
||||||
timestamp: message.timestamp,
|
timestamp: message.timestamp,
|
||||||
};
|
};
|
||||||
|
} else if (message.type === MessageType.COLORS) {
|
||||||
|
historyItemContent = {
|
||||||
|
type: 'colors',
|
||||||
|
timestamp: message.timestamp,
|
||||||
|
};
|
||||||
} else if (message.type === MessageType.STATS) {
|
} else if (message.type === MessageType.STATS) {
|
||||||
historyItemContent = {
|
historyItemContent = {
|
||||||
type: 'stats',
|
type: 'stats',
|
||||||
|
|||||||
@@ -360,6 +360,11 @@ export type HistoryItemHooksList = HistoryItemBase & {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HistoryItemColors = HistoryItemBase & {
|
||||||
|
type: 'colors';
|
||||||
|
timestamp: Date;
|
||||||
|
};
|
||||||
|
|
||||||
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
|
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
|
||||||
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
|
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
|
||||||
// 'tools' in historyItem.
|
// 'tools' in historyItem.
|
||||||
@@ -374,6 +379,7 @@ export type HistoryItemWithoutId =
|
|||||||
| HistoryItemWarning
|
| HistoryItemWarning
|
||||||
| HistoryItemAbout
|
| HistoryItemAbout
|
||||||
| HistoryItemHelp
|
| HistoryItemHelp
|
||||||
|
| HistoryItemColors
|
||||||
| HistoryItemToolGroup
|
| HistoryItemToolGroup
|
||||||
| HistoryItemStats
|
| HistoryItemStats
|
||||||
| HistoryItemModelStats
|
| HistoryItemModelStats
|
||||||
@@ -401,6 +407,7 @@ export enum MessageType {
|
|||||||
USER = 'user',
|
USER = 'user',
|
||||||
ABOUT = 'about',
|
ABOUT = 'about',
|
||||||
HELP = 'help',
|
HELP = 'help',
|
||||||
|
COLORS = 'colors',
|
||||||
STATS = 'stats',
|
STATS = 'stats',
|
||||||
MODEL_STATS = 'model_stats',
|
MODEL_STATS = 'model_stats',
|
||||||
TOOL_STATS = 'tool_stats',
|
TOOL_STATS = 'tool_stats',
|
||||||
@@ -442,6 +449,10 @@ export type Message =
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
content?: string; // Optional content, not really used for HELP
|
content?: string; // Optional content, not really used for HELP
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: MessageType.COLORS;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: MessageType.STATS;
|
type: MessageType.STATS;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
|
|||||||
Reference in New Issue
Block a user